Neal Humphrey | August 16, 2017
Lately I’ve been working on making D3 charts more reusable. In particular, I wanted a structure that I could use when I started any new chart that would naturally lead to an elegant code design and so that I wouldn’t need big code refactors when I (inevitably) wanted to add features like resizability. This mostly developed during my work on the Housing Insights project. John Osterman and I worked on a few permutations and settled on a structure that I think works well. I’m pulling this out into its own open source project because I think it has a lot of potential for making D3 charts easier for future projects and other developers - I’m launching this as D3 Boilerplate.
So what makes a good reusable chart structure? Here’s some of the design decisions that went into laying out the initial structure and some of the existing reusable chart knowledge that’s out there.
One of the clearest descriptions of a design pattern for reusable charts comes from the creator of D3, Mike Bostock, in his article Towards Reusable Charts, and as such was a natural starting point in my hunt for the design pattern I wanted. He recommended a closure with custom getter/setter methods:
While you could do this more simply without the getter/setters, since each of them returns the chart function itself, they allow method chaining - an elegant way to set the properties without repeatedly typing the chart instance’s variable name and without needed to maintain a separate configuration object. It’s also the way the rest of D3 works so it’s a familiar pattern (I recommend reading the original article if the value of this approach isn’t apparent).
Custom getters/setters that return the chart object itself - check!
But the closure was bothering me. I wrote up a whole chart using the closure approach in an earlier part of Housing Insights. However, while working on Yellowbrick, a Python-based visualization library for machine learning, I’d used a classical class inheritance structure where parent classes provided common methods needed by a group of related charts. My goal was not just to make reusable individual charts but more so to provide a common structure I could use for any future charts too - and not have to rewrite all the code that those charts might share. For example every chart needs a width and a height - why should I copy those getters/setters into every chart I make? With some hoop-jumping you can implement inheritance from a closure (example) but it’s not pretty - for me then, closures are out.
That brings us to using an object-oriented approach; Mike Bostock even points this out in his original post:
A conventional object-oriented approach as Chart.prototype.render would also work, but then you must manage the
thiscontext when calling the function.
True, but a small tradeoff for usability (and if you’re not confident in how to manage
this, the You Don’t Know JS series on the topic is a great starting point).
Object.create(). It looks like this:
Now if we want to use the charts:
There’s a few things going on here. All charts that use the
baseChart are drawn by calling the
.setup() method and providing it with a ‘container’ string used by the
d3.select(container) call - this would typically be the id of the containing div. The base chart adds an SVG to that div where the chart will be drawn.
The child chart gets all the methods of the parent when we create it using
childChart = Object.create(baseChart), so we don’t need to repeat the
But we then supply it with a new
.setup() method, which will thus override
baseChart.setup(). We still want to run
baseChart.setup() though. We can run the setup method from the parent in the context of the child using
this refers to the copy of the chart that we’ve called
myChart. The first argument of
.call() is what we want
this to mean when we’re inside
baseChart.setup.call(this,container) says “Go find the
setup() method on the baseChart() object, and run it as if it were a method of
This approach means that the developer creating
childChart needs to explicitly decide what the new
setup() method will do, and in what order, and whether to use or replace the functionality of the parent method. This is both a) more flexible, since new charts might have unforseen needs and b) more transparent to the end user of childChart, since they can directly see what is called and in what order, and where to find the source code. In an earlier version, I had flipped this logic with the baseChart always trying to run the childChart setup method if it existed, but that was both less clear and less flexible.
Right now I’m using two monolithic
setup() methods, but as the needs of different charts diversify, the setup() logic could also be broken into more reusable functions, like so:
By naming both methods
.setup() instead of making both methods available on the child directly (i.e. naming them
.childSetup()) we also provide a consistent API for chart users - whether its the parent chart, child chart, or grandchild chart they’re all drawn using the same
Now that we have a reusable chart object with method chaining, how should we divide up the work of making our chart? One common operations needed by many D3 charts are updating the data based on some new condition. In the world of responsive websites, we often want different sized charts in different contexts as well.
The challenge of making a chart that does all these things is separating things that need to happen once (e.g. putting our svg element on the page) vs things that need to happen when things change (e.g. updating the width of our rectangle). Most D3 examples don’t separate these out unless they’re explicitly needed, so the vast majority of tutorials and examples look like this one that’s designed to run just once on page load.
The “Towards Reusable Charts” post doesn’t discuss this, but in the concluding time-series-chart example one solution is quietly hidden. Cleverly, Bostock uses the d3 data binding on the SVG element itself, allowing the
enter() selection to append the svg if needed:
This means that running the function the first time will create the SVG, while running it a second time will simply re-select the SVG element that exists. However while the
enter() selection is a core component to D3 chart design, most tutorials don’t use it for this purpose - and I wanted something more obvious to someone implementing their own childChart. Another pattern I’ve seen, and the one I decided to adopt, is the Setup/Resize/Update pattern. I came across this in a tutorial run by Chris Given which he kindly repeated for the Housing Insights volunteers. From his tutorial code:
I typically use three lifecycle functions for each chart, each of which calls any successive functions:
- setup (stuff that only ever happens once)
- resize (anything that’s dependent on the size of the chart’s parent element)
- update (stuff that changes anytime either the data or chart size does)
I did make one alteration to this pattern, wherein
setup() doesn’t automatically call
resize(), allowing the setup function establish defaults that the user can then override with method chaining.
update() are designed to run both when the chart is first created (i.e. when called by
setup()) as well as triggered by user events.
Even if we don’t yet want resizability or updatability in a simple chart, thinking briefly about which of these functions is a better home for each component - adding an SVG to the page, or setting the width attribute of a rect element - means that we’re set up for it when we decide to do it later, but the only thing that’s different about our version 1 code is how we split the same lines between the three functions.
When we put all this together, we have a base chart that can do all the common setup needed by most D3 charts. We also have a consistent API format for any new charts created using this model - with their own
update() methods, and method chaining for applying their configuration parameters. But, developers aren’t locked into any built in behavior. When making their own childCharts, they must explicitly choose which behavior to get from their parents.
Do we need another D3 chart library? There are already a lot of them out there, and I haven’t reviewed them all. However I think the emphasis on building an extensible tool for D3 developers - rather than a tool for people that want to put pre-built charts on a page quickly - is unique. Comments, feedback, and especially project contributors are highly encouraged! Check out the repository here.
Finally, let’s see an example in action. Click the chart to see its resize and update functions.