Chapter 06Treemaps

A treemap is a rectangular visualization for a hierarchal model as shown below.

<script>
var data = {"value": 45.25, "children": [
                {"value": 2.25},
                {"value": 1},
                {"value": 3},
                {"value": 3},
                {"value": 4},
                {"value": 3},
                {"value": 6},
                {"value": 11},
                {"value": 3},
                {"value": 2},
                {"value": 5.25},
                {"value": 1.50},
                {"value": 0.25}
            ]};

var root = d3.hierarchy(data);

var treemap = d3.treemap()
  .size([580,250])
  .paddingOuter(10);

treemap(root);

d3.select("#demo1")
  .selectAll('rect.node')
  .data(root.descendants())
  .enter()
  .append('rect')
  .classed('node', true)
  .attr('x', d => d.x0)
  .attr('y', d => d.y0)
  .attr('width', d => d.x1 - d.x0)
  .attr('height', d => d.y1 - d.y0);
</script>

<svg id="demo1" width=580 height=250></svg>

Each node in the model is represented by a rectangle in the visualization and the rectangles for child nodes are placed within the rectangles for the parent nodes. For example, in the treemap shown above, each of the darker green inner rectangles represent nodes that are children of a parent node that is represented by the lighter green outer rectangle.

The Treemap Layout

d3.treemap() is a function generator that returns a new treemap layout. When we call the treemap layout, we pass it the root of a hierarchy model. It then computes the location for each rectangle and sets 4 properties in each node: x0, y0, x1, and y1. These properties represent the top left and bottom right corners of the rectangle.

The proportion of space that a child’s rectangle covers inside its parent’s rectangle is based on the values in the child’s and the parent’s value attributes. For example, to fully pack a parent rectangle with rectangles for its children, the parent’s value property should be equal to the sum of the childrens’ value properties.

The location of each rectangle is determined by the tiling method used. D3 supports 6 built-in tiling methods which will be discussed later. The default tiling method is d3.treemapSquarify. To change the tiling method we can call treemap.tile([tile]).

The default size of the outermost rectangle is [1,1]. To change the size we can call treemap.size([width,height]).

Creating a Treemap

To create a treemap we do the following:

Setting the Value Properties

The D3 node object has various methods that can be called to set the value properties if the objects in the data passed to d3.hierarchy don’t have value properties. Both node.sum(function) and node.eachAfter(function) traverse the herarchy in post-order traversal.

node.sum is an accumulator method that takes a function as an argument and returns the node. The function that is passed to node.sum is called for each node and when called, it is passed the data object (not the node itself) associated with the node. The value set in the node’s value property is equal to the value returned by the sum's function argument plus the sum of the values stored in each child node’s value property.

In the two examples below, our data consists of a root node for New York City, and 5 child nodes, one for each borough. Each borough object has a population property, but the root node does not. None of the nodes have a value property.

We can create value properties in each node in various ways. The simplest way is to call root.sum and pass it a function which returns the population value if one exists otherwise it returns 0.

root.sum(d => d.hasOwnProperty("population") ? d.population : 0);

Recall, the sum method sets the value properties automatically. Since the borough nodes have population properties and do not have children, their value property in each node will be set equal to the value in their population property plus 0. The node for New York City, however does not have a population property but does have children, so the value for its value property will be set to 0 plus the sum of its children’s value properties.

An alternative way to set the value properties is to set them manually using node.eachAfter, as shown below and as illustrated in the second example.

root.eachAfter(d => {
  if (!d.hasOwnProperty("children")) {
    d.value = d.data.population;
  }
  else {
    d.value = 0;
    for (var i in d.children)
        d.value += d.children[i].value;
  }
});

<script>
var data = {"name": "New York City", "children": [
                {"name": "Bronx", "population": 1471160},
                {"name": "Brooklyn", "population": 2648771},
                {"name": "Manhattan", "population": 1664727},
                {"name": "Queens", "population": 2358582},
                {"name": "Staten Island", "population": 479458}
            ]};

var drawTreemap = function(id) {
  let root = d3.hierarchy(data);

  if (id === "demo2") {
    root.sum(d => d.hasOwnProperty("population") ? d.population : 0);
  }
  else if (id === "demo3") {
    root.eachAfter(d => {
      if (!d.hasOwnProperty("children")) {
        d.value = d.data.population;
      }
      else {
        d.value = 0;
        for (var i in d.children)
            d.value += d.children[i].value;
      }
    });
  }

  let treemap = d3.treemap()
    .size([250,250])
    .paddingOuter(10);

  treemap(root);

  d3.select("#" + id)
    .selectAll('rect.node')
    .data(root.descendants())
    .enter()
    .append('rect')
    .classed('node', true)
    .attr('x', d => d.x0)
    .attr('y', d => d.y0)
    .attr('width', d => d.x1 - d.x0)
    .attr('height', d => d.y1 - d.y0);
};

drawTreemap("demo2");
drawTreemap("demo3");
</script>

<svg id="demo2" width=250 height=250></svg>
<svg id="demo3" width=250 height=250></svg>

Sorting the Nodes

A treemap visualization is often more informative when sibling rectangles are positioned from largest to smallest. To accomplish this we simply need to sort the nodes using node.sort(compare) prior to calling the treemap layout. In the example below we set the value properties and then sort the nodes by descending height and for nodes at the same height by descending value.

var root = d3.hierarchy(data)
  .sum(d => d.hasOwnProperty("population") ? d.population : 0)
  .sort((a, b) => b.height - a.height || b.value - a.value);
<script>
var data = {"name": "New York City", "children": [
                {"name": "Bronx", "population": 1471160},
                {"name": "Brooklyn", "population": 2648771},
                {"name": "Manhattan", "population": 1664727},
                {"name": "Queens", "population": 2358582},
                {"name": "Staten Island", "population": 479458}
            ]};

var root = d3.hierarchy(data)
  .sum(d => d.hasOwnProperty("population") ? d.population : 0)
  .sort((a, b) => b.value - a.value);

var treemap = d3.treemap()
  .size([250,250])
  .paddingOuter(10);

treemap(root);

d3.select("#demo4")
  .selectAll('rect.node')
  .data(root.descendants())
  .enter()
  .append('rect')
  .classed('node', true)
  .attr('x', d => d.x0)
  .attr('y', d => d.y0)
  .attr('width', d => d.x1 - d.x0)
  .attr('height', d => d.y1 - d.y0);
</script>

<svg id="demo4" width=250 height=250></svg>

Tiling Methods

D3 provides the following 6 tiling methods:

We can change the tiling method using treemap.tile([tile]). Below are examples of each of the tiling methods on the same data set. For more information on these tiling methods please consult the D3 API documentation.

<script>
var data = {"name": "New York City", "children": [
                {"name": "Bronx", "population": 1471160},
                {"name": "Brooklyn", "population": 2648771},
                {"name": "Manhattan", "population": 1664727},
                {"name": "Queens", "population": 2358582},
                {"name": "Staten Island", "population": 479458}
            ]};

var root = d3.hierarchy(data)
  .sum(d => d.hasOwnProperty("population") ? d.population : 0)
  .sort((a, b) => b.value - a.value);

var renderRect = function(id, tileMap) {

  var treemap = d3.treemap()
    .size([150,150])
    .tile(tileMap)
    .paddingOuter(5);

  treemap(root);

  d3.select("#" + id)
    .selectAll('rect.node')
    .data(root.descendants())
    .enter()
    .append('rect')
    .classed('node', true)
    .attr('x', d => d.x0)
    .attr('y', d => d.y0)
    .attr('width', d => d.x1 - d.x0)
    .attr('height', d => d.y1 - d.y0);
};

renderRect("demo5", d3.treemapBinary);
renderRect("demo6", d3.treemapDice);
renderRect("demo7", d3.treemapSlice);
renderRect("demo8", d3.treemapSliceDice);
renderRect("demo9", d3.treemapSquarify);
renderRect("demo10", d3.treemapResquarify);

</script>

<svg id="demo5" name="treemapBinary" width=150 height=150></svg>
<svg id="demo6" width=150 height=150></svg>
<svg id="demo7" width=150 height=150></svg><br>
<svg id="demo8" width=150 height=150></svg>
<svg id="demo9" width=150 height=150></svg>
<svg id="demo10" width=150 height=150></svg>

Rounding

Per D3v3 the treemap.round([round]) method takes a boolean as an argument and “sets whether or not the treemap layout will round to exact pixel boundaries. This can be nice to avoid antialiasing artifacts in SVG.”

Padding

Each of the padding methods takes either a number or a function as an argument. By default the inner padding is 0

Below are demonstrations of each of these in the order given above.

<script>
var data = {"value": 45.25, "children": [
                {"value": 2.25},
                {"value": 1},
                {"value": 3},
                {"value": 3},
                {"value": 4},
                {"value": 3},
                {"value": 6},
                {"value": 11},
                {"value": 3},
                {"value": 2},
                {"value": 5.25},
                {"value": 1.50},
                {"value": 0.25}
            ]};

var root = d3.hierarchy(data);

var renderRect = function(id) {

  let treemap = d3.treemap()
    .size([150,150]);

  if (id === "demo12")
    treemap.padding(10);
  else if (id === "demo13")
    treemap.paddingInner(10);
  else if (id === "demo14")
    treemap.paddingOuter(10);
  else if (id === "demo15")
    treemap.paddingTop(10);
  else if (id === "demo16")
    treemap.paddingRight(10);
  else if (id === "demo17")
    treemap.paddingBottom(10);
  else if (id === "demo18")
    treemap.paddingLeft(10);

  treemap(root);

  d3.select("#" + id)
    .selectAll('rect.node')
    .data(root.descendants())
    .enter()
    .append('rect')
    .classed('node', true)
    .attr('x', d => d.x0)
    .attr('y', d => d.y0)
    .attr('width', d => d.x1 - d.x0)
    .attr('height', d => d.y1 - d.y0)
    .attr('fill', d => d.height == 0 ? "cadetblue" : "pink");
};

renderRect("demo12");
renderRect("demo13");
renderRect("demo14");
renderRect("demo15");
renderRect("demo16");
renderRect("demo17");
renderRect("demo18");

</script>

<svg id="demo12" width=150 height=150></svg>
<svg id="demo13" width=150 height=150></svg>
<svg id="demo14" width=150 height=150></svg><br>
<svg id="demo15" width=150 height=150></svg>
<svg id="demo16" width=150 height=150></svg>
<svg id="demo17" width=150 height=150></svg>
<svg id="demo18" width=150 height=150></svg>