2

I am using d3.nest() to convert flat JSON files into hierarchical trees. This works just fine if you know in advance the levels that found in your data: d3.nest() creates a nesting-level for each key function you pass it.

However, I would like to know if there is an elegant way to dynamically pass key functions. This might be an elementary question, but my Javascript skills are also elementary.

This is a static example working with key names stored in an array:

var levels = ['first', 'second', 'third']

var root = {
    "key":"root", 
    "values": 
        d3.nest()
            .key(function(d){ return d[levels[0]] })
            .key(function(d){ return d[levels[1]] })
            .key(function(d){ return d[levels[2]] })
            .entries(data)
}

My solution now is an if-else loop that pseudo-dynamically creates different nestings:

// build the nested tree pseudo-dynamically
if (levels.length === 1) {
    var nest = d3.nest()
           .key(function(d){return d[levels[0]]})
           .entries(data);
} else if (levels.length === 2) {
    var nest = d3.nest()
           .key(function(d){return d[levels[0]]})
           .key(function(d){return d[levels[1]]})
           .entries(data);
} else if (levels.length === 3) {
    var nest = d3.nest()
           .key(function(d){return d[levels[0]]})
           .key(function(d){return d[levels[1]]})
           .key(function(d){return d[levels[2]]})
           .entries(data);
}
[...]

Is there an elegant way to dynamically pass keys to d3.nest()? What I would like is something in principle like the following example (which DOES NOT WORK):

var root = {
    "key":"root", 
    "values": 
        d3.nest()
            for (var i = 0; i < levels.length; i++) {
                .key(function(d){return d[levels[i]]})
            }
            .entries(data)
}

Thanks for your time!

emiguevara
  • 1,359
  • 13
  • 26
  • It sounds like nesting isn't the right thing here. [This question](http://stackoverflow.com/questions/20913689/complex-d3-nest-manipulation) may help. – Lars Kotthoff Mar 19 '14 at 19:29

3 Answers3

5

It's not as easy as you hypothetical elegant code, but you can certainly simplify it compared to your if-else method.

A first instinct is to do something like this:

var levels = ['first', 'second', 'third']

var nest = d3.nest();
for (var i = 0; i < levels.length; i++) {
    nest = nest.key(function(d){return d[levels[i]]});
    //create a new nesting function that has one more key function added
    //and save it in the variable
}

var root = {
      "key":"root", 
      "values": nest.entries(data) //compute the nest
    }

However, this doesn't work because your nesting function won't be used until after you create all your nesting functions, so by the time the nest is actually run your i variable equals 3 and levels[i] returns undefined, and therefore d[levels[i]] returns undefined. Example here.

You need to create a separate function enclosure so that the correct value from levels is locked in:

function createNestingFunction(propertyName){
  return function(d){ 
            return d[propertyName];
         };
}

var levels = ['first', 'second', 'third']

var nest = d3.nest();
for (var i = 0; i < levels.length; i++) {
    nest = nest.key( createNestingFunction(levels[i]) );
    //create a new nesting function that has one more key function added
    //and save it in the variable

    //the function `createNestingFunction` is called *immediately*
    //with a parameter based on the current value of `i`
    //the returned function will always use that parameter,
    //regardless of how many times createNestingFunction is called
}

var root = {
      "key":"root", 
      "values": nest.entries(data) //compute the nest
    }

The working version of the example: http://fiddle.jshell.net/brVLH/1/

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • Thanks for such a good answer! That works very well and it is quite elegant actually! – emiguevara Mar 20 '14 at 09:53
  • what if i need to identify level based on group by ? suppose I have two group by as key than how should i know that this level is based on this values ? somehow i can add the attr that i use to group-by with key attr like attkey etc ? – Amit Rana Jul 30 '15 at 11:47
  • @AmitRana I'm not quite sure what you mean. Maybe start a new question so that you can explain it more completely (with simple examples) and others can try to help? – AmeliaBR Jul 31 '15 at 14:41
  • @AmeliaBR, thank you, very useful. I've preferred to use forEach instead of the for loop, so you don't need the separate function: `levels.forEach(function (k) { nest = nest.key(function (d) { return d[k] }); });` – Luis Palacios Mar 09 '17 at 07:31
1

Found another possible solution after posting the question. It uses forEach and it is described here.

Community
  • 1
  • 1
emiguevara
  • 1,359
  • 13
  • 26
  • no adding multiple key is not problem ... What i wanted When i add multiple keys it group them based on that keys ...for each level I just want is which level is group by on which key ... – Amit Rana Jul 31 '15 at 14:43
  • I seriously do not understand your comment, @Amit – emiguevara Aug 07 '15 at 13:55
  • Follow last example here http://stackoverflow.com/questions/31723061/add-attribute-with-nested-entries-in-d3-js – Amit Rana Aug 07 '15 at 14:06
1

A simpler solution, following AmeliaBR one:

let levels = ['first', 'second', 'third']
// create the nest
let nest = d3.nest()
// for each value in 'levels' array
// add the key() function
levels.forEach(function(level){
  // re-assign to the nest key
  nest = nest.key(d => d[level])
});
// compute the nest
nest = nest.entries(data)
// add the root
let root = {
  "key":"root", 
  "values": nest
}
mimau
  • 118
  • 6