D3 Impressions

I used D3 to do a few graphs and animations for a recent company hackathon, and I thought it might be worthwhile to jot down some notes on my impressions.

Why D3?

The rough problem we were solving was visualizing a tree-based data structure, with a centered root node and child nodes splaying out to the left and right (depending on the type of child) for several different levels.

I've messed around with HTML5 Canvas elements before, but I wanted the tree to be interactive. I wanted to be able to click on nodes of the tree and have the tree be recentered so that the clicked-on node becomes the root node (which might require some new data to be loaded).

That seemed like a pain to do with Canvas, because state information like "there's a circle rendered here" is discarded as soon as the drawing operation is completed. I'd have to manually keep track of where nodes were and translate "a click at a given (x, y)" to "a click on node z".

On the other hand, I knew just enough about rendering SVG in the browser to realize that it was probably a better fit for this scenario. SVG elements are DOM elements, which lets us sidestep the process of mapping xy-click coordinates to tree nodes and rely on the information contained in the click event instead.

But, having never worked with SVG before (except for messing about in Inkscape drawing logos), I thought it best to "go with the flow" and explore it in the context of a popular library.

What's the point of D3?

Depending on your web front-end (& SVG) experience, D3 as shown by tutorials seems either arcane, or completely pointless.

     .attr("width", 50)
     .attr("height", 50)
     .attr("cx", 25)
     .attr("cy", 25)
     .attr("r", 25)
     .style("fill", "purple");

(try this example)

Arcane because it magically knows about body, svg & circle (does it also know about dotted-squigly-line?) and the syntax for allowable attributes vs styles doesn't seem to be documented anywhere in D3. In reality, 'circle' is just the name of the svg element, much like p or div, attr() sets whatever attribute you want, and style() sets whatever css property you want.

Pointless because, well, if that's all it's doing, can't we do the exact same thing with jQuery?

     .attr("width", 50)
     .attr("height", 50).append(
         .attr("cx", 25)
         .attr("cy", 25)
         .attr("r", 25)
         .css("fill", "purple")));

A little uglier, but close enough? If you try this jQuery example you'll see that although the DOM nodes get added, there's no purple circle shown. What the?

As pointed out by answers on trusty StackOverflow, the reason behind this is that SVG and HTML elements belong to different namespaces; you can create an SVG element using jQuery, but it ends up in the wrong namespace (see also MDN's Namespaces Crash Course. Anyway, some quick hackery and we're back:


Now we can observe our purple circle in all its glory, but woops! We just lost events we had bound to any SVG elements because that snippet recreates them from scratch.

If all you want to do is draw a non-interactive SVG animation (or you have masochistic tendencies) and don't want to drag in another dependency, then use jQuery.

So D3 is a jQuery that works for SVG?

Yes, but the more interesting thing is how D3 helps you deal with changing data. Most tutorials do an average job of explaining this, so I'll have a swing at explaining it in the way that I wish I could have had it explained to me.

D3, Data, & Binding

You have an array of Javascript objects, and you want to use this data to affect some set of DOM nodes, such as a bunch of circle elements. So, first you use a select() to do CSS selection (just like jQuery), and then you bind your data array to the selection using data().

var circleSelection = d3.select("svg").selectAll("circle").data([10, 20, 40]);

Each time you bind data like this, you can access 3 possible "stages" (D3 terminology), which are selections themselves:

  • enter() - you have more data than elements you selected. You probably want to append() a new element for each of your excess data points, and then use the relevant data point to do something: circleSelection.enter().append("circle").attr("cy", 30).attr("r", 5).attr("cx", function(d) { return d; }); (here we use the data point as the x position)
  • update() - you have some data for which there is already a matching element. You probably want to use the new data to change the data node; in our case we want to update the x position again: circleSelection.update().attr("cx", function(d) { return d; }); (note that unlike in enter(), there's no need to re-set cy or r because those will already have been set for the elements when they are created)
  • exit() - you have less data than elements you selected. You probably want to make those elements disappear: circleSelection.exit().remove(); (remove() is a D3 function to disappear selected elements)

That's all you need to know to get started (+ the MDN SVG docs).

D3 features you should probably know about

D3 includes a bunch of other functionality; skimming through this list will avoid you reinventing the wheel a few times:

  • Want fancy animations? Call transition() on a selection before doing something like calling attr() or style() on it (By the way, D3 calls these operators).
  • Loading data from the server? You've got d3.json() and more.

  • Want to scale your values so your elements don't go off the edge of your SVG element? D3 has Scales.

  • Drawing curvy lines? D3 has Path data generators
  • Trying to position nodes in a tree layout, or make a histogram, or a pie chart? D3 has layouts, which can do useful things like augment your data array objects with x & y coordinates.
  • Text overlapping? D3 has some support for zooming.

These things were useful to me, but it's worth skimming the API docs yourself.

What about that hackathon project?

Success! We implemented a more advanced version of this tree.

We won a minor prize too, but the jury's out on whether it was the utility or the animations which got us that.

Either way, it's a vote of confidence in D3 ;)