emiguevara
emiguevara

Reputation: 1369

D3.js: use d3.nest() adding keys dynamically

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!

Upvotes: 2

Views: 4766

Answers (3)

mimau
mimau

Reputation: 127

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
}

Upvotes: 1

emiguevara
emiguevara

Reputation: 1369

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

Upvotes: 1

AmeliaBR
AmeliaBR

Reputation: 27544

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/

Upvotes: 5

Related Questions