Build graphs with D3.js

Graphs and charts can be build using SVG or the DOM. But doing this by hand is quite annoying and repetitive. Luckily, there is a library to help: D3.js makes manipulating the DOM based on data easy.

Basics

I like to see the temperature graph for some data I got. The x axis shall be the timeline and the y axis the temperature, depending on the passed in values. Sounds like an easy task, right?

To get started, D3 has to be included and somehow we need some data. Let’s say we have this median temperature data for each month in Hamburg:

var temperatureData = [
  {time: new Date('2016-01-15'), temperature: 1.3},
  {time: new Date('2016-02-15'), temperature: 1.7},
  {time: new Date('2016-03-15'), temperature: 4.4},
  {time: new Date('2016-04-15'), temperature: 7.8},
  {time: new Date('2016-05-15'), temperature: 12.6},
  {time: new Date('2016-06-15'), temperature: 15.4},
  {time: new Date('2016-07-15'), temperature: 17.4},
  {time: new Date('2016-08-15'), temperature: 17.2},
  {time: new Date('2016-09-15'), temperature: 13.6},
  {time: new Date('2016-10-15'), temperature: 9.4},
  {time: new Date('2016-11-15'), temperature: 5.1},
  {time: new Date('2016-12-15'), temperature: 2.5}
];

Generating the SVG

To show off some graph in a SVG, we need that SVG first! So let’s create one.

var svgHeight = 100,
  svgWidth = 440;

var svg = d3.select('body').append('svg')
    .attr('class', 'visualization')
    .attr('width', svgWidth)
    .attr('height', svgHeight);

With D3 we can select DOM elements similar to JQuery. After selecting the body element, we append a new svg element to it. To be able to style or position it, a class is added. Adding a width and height is important as the SVG wouldn’t use any space otherwise.

Now check the DOM with your browsers dev tools – there should be an empty SVG container in the defined dimensions. Hurray!

Creating scales

To create the x and y scales, we need to define what size to use. As the axes to these scales should be rendered later, too, we need some padding in the SVG on the left and bottom. The final width and height our graph can use is thus smaller than the whole SVG.

This can be achieved easily in plain JS:

var svgPadding = {left: 40, bottom: 20},
  graphWidth = svgWidth - svgPadding.left,
  graphHeight = svgHeight - svgPadding.bottom;

The scales are defined via the D3 API and there are various to choose from: quantitive, ordinal and time scales. We choose a time scale for the x scale and a quantitive linear scale for the y scale.

var x = d3.time.scale()
  .rangeRound([0, graphWidth])
  .domain(d3.extent(temperatureData, d => d.time));

var y = d3.scale.linear()
  .range([0, graphHeight])
  .domain([25, -5]);

One thing the scales need to know is the space that can be used to render. This is done with range definitions. For the x scale it is the width for the graph we just calculated and for the y scale it is the calculated height.

Before combining the space with our data, the scales need to know their domain, their extents. This roughly means we need to map the SVG coordinates to a value. Creating these scales gives us two functions, x and y, that we’ll need later to calculate the axes and the graph.

As the x scale shall span over the whole timespan we have in our data, we need to tell it so. d3.extent returns the minimum and maximum time for our data, that we pass on to the d3.domain. Calling the x scale with some data will render the time 2016-01-15 at the left of the graph. On to the next!

The y scale shall span from -5°C to 25°C, no matter the data. Note: as the y scale in SVG starts at the top, we have to reverse the extents. Defining [25, -5] gives us a scale that renders 25°C at the very top at y = 0 and -5°C at the bottom position, at y = graphHeight.

We still can’t see nothing more than the empty SVG container. But that will change. Now!

Rendering the graph

To get the graph rendered, the two scales and the data need to be combined.

var line = d3.svg.line()
    .x(d => x(d.time))
    .y(d => y(d.temperature));

svg.append('path')
    .datum(temperatureData)
    .attr('transform', 'translate(' + svgPadding.left + ', 0)')
    .attr('class', 'line line--1')
    .attr('d', line);

We need to define a line using the two scales. Then a new path element is added to the SVG element with the temperature data defining the path. As we later want to render an axis on the left that explains what we are seeing here, the graph needs to be moved a bit to the right via a transform.

Finally something to see!

Rendering axis legends

Seeing a simple line doesn’t say much. What does it mean? So let’s add some axes!

var xAxis = d3.svg.axis()
  .orient('bottom')
  .scale(x)
  .ticks(12)
  .tickFormat(d3.time.format('%b'));

var yAxis = d3.svg.axis()
  .orient('left')
  .scale(y)
  .ticks(3)
  .tickFormat(d => `${d}°C`);

svg.append('g')
  .attr('class', 'x axis')
  .attr('transform', 'translate(' + svgPadding.left + ', ' + (svgHeight - svgPadding.bottom) + ')')
  .call(xAxis);

svg.append('g')
  .attr('class', 'y axis')
  .attr('transform', 'translate(' + svgPadding.left + ', 0)')
  .call(yAxis);

For each scale an axis is created that is assigned to the scale. Depending on the data and the axis we format and limit the ticks, the entries to show on the axis legend. For the x axis, we want to show at most 12 time points. D3 will take care to choose rounded values and not overcrowd the axis to keep it readable. On the y axis we define 3 ticks and format it to add the unit.

With these fresh defined axes we can render them by appending a group and letting D3 take care of rendering the axes content.

Here we go – a graph showing the awesome weather in Hamburg! See the complete source code here. And book a flight, best in summer.

Conclusion

D3 is a powerful and flexible library to help organizing and visualizing data. The API is a bit weird sometimes, but all in all it’s great to work with!

I wonder why I didn’t dig into this some years ago.