nyan22 nyan22 - 7 days ago 3
Javascript Question

D3.js nesting and rollup at same time

I am looking to convert a csv file into a particular JSON format for a D3.js visualization. The basic idea is to simultaneously nest and rollup at each level. In other words, parents at every level in the structure would contain the sum of their children.

An example csv file is the following:

Country,State,City,Population,
"USA","California","Los Angeles",18500000,
"USA","California","San Diego",1356000,
"USA","California","San Francisco",837442,
"USA","Texas","Austin",885400,
"USA","Texas","Dallas",1258000,
"USA","Texas","Houston",2196000


The JSON format that I am hoping to create is as follows:

[
{
key: "USA",
Population: 25032842,
values: [
{
key: "California",
Population: 20693442,
values: [
{
key: "Los Angeles",
Population: 18500000
},
{
key: "San Diego",
Population: 1356000
},
{
key: "San Francisco",
Population: 837442
}
]
},
{
key: "Texas",
Population: 4339400,
values: [
{
key: "Austin",
Population: 885400
},
{
key: "Dallas",
Population: 1258000
},
{
key: "Houston",
Population: 2196000
}
]
}
]
}
]

Answer

Note: This will only work for D3 v3. Things have changed slightly with the new version 4, which requires a little adjustment when accessing the rollup's return value. This is covered by "D3.js nesting and rollup at the same time in v4".


D3 has no built-in function to do what you are looking for. Using nest.rollup() will prune all child nodes to replace them with this function's return value. However, writing a small helper function this can easily be done in two steps:

  1. Prepare the nested data structure using d3.nest():

    var nested = d3.nest()
      .key(function(d) { return d.Country; })
      .key(function(d) { return d.State; })
      .rollup(function(cities) {
        return cities.map(function(c) {
          return {"City": c.City, "Population": +c.Population };
        });
      })
      .entries(data);
    
  2. Loop through all top level nodes to recursively calculate the sums of all children.

    // Recursively sum up children's values
    function sumChildren(node) {
      node.Population = node.values.reduce(function(r, v) {
        return r + (v.values ? sumChildren(v) : v.Population);
      },0);
      return node.Population;
    }
    
    // Loop through all top level nodes in nested data,
    // i.e. for all countries.
    nested.forEach(function(node) {
      sumChildren(node);
    });
    

This will give you exactely the desired output. Have a look at the following snippet to see it in action.

// Initialization
var csv = 'Country,State,City,Population\n' + 
'"USA","California","Los Angeles",18500000\n' + 
'"USA","California","San Diego",1356000\n' + 
'"USA","California","San Francisco",837442\n' + 
'"USA","Texas","Austin",885400\n' + 
'"USA","Texas","Dallas",1258000\n' + 
'"USA","Texas","Houston",2196000\n';

var data = d3.csv.parse(csv);

// Nesting the input using d3.nest()
var nested = d3.nest()
  .key(function(d) { return d.Country; })
  .key(function(d) { return d.State; })
  .rollup(function(cities) {
    return cities.map(function(c) {
      return {"City": c.City, "Population": +c.Population };
    });
  })
  .entries(data);

// Recursively sum up children's values
function sumChildren(node) {
  node.Population = node.values.reduce(function(r, v) {
    return r + (v.values ? sumChildren(v) : v.Population);
  },0);
  return node.Population;
}

// Loop through all top level nodes in nested data,
// i.e. for all countries.
nested.forEach(function(node) {
  sumChildren(node);
});

// Output. Nothing of interest below this line.
d3.select("body").append("div")
  .style("font-family", "monospace")
  .style("white-space", "pre")
  .text(JSON.stringify(nested,null,2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>