I’d like to propose a convention for encapsulating reusable charts in D3. Wait for it…
function chart() {
// generate chart here
}
A function; the standard unit of code reuse!
I jest; not any function will do. In truth we need a configurable function, since most charts require customization of their appearance or behavior. For example, you may need to specify the width and height, or the color palette. A simple method of configuration is passing arguments:
function chart(width, height) {
// generate chart here, using `width` and `height`
}
Yet this is cumbersome for the caller: they must store the chart’s various arguments, and pass them whenever an update is needed. A simple function is insufficient for highly-configurable charts. You could try a configuration object instead, as is done by many charting libraries:
function chart(config) {
// generate chart here, using `config.width` and `config.height`
}
However, the caller must then manage both the chart function (assuming you have multiple types of charts to pick from) and the configuration object. To bind the chart configuration to the chart function, we need a closure:
function chart(config) {
return function() {
// generate chart here, using `config.width` and `config.height`
};
}
Now, the caller need merely say:
var myChart = chart({width: 720, height: 80});
And subsequently, myChart()
to update. Simple!
But what if you want to change the configuration after construction? Or if you want to inspect the configuration of an existing chart? The configuration object is trapped by the closure and inaccessible to the outside world. Fortunately, JavaScript functions are objects, so we can store configuration properties on the function itself!
var myChart = chart();
myChart.width = 720;
myChart.height = 80;
The chart implementation changes slightly so that it can reference its configuration:
function chart() {
return function my() {
// generate chart here, using `my.width` and `my.height`
};
}
With a little bit of syntactic sugar, we can replace raw properties with getter-setter methods that allow method chaining. This gives the caller a more elegant way of constructing charts, and also allows the chart to manage side-effects when a configuration parameter changes. The chart may also provide default configuration values. Here we create a new chart and set two properties:
var myChart = chart().width(720).height(80);
Modifying an existing chart is similarly easy:
myChart.height(500);
As is inspecting it:
myChart.height(); // 500
Internally, the chart implementation becomes slightly more complex to support getter-setter methods, but convenience for the user merits additional developer effort! (And besides, this pattern becomes natural after you’ve used it for a while.)
function chart() {
var width = 720, // default width
height = 80; // default height
function my() {
// generate chart here, using `width` and `height`
}
my.width = function(value) {
if (!arguments.length) return width;
width = value;
return my;
};
my.height = function(value) {
if (!arguments.length) return height;
height = value;
return my;
};
return my;
}
To sum up: implement charts as closures with getter-setter methods. Conveniently, this is the same pattern used by D3’s other reusable objects, including scales, layouts, shapes, axes, etc.
The chart can now be configured, but two essential ingredients are still missing: the DOM element into which to render the chart (such as a particular div or document.body
), and the data to display. These could be considered configuration, but D3 provides a more natural representation for data and elements: the selection.
By taking a selection as input, charts have greater flexibility. For example, you can render a chart into multiple elements simultaneously, or easily move a chart between elements without explicitly unbinding and rebinding. You can control exactly when and how the chart gets updated when data or configuration changes (for example, using a transition rather than an instantaneous update). In effect, the chart becomes a rubber stamp for rendering data visually.
The simplest way of invoking our chart function on a selection, then, is to pass the selection as an argument:
myChart(selection);
Or equivalently, using selection.call:
selection.call(myChart);
Internally, a call-based chart implementation looks something like this:
function my(selection) {
selection.each(function(d, i) {
// generate chart here; `d` is the data and `this` is the element
});
}
To make this proposal concrete, consider a simple yet ubiquitous use case: time-series visualization. A time series is a variable that changes over time. We can visualize this as an area chart, where the x- and y-axes respectively encode time and value as position:
To hold the chart, this page has an initially-empty p
(paragraph) element:
<p id="example">
For data, there’s an external CSV file (sp500.csv), the first few lines of which looks like this:
date,price
Jan 2000,1394.46
Feb 2000,1366.42
Mar 2000,1498.58
Apr 2000,1452.43
May 2000,1420.6
Jun 2000,1454.6
Jul 2000,1430.83
Aug 2000,1517.68
To create the chart, we first call timeSeriesChart
to make a new chart instance, defining accessors for the x (date, a Date object) and y (price, a number) dimensions for our data:
var chart = timeSeriesChart()
.x(function(d) { return formatDate.parse(d.date); })
.y(function(d) { return +d.price; });
var formatDate = d3.time.format("%b %Y");
The price is coerced to a number using the unary +
operator; JavaScript is weakly-typed, so it’s a good idea to convert the loaded data from string to number. The date requires a d3.time.format for parsing; %b
refers to the abbreviated month name, and %Y
refers to the four-digit year. We might want to configure other options as well, but in this simple example the default width, height and margin are suitable, and no other configuration is needed.
Lastly, we load the data via d3.csv. When available, we bind the data to the empty paragraph element, and use selection.call to render the chart!
d3.csv("sp500.csv", function(data) {
d3.select("#example")
.datum(data)
.call(chart);
});
We now have a strawman convention for reusable visualization components. Yet there is far more to cover as to what should be considered configuration
or even a chart
. Is a traditional chart typology the best choice? Consider drawing inspiration from the Grammar of Graphics (see Polychart and ggplot2); there are more modular units for composition. Even with traditional chart types, should you expose the underlying scales and axes, or encapsulate them with chart-specific representations? Should your chart support interaction and animation automatically? Should the user be able to reach into your chart and tweak some aspect of its behavior? All of these are possible using the proposed convention, so have at it!