Software! Math! Data! The blog of R. Sean Bowman
The blog of R. Sean Bowman
August 13 2016

D3 is a Javascript library for creating charts, animations, and other visualizations. It’s awesome. One of its key features is the easy creation of a DOM heirarchy mirroring some data you’ve carefully constructed for the job. Here’s a way to create a hierarchy of elements where the elements in each level aren’t necessarily homogeneous. I used this to make a table with a header column, but I’m sure it has lots of uses.

Selections, Joins, and Nesting

Idiomatic D3 uses joins to create groups of elements without explicit looping. These can be used together with nested selections to make pretty complicated DOM subtrees in a very concise way. For example, we can make the table

Bob4
Jill11

with the following code, assuming that #container is the selector of an element we want to add a table to:

var table_data = [
  ["Bob", 4],
  ["Jill, 11"]
];

var table = d3.select("#container")
  .append("table")
  .append("tbody");

table.selectAll("tr")
  .data(table_data)
  .enter()
  .append("tr")
  .selectAll("td")
  .data(function(d) { return d; })
  .enter()
  .append("td")
  .html(function(d) { return d; });

This is great, but most of the examples I’ve seen (including the ones I liked to above) create selections that are homogeneous at a particular level. That is, all the children of the tr elements are tds, and so on. What if we have a table that needs a header column, so that not every child of a row is a td? Here’s an example:

SizeColor
x3red
y7blue

In HTML this looks like

<table>
  <thead>
    <tr><th><th><th>Size</th><th>Color</th></tr>
  </thead>
  <tbody>
    <tr><th>x</th><td>3</td><td>red</td></tr>
    <tr><th>y</th><td>7</td><td>blue</td></tr>
  </tbody>
</table>

The problem is that naive use of D3’s append verb only gets us the same type of elements, all td for example. However, we can append more than once by reusing the selection we create with the first join, like this:

var table_header = [
  "", "Size", "Color"
];

var table_data = [
  {name: "x", size: 3, color: "red"},
  {name: "y", size: 7, color: "blue"}
];

var div = d3.select("#table-goes-here")
  .append("table")
  .attr("class", "table");

div.append("thead")
  .append("tr")
  .selectAll("th")
  .data(table_header)
  .enter()
  .append("th")
  .html(function(d) { return d; });

var trs = div.append("tbody")
  .selectAll("tr")
  .data(table_data)
  .enter()
  .append("tr");

trs.append("th")
  .html(function(d) { return d.name; });

trs.selectAll("td")
  .data(function(d) { return [d.size, d.color]; })
  .enter()
  .append("td")
  .html(function(d) { return d; });

Here, we create the header and body in two steps, and when we get to the body, we create all the elements in the first column (the th elements) and then all the rest of the table cells.

Another way to do the same thing leverages D3’s use of functions as arguments in lots of its API. Here’s what it looks like:

var table_mdata = [
  {t: "thead", cells: [[
    {t: "th", v: ""}, {t: "th", v: "x"}, {t: "th", v: "y"}
  ]]},
  {t: "tbody", cells: [
    [{t: "th", v: "Foo"}, {t: "td", v: "3"}, {t: "td", v: "2"}],
    [{t: "th", v: "Bar"}, {t: "td", v: "6"}, {t: "td", v: "4"}],
    [{t: "th", v: "Baz"}, {t: "td", v: "9"}, {t: "td", v: "5"}]
  ]}
];

div2.selectAll("thead").data(table_mdata)
  .enter()
  .append(function(d) { return document.createElement(d.t); })
  .selectAll("tr")
  .data(function(d) { return d.cells; })
  .enter()
  .append("tr")
  .selectAll("td")
  .data(function(d) { return d; })
  .enter()
  .append(function(d) { return document.createElement(d.t); })
  .html(function(d) { return d.v; });

Please forgive the short variable names; t stands for “tag” and v for "value". Here we use append to return an appropriate element for the data. Note that the selectAll calls are fake: we’re not necessarily adding tr or td elements, we just need the call to create a selection we can stick our data on.

This approach has at least two drawbacks. First, the data used to create the table (table_mdata here) mixes table data and metadata. Second, the shorter length seems to come at the price of readability, at least for me. Even with these minuses the technique is probably useful sometimes.

Conclusion

Other people have written about D3 tables, and their solutions influenced mine. Pain-free tables and d3noob are two notable examples. Both are great solutions, but both don’t quite scratch my itch.

One lesson for me here is that it’s important to not take simple things for granted. By thinking hard about how to do something routine like make a table with D3, I found several ways that work better than what I had been doing before. I suspect that I’ll find other uses for heterogeneous nesting in D3, probably more complicated ones. Another lesson is that D3 is super cool and worth studying closely. Now go make some tables!

Approx. 734 words, plus code