D3 in Depth covers version 6 of D3
Home About
Build a real-world, custom, interactive and beautiful data visualization from scratch using D3.
Over 50 online text-based lessons on D3, interaction, code architecture, state management, styling and more.
Visit course page

Hierarchies

Hierarchical data is data that consists of two or more layers.

For example, consider the following city data:

CountryCityPopulation
JapanTokyo7977000
IndonesiaJakarta34540000
IndiaDelhi29617000
IndiaMumbai23355000
PhilippinesManila23088000
ChinaShanghai22120000
BrazilSao Paulo22046000
South KoreaSeoul21794000
MexicoMexico City20996000
ChinaGuangzhou20902000
ChinaBeijing19433000
EgyptCairo19372000
United StatesNew York18713220
IndiaKolkata17560000

If you group the countries together the data looks like:

CountryCityPopulation
JapanTokyo7977000
IndonesiaJakarta34540000
IndiaDelhi29617000
Mumbai23355000
Kolkata17560000
PhilippinesManila23088000
ChinaShanghai22120000
Guangzhou20902000
Beijing19433000
BrazilSao Paulo22046000
South KoreaSeoul21794000
MexicoMexico City20996000
EgyptCairo19372000
United StatesNew York18713220

The data now has a hierarchical structure. At the top level are countries (Japan, Indonesia, India, Philippines etc.). The next level consists of cities. Each country has one or more cities. For example, India has three cities: Delhi, Mumbai and Kolkata.

Hierarchical data can have any number of levels. For example, a region column can be added to the previous example and a hierarchy consisting of regions at the top level, countries at the next level and cities at the final level can be created.

There are a number of chart types that let you visualise hierarchical data including trees:

treemaps:

packed circles:

and sunburst charts:

There are a number of ways of expressing hierarchical data. It can be expressed in tabular form (as in the previous examples) but if you’re using D3 to produce the above charts it’ll need to be in the form of a nested object such as:

{
  "name": "Countries",
  "children": [
    {
      "name": "Japan",
      "children": [
        {
          "name": "Tokyo",
          "population": 7977000
        }
      ]
    },
    {
      "name": "Indonesia",
      "children": [
        {
          "name": "Jakarta",
          "population": 34540000
        }
      ]
    },
    {
      "name": "India",
      "children": [
        {
          "name": "Delhi",
          "population": 29617000
        },
        {
          "name": "Mumbai",
          "population": 23355000
        },
        {
          "name": "Kolkata",
          "population": 17560000
        }
      ]
    },
    ...
  ]
}

D3 uses layout functions to help you create hierarchical charts. In essence a layout function takes your data as input and adds visual variables such as position and size to it.

For example the tree layout takes a hierarchical data structure and adds x and y values to each node such that the nodes form a tree-like shape:

In this chapter we’ll look at the tree, cluster, treemap, pack and partition layouts. Note that treemap, pack and partition are designed to lay out hierarchies where the nodes have an associated numeric value (e.g. revenue, population etc.).

D3 version 4 and up requires the hierarchical data to be in the form of a d3.hierarchy object which we’ll cover next.

d3.hierarchy

The layout functions in this chapter accept a d3.hierarchy object as input. This is a data structure that represents a hierarchy and has a number of methods for retrieving things like ancestor, descendant and leaf nodes and for computing the path between nodes.

If you have a nested object like:

var data = {
  "name": "A1",
  "children": [
    {
      "name": "B1",
      "children": [
        {
          "name": "C1",
          "value": 100
        },
        {
          "name": "C2",
          "value": 300
        },
        {
          "name": "C3",
          "value": 200
        }
      ]
    },
    {
      "name": "B2",
      "value": 200
    }
  ]
}

you can create a D3 hierarchy object using:

var root = d3.hierarchy(data);

The object returned by d3.hierarchy has some useful methods such as:

root.descendants();  // return a flat array of root's descendants
root.links();        // return a flat array of parent-child links

Tree layout

The tree layout arranges the nodes of a hierarchy in a tree like arrangement.

Start by creating a tree layout function using d3.tree():

var treeLayout = d3.tree();

d3.tree() returns a layout function into which you can pass a hierarchy object.

You can configure the tree’s size using .size:

treeLayout.size([400, 200]);

You can then call treeLayout, passing in the hierarchy object root that was defined above:

treeLayout(root);

This’ll write x and y values on each node of root.

To draw the nodes:

  • use root.descendants() to get an array of all the nodes
  • join this array to circles (or any other type of SVG element)
  • use x and y to position the circles

To draw the links:

  • use root.links() to get an array of all the links
  • join the array to line (or path) elements
  • use x and y of the link’s source and target properties to position the line

root.links() returns an array where each element is an object containing two properties source and target which represent the link’s source and target nodes.

// Nodes
d3.select('svg g.nodes')
  .selectAll('circle.node')
  .data(root.descendants())
  .join('circle')
  .classed('node', true)
  .attr('cx', function(d) {return d.x;})
  .attr('cy', function(d) {return d.y;})
  .attr('r', 4);

// Links
d3.select('svg g.links')
  .selectAll('line.link')
  .data(root.links())
  .join('line')
  .classed('link', true)
  .attr('x1', function(d) {return d.source.x;})
  .attr('y1', function(d) {return d.source.y;})
  .attr('x2', function(d) {return d.target.x;})
  .attr('y2', function(d) {return d.target.y;});

Cluster layout

The cluster layout is very similar to the tree layout the main difference being all leaf nodes are placed at the same depth.

var clusterLayout = d3.cluster()
  .size([400, 200]);

var root = d3.hierarchy(data);

clusterLayout(root);

Treemap layout

Treemaps were invented by Ben Shneiderman to visually represent hierarchies where each item has an associated value.

For example, imagine you have country population data where each country has a region and a population value.

You can use a treemap to represent each region as a rectangle. Each region consists of smaller rectangles which represent a country. Each country is sized proportionally to the population:

Create a treemap layout function by calling d3.treemap() :

var treemapLayout = d3.treemap();

As before you can configure the layout:

treemapLayout
  .size([400, 200])
  .paddingOuter(10);

Before applying this layout to your hierarchy you must run .sum() on the hierarchy. This traverses the tree and sets .value on each node to be the sum of its children:

root.sum(function(d) {
  return d.value;
});

Note an accessor function has been passed into .sum() to specify which property to sum.

You can now call treemapLayout, passing in the hierarchy object root that was defined earlier:

treemapLayout(root);

The treemap layout function adds 4 properties x0, x1, y0 and y1 to each node which specify the dimensions of each rectangle in the treemap.

Now you can join the nodes to rect elements and update the x, y, width and height properties of each rect:

d3.select('svg g')
  .selectAll('rect')
  .data(root.descendants())
  .join('rect')
  .attr('x', function(d) { return d.x0; })
  .attr('y', function(d) { return d.y0; })
  .attr('width', function(d) { return d.x1 - d.x0; })
  .attr('height', function(d) { return d.y1 - d.y0; })

If you’d like labels in each rectangle you can join g elements to the array and add rect and text elements to each g:

var nodes = d3.select('svg g')
  .selectAll('g')
  .data(rootNode.descendants())
  .join('g')
  .attr('transform', function(d) {return 'translate(' + [d.x0, d.y0] + ')'})

nodes
  .append('rect')
  .attr('width', function(d) { return d.x1 - d.x0; })
  .attr('height', function(d) { return d.y1 - d.y0; })

nodes
  .append('text')
  .attr('dx', 4)
  .attr('dy', 14)
  .text(function(d) {
    return d.data.name;
  })

treemap layouts can be configured in a number of ways:

  • the padding around a node’s children can be set using .paddingOuter
  • the padding between sibling nodes can be set using .paddingInner
  • outer and inner padding can be set at the same time using .padding
  • the outer padding can also be fine tuned using .paddingTop, .paddingBottom, .paddingLeft and .paddingRight.

In the above example paddingTop is 20 and paddingInner is 2.

Treemaps have more than one strategy for arranging the rectangles. D3 has a few built-in ones such as treemapBinary, treemapDice, treemapSlice, treemapSliceDice and treemapSquarify.

treemapBinary strives for a balance between horizontal and vertical partitions, treemapDice partitions horizontally, treemapSlice partitions vertically, treemapSliceDice alternates between horizontal and vertical partioning and treemapSquarify allows the aspect ratio of the rectangles to be influenced.

You can select a tiling strategy using the .tile method:

treemapLayout.tile(d3.treemapDice)

The effect of different squarify ratios can be seen here.

Pack layout

The pack layout is similar to the tree layout but circles are used to represent nodes.

In this example each country is represented by a circle (sized according to population) and the countries are grouped by region.

Create a pack layout function using d3.pack():

var packLayout = d3.pack();

As before you can configure its size by passing an array [width, height] into the .size method:

packLayout.size([300, 300]);

As with the treemap you must call .sum() on the hierarchy object root before applying the pack layout:

rootNode.sum(function(d) {
  return d.value;
});

packLayout(rootNode);

The pack layout adds x, y and r (for radius) properties to each node.

Now you can join circle elements to each descendant of root:

d3.select('svg g')
  .selectAll('circle')
  .data(rootNode.descendants())
  .join('circle')
  .attr('cx', function(d) { return d.x; })
  .attr('cy', function(d) { return d.y; })
  .attr('r', function(d) { return d.r; })

Labels can be added by creating g elements for each descendant:

var nodes = d3.select('svg g')
  .selectAll('g')
  .data(rootNode.descendants())
  .join('g')
  .attr('transform', function(d) {return 'translate(' + [d.x, d.y] + ')'})

nodes
  .append('circle')
  .attr('r', function(d) { return d.r; })

nodes
  .append('text')
  .attr('dy', 4)
  .text(function(d) {
    return d.children === undefined ? d.data.name : '';
  })

The padding around each circle can be configured using .padding():

packLayout.padding(10)

Partition layout

The partition layout subdivides a rectangular space into layers, each of which represents a layer in the hierarchy. Each layer is further subdivided for each node in the layer:

Create a partition layout function using d3.partition():

var partitionLayout = d3.partition();

As before you can configure its size by passing an array [width, height] into the .size method:

partitionLayout.size([400, 200]);

As with the treemap you must call .sum() on the hierarchy object root and before applying the partition layout:

rootNode.sum(function(d) {
  return d.value;
});

partitionLayout(rootNode);

The partition layout adds x0, x1, y0 and y1 properties to each node.

You can now join rect elements to each descendant of root:

d3.select('svg g')
  .selectAll('rect')
  .data(rootNode.descendants())
  .join('rect')
  .attr('x', function(d) { return d.x0; })
  .attr('y', function(d) { return d.y0; })
  .attr('width', function(d) { return d.x1 - d.x0; })
  .attr('height', function(d) { return d.y1 - d.y0; });

Padding can be added between nodes using .padding():

partitionLayout.padding(2);

If you’d like to change the orientation of the partition layout so that the layers run left to right you can swap x0 with y0 and x1 with y1 when defining the rect elements:

  .attr('x', function(d) { return d.y0; })
  .attr('y', function(d) { return d.x0; })
  .attr('width', function(d) { return d.y1 - d.y0; })
  .attr('height', function(d) { return d.x1 - d.x0; });

You can also map the x dimension into a rotation angle and y into a radius to create a sunburst partition:

Stay up to date with D3 related articles, tutorials and courses