Search code examples
javascriptd3.jssunburst-diagram

d3.nest with variable children depth


I have a tsv file with a simple "type" "count" content like this:

type    count
level1/level2/level3/Foo    24
level1/level2/level3/Bar    2
level1/level2/Baz   28
level1/level2/Quz   3
...

The level strings can be any String, i just named them like this here, to convey the meaning. The last element of the type, Foo, Bar, etc. can be considered to be the leafs of the data.

Using d3, i want to turn this into a sunburst diagram. To do so, i use the d3.nest function to break up the type at the slashes.

d3.tsv("data.tsv", function(error, data) {
  data.forEach(function(d) {
    d.count = +d.count;
  });

  // define key functions for a depth up to 4 
  var nestedData = d3.nest()
     .key(function(d) { return level(d.type, 2); })
     .key(function(d) { return level(d.type, 3); })
     .key(function(d) { return level(d.type, 4); })
     .entries(data);

  // create the key for a specified level 
  function level(type, l) {
     var parts = type.split("/");
     var result = "";
     for (i = 0; i < l && i < parts.length; i++) {
        result += parts[i];
        if (i < l-1 && i < parts.length-1) {
            result += "/";
        }
     }
     return result;
  }
  ...

The issue here is that the resulting nestedData always has the entries/leafs at a depth of 4. In the example data you can see, that the leafs can be at any depth.

How can i construct nested data so that the entries can occur at any depth, not only at a predefined depth?


Solution

  • Since d3.nest makes you register the nesting keys ahead of time before you call nest.entries there is no way to do a nesting with a variable number of levels by specifying everything up front.

    One thing that might work is to recursively use a nest.rollup function to control what the values at each level look like. Inside your rollup you can decide if each entry element should be a new d3.nest containing the next level of detail, or if it should be a leaf node.

    Here's a rough crack at it (assumes level1 will always be present):

    function rollupFn(group) {
        var leaves = [],
            groups = [];
    
        // Split the items into leaves and groups
        group.forEach(function(item) {
            // First eliminate level already accounted for by d3.nest key
            item.type = item.type.substr(item.type.indexOf('/')+1);
    
            if (item.type.indexOf('/') > -1) {
                groups.push(item);
            } else {
                leaves.push(item);
            }
        });
    
        // Convert the "group" items into a d3.nest object
        groups = d3.nest().key(function(d) {
                return d.type.split('/')[0];
            })
            .rollup(rollupFn)
            .entries(groups);
    
        var results = [];
        if (groups.length > 0) { results.push(groups); }
        if (leaves.length > 0) { results.push(leaves); }
        return results;
    };
    
    var nestedData = d3.nest()
        .key(function(d) { return "level1" })  // optional: first group contains everything
        .rollup(rollupFn)
        .entries(data);
    

    The values at each level will consist of one element containing a d3.nest object representing all lower levels followed by all of the leaf nodes. If there are no further levels, the values array will be all leaf nodes.

    Here is some working code you can step through: http://jsfiddle.net/S8aMU/10