Leveling up D3, The Reusable Chart API
Last published on December 07, 2015 by Marcos Iglesias
This article was originally posted on the Eventbrite Engineering Blog.
This is the first post in a series where I'll show you how to improve D3 charts. My goal is to help you build D3 charts in a less painful way by using the Reusable API, a Test Driven approach and good quality code.
In this first post, I'll demonstrate why the Reusable API pattern is so amazing. In the next post in this series, I'll cover how to craft a bar chart test-by-test, following a TDD approach. A third post will deal with more advanced refactoring facilitated by our new high quality code, like improved accessors and event handling.
Introduction
In the past, to create a new chart we took an example as a jumping-off point. We modified it, plugged in the new data and we had something that works. This way of coding comes with a bunch of problems, especially those related to the reusability, extendability, simplicity and general quality of our code. Software will always change, so how do we prepare for change? Using the Reusable Chart API and a test driven approach will make your D3 charts easier to create and maintain.
My Experience
When I first started out with D3.js two years ago, I built things in the same old fashioned way, customizing examples, and although that worked, I was never proud of my code. The chaining of methods makes the graphs concise, but it costs a lot in terms of cognitive overload and maintainability. Building this way is painful to modify, reuse or even to understand once you return from lunch! I had a huge revelation when I discovered the Reusable Chart API (a modular structure to create and reuse D3 elements). It was like moving from namespaced JavaScript modules to using AMD or Common JS modules. It makes a big difference, so let’s jump into it.
The Reusable API
The first example of this pattern was seen in Mike Bostock's seminal post Towards Reusable Charts (2012), and it hasn’t changed a lot since then. Take a look at the code we will be using:
/**
* This function creates the graph using the selection as container
* @param {D3Selection} _selection A d3 selection that represents
* the container(s) where the chart(s) will be rendered
*/
function exports(_selection) {
/* @param {object} _data The data to attach and generate the chart */
_selection.each(function (_data) {
chartWidth = width - margin.left - margin.right;
chartHeight = height - margin.top - margin.bottom;
data = _data;
buildScales();
buildAxis();
buildSVG(this);
buildContainerGroups();
drawBars();
drawAxis();
});
}
/**
* Gets or Sets the margin of the chart
* @param {object} _x Margin object to get/set
* @return { margin | module} Current margin or Bar Chart module to chain calls
* @public
*/
exports.margin = function (_x) {
if (!arguments.length) {
return margin;
}
margin = _x;
return this;
};
return exports;
(This example and the code that I’ll show in the following post evolved from the case shown in Developing a D3.js Edge, a lovely little book about D3.js that I recommend to everyone interested in improving their D3 skills.)
This piece of code returns a function that will accept a D3 selection as input. Then it will extract the data of that selection to build a chart, using the selection as container. It will also accept some configuration (the margin, in this case) that will be set beforehand. Let’s see how we use it:
// Creates bar chart component and configures its margins
barChart = chart().margin({ top: 5, left: 10 });
container = d3.select(".chart-container");
// Calls bar chart with the data-fed selector
container.datum(dataset).call(barChart);
And that’s it! This code would create an instance of a bar chart in the DOM element with class ‘chart-container'. We could also create different instances of the same chart, and they could live together in the same page without any problems. This way of creating a chart differs a lot from what we usually see in the regular block examples. We will get into more details about it, discussing the implementation and specific drawbacks of the example-based approach in the next post in this series. For now, let me tell you about the benefits of the Reusable Chart API:
Modularity
The first and main reason this pattern can improve your D3 chart code is modularization. It won’t be a surprise for most engineers out there, as splitting big problems into smaller ones has been a basic problem-solving procedure since the earliest days of science. With the Reusable API, you will be able to separate the different parts of a large and complicated chart into diverse components. Another advantage is that you will be able to bundle all these components into a package, creating your own charting library that could be distributed as a bower or npm package.
Composability
The Reusable API pattern will allow you to compose large charts from small components, as if they were mere Lego blocks. These flexible components can also call other components inside themselves, creating instances with different configurations depending on the specific needs of the parent component. This way, through the use of a common methodology of creating components, we can intuitively compose them, creating large systems from small pieces of functionality.
Simplicity
D3 charts tend to grow a lot, especially when different stakeholders push for their favorite features on a chart that “was going to be a simple thing.” The good news is that it’s super easy to change things on the components you create with the Reusable API pattern. You will only need to think beforehand which parameters you want each component to make configurable and add an API accessor to them. However, if you didn’t expect them to be configurable and if the requirements change, it’s a matter of minutes to make more options public.
Reusability
The Reusable API makes it easy to reuse the code you write. It’s true that this won’t be the case for ALL the code, but most of the small components that form a full-fledged chart (think tooltips, brushes, legends) will be completely reusable. For the main chart component, you will be able to use its structure and some main blocks to create the next chart. For example, if you have a bar chart and want to create a line chart from it, you can probably reuse the x- and y-axes creation functions, the tooltips and maybe the data loading functionality.
Testability
One of my favorite reasons for using the Reusable API is the ability to test your charts. You will be building small units of functionality with a given interface, and these can be easily tested most of the time. These components will do one small thing, and the idea is that they will do it right, so the number of dependencies, which is one of the main problems of client-side testing, will be negligible. You won’t even need mocks! Being able to test your charts means that they no longer need a preferential treatment, so there won’t be problems getting that code into your usual workflow. If your team is using a continuous integration tool, you can include your charting library tests in it.
Teamwork enabling
While not super common, working with a team with d3 charts can be an absolute pain if everybody used an example-based approach. With the Reusable API though, working within a team is seamless. Another advantage: as you all know, D3 has a pretty steep learning curve, so modularizing the parts of a chart into small pieces will help less senior members of your team jump into that code and own it. This approach also fosters collaboration and communication. Every team member can work on different pieces of the chart, and because every piece has a given API that can be agreed upon beforehand, you can avoid huge complications while working.
Next Up
Stay with me! In the next post, I will walk you through the process of implementing a basic bar chart with a Test Driven Development approach. In the meantime, connect with me on twitter where I share more articles and resources about data visualization, d3 and front end technologies in general.