Reusable and Extendable D3 Charts

D3 v4's shift to a plugin focused architecture is an awesome thing. It lets charts get broken down into small, reusable components that are easy to package up and distribute.

Creating a D3 v4 plugin is a simple process, but what about creating reusable charts?

There are already many libraries that try and provide a simple interface for charts, but none of them offer flexibility for most use cases. As one of my acquaintances has put it:

I just need to make a line graph and then put a single arbitrary dot on the chart. None of the packages out there let me do that. So now I either have to fork and hack something or rebuild it all of it in D3. I don't have time for this.

So, lets create a line chart plugin that creates the major parts of the chart for us, gives us control over what is being drawn, and can be distributed through npm or static builds.

What we'll end up with is something that lets us make this:

var size = {width: 710, height: 200};
// create our svg element
var svg = d3_svg.create('#example-1', size);

// build our chart
var chart = d3_line_chart.chart()
.width(size.width)
.height(size.height)
.margin({top: 10, left: 0, right: 0, bottom: 0})
.xValue(function(d) { return d.date; })
.yValue(function(d) { return d.val; });

// draw the chart
svg.datum(data).call(chart);

// add whatever we want
chart.g().append('text')
.text('much text')
.attr('dy', '1em');

chart.g().append('text')
.text('wow')
.attr('dy', '2em');
General Structure

Check out the entire project on Github.

A good pattern for creating D3 charts has been around for a while. It can be verbose, but this method creates a chart with a very clean interface.

// D3's code for selection operations
import * as d3_selection from 'd3-selection';

// our function that we export
export default constructor() {

  // private variables for this chart
  var _g;

  // what is evaluated by `.call()`
  function chart(selection) {
    selection.each(function(data) {
      // our selected elements
      _g = d3_selection.select(this).append('g');
    });
  }

  // getters and setters for `chart`
  chart.g = function(val) {
    if (!val) {
      return _g;
    }
    _g = val;
    return line_chart;
  };

  return chart;
}

The line chart plugin follows this patten and ends up creating a chart that has a lot of flexibility while still focusing on doing one thing well.

Drawing a chart

Drawing the chart is no different then anything else you have created in D3, outside of having to worry about making variables get/settable.

The main chart function is where your magic is going to happen.

// These are available via getters and setters
var x = d3_scale.scaleTime();
var y = d3_scale.scaleLinear();
var margin = {top: 0, right: 0, bottom: 0, left: 0};

function chart(selection) {
  selection.each(function(data) {
    // our selected elements
    var chartG = d3_selection.select(this)
                  .append('g')
                  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    var line = d3_shape.line()
          .x(function(d) { return x(d); })
          .y(function(d) { return y(d); });

    chartG.append('path')
          .datum(data)
          .attr('class', 'line')
          .attr('d', line);

  });
}
Being Reusable and Extendable

Lets return to the reason why we're making this chart: reusability and extendability.

The reusability of this chart is pretty straight forward - once the chart is defined we have a function that can draw the same chart over and over again. The chart also provides access to the underlying elements through chart.g() so any arbitrary elements can be added or modified.

So lets add a bunch of random dots to our chart from earlier.

Finally, this method of creating charts is extendable for one very awesome reason: npm install d3-line-chart-plugin. Since this chart is available through npm it is possible for others to easily use this code as a basis for their own charts.

import * as line_chart from 'd3-line-chart-plugin';

export default anotherConstructor() {

  function anotherChart(selection) {
    selection.each(function(data) {
      var _g = d3_selection.select(this).append('g');
      // draw the line chart
      _g.call(line_chart);
      // and then keep adding to it
    });
  }

  return anotherChart;
}

While D3 v4 is still in alpha, the power of its plugin framework is already available. What awesome things are you going to create?

Discuss this on Hacker News


Keep Reading

Implementing the Entropy language by abusing decorators

Working with up