A better way to structure D3 code (updated with ES6 and D3 v4)

Note: This blog post is aimed at beginner-to-intermediate users of the D3 JavaScript library. If you want to skip straight to the good stuff, check out the accompanying example on bl.ocks.org.

Code written using D3 is difficult to manage. Even in a simple line chart, there will be almost a dozen important variables such as (deep breath): width, height, margins, SVG, x-scale, x-axis generator function, x-axis SVG group, x-scale, y-axis generator function, y-axis SVG group, line generator function, path element and (most important of all) a data array/object. And that’s just the bare minimum.

Because most of these need to be accessible at several points in a script, the temptation is to structure the entire thing in one giant function. Many examples on bl.ocks.org are essentially unstructured, which makes the concepts nice and clear but in real-world code can lead to an unmanageable mess.

Credit where credit is due: I was introduced to this idea by my colleague Jason French. I’ve since adopted it and use it regularly. This is my attempt at formalising it.

A solution: object-oriented programming

Think of a D3 chart or visualisation as a ‘widget’ on the page. This provides a number of benefits:

Here’s what we’re aiming for: being able to create the chart as if it were a Highcharts/C3-style thing.

const chart = new Chart({
  element: document.querySelector(".chart-container"),
  data: [
    [new Date(2016, 0, 1), 10],
    [new Date(2016, 1, 1), 70],
    [new Date(2016, 2, 1), 30],
    [new Date(2016, 3, 1), 10],
    [new Date(2016, 4, 1), 40],
  ],
});

Which we could then modify like so:

// load in new data
chart.setData(newData);

// change line colour
chart.setColor("blue");

// redraw chart, perhaps on window resize
chart.redraw();

A quick introduction to classes

Before we move onto the D3-specific stuff, it’s worth learning how to use classes. This is a useful general-purpose pattern for JavaScript code used frequently in both JavaScript’s native functions and in third-party libraries.

You may already be familiar with:

const d = new Date(2016, 0, 1);

This creates a new object stored in d, which is based on (but does not replace) the original Date object. Date is a class, and d is an instance of Date.

We can make our own class like so:

class Cat {
  constructor() {
    // nothing here yet
  }
  cry() {
    return "meoww";
  }
}

The constructor function is called when a new instance of Cat is created. The other functions (in this case, just cry) are instance methods, and are available to each instance of the Cat class. We would call it like so:

const bob = new Cat();
bob.cry(); // => 'meoww'

Inside of the class, there is a special variable called this which refers to the current instance. We can use it to share variables between instance methods.

class Cat {
  constructor(crySound) {
    this.crySound = crySound;
  }
  cry() {
    return this.crySound;
  }
}

In this case, we are customising the new cat’s crySound.

const bob = new Cat("meoww");
const noodle = new Cat("miaow");
bob.cry(); // => 'meoww'
noodle.cry(); // => 'miaow'

Because each instance is a new object, this style of coding is called object-oriented programming.

There’s a lot more to classes, and if you want to learn more I recommend reading CSS Tricks’ Understanding JavaScript Constructors and Douglas Crockford’s more hardcore Classical Inheritance in JavaScript – both of which use the old ES5 ‘constructor function’ syntax.

A chart as a class

Instead of Cat — which is obviously a fairly useless class — we could instead make a class for a D3 chart:

class Chart {
  constructor(opts) {
    // stuff
  }
  setColor() {
    // more stuff
  }
  setData() {
    // even more stuff
  }
}

Here’s a live example of a chart made using a Chart class. Try clicking the buttons below and resizing the window.

This example is written in ES6 and will only work in the latest versions of Chrome, Firefox and Safari.

<style>
/* a little bit of CSS to make the chart readable */
.line {
    fill: none;
    stroke-width: 4px;
}
.axis path, .tick line {
    fill: none;
    stroke: #333;
}
</style>

<!-- here's the div our chart will be injected into -->
<div class="chart-container" style="max-width: 1000px;"></div>

<!-- these will be made useful in the script below -->
<button class="color" data-color="red">red line</button>
<button class="color" data-color="green">green line</button>
<button class="color" data-color="blue">blue line</button>
<button class="data">change data</button>

<script>
// create new chart using Chart class
const chart = new Chart({
	element: document.querySelector('.chart-container'),
    data: [
        [new Date(2016,0,1), 10],
        [new Date(2016,1,1), 70],
        [new Date(2016,2,1), 30],
        [new Date(2016,3,1), 10],
        [new Date(2016,4,1), 40]
    ]
});
// change line colour on click
d3.selectAll('button.color').on('click', function(){
    const color = d3.select(this).text().split(' ')[0];
    chart.setColor( color );
});
// change data on click to something randomly-generated
d3.selectAll('button.data').on('click', () => {
    chart.setData([
        [new Date(2016,0,1), Math.random()*100],
        [new Date(2016,1,1), Math.random()*100],
        [new Date(2016,2,1), Math.random()*100],
        [new Date(2016,3,1), Math.random()*100],
        [new Date(2016,4,1), Math.random()*100]
    ]);
});
// redraw chart on each resize
// in a real-world example, it might be worth ‘throttling’ this
// more info: http://sampsonblog.com/749/simple-throttle-function
d3.select(window).on('resize', () => chart.draw() );
</script>

And here’s the corresponding JavaScript for the chart. To see how it’s being used, read the full code on bl.ocks.org.

A few things worth emphasising here:

Watch out for anonymous functions

The only catch with using constructor functions is that the value of this will change inside of anonymous functions. In D3, anonymous functions are everywhere – but since ES6 arrow functions don’t change the value of this, we can use them to get around this on the most part.

What do I mean by that? Inside of the Chart class, this refers to the Chart instance, as expected.

class Chart {
  constructor(opts) {
    // here, `this` is the chart
  }
  setColor() {
    // here, `this` is still the chart
  }
}

However, the value of this can change when inside an anonymous function:

class Chart {
  // ...
  example() {
    // here, `this` is the chart
    const line = d3.svg.line().x(function (d) {
      // but in here, `this` is the SVG line element
    });
  }
}

There’s a simple solution, which is to use an arrow function instead:

class Chart {
  // ...
  example() {
    // here, `this` is the chart
    const line = d3.svg.line().x((d) => {
      // and in here, `this` is still the chart
    });
  }
}

Sometimes you might need the value of the original this (aka the chart) and the new this (aka whichever SVG element is being modified). In those cases, you can load the original this into another variable. I like to use that.

class Chart {
  // ...
  example() {
    // here, `this` is the chart
    const that = this;
    const line = d3.svg.line().x(function (d) {
      // in here, `this` is the SVG line element
      // and `that` is the chart
    });
  }
}

Rules to live by

To keep the Chart function’s responsibilities from spiralling out of control, I try to stick by these rules:

It works, honest

I’ve used this pattern many times now in graphics published on WSJ.com, including ECB Meets, Euro Reacts and The World’s Safest Bonds Are Actually Wild Risks. In both cases, the class pattern made managing these dynamically-updating charts a breeze. The rest of the code, on the other hand…

Thanks to Amelia Bellamy-Royds for providing feedback on a draft of this post.

Footnotes

  1. In this case, there is no practical difference between the object’s ‘private’ and ‘public’ methods — they are all accessible from outside of the object. For a list of ways to make pseudo-private or actual private methods, see this article.

  2. If you’re feeling especially brave, you could extend the class.

Published .