Some simple tricks for creating responsive charts with D3

November 23rd 2015 by Christopher

In this post I have collected some techniques that I used recently when creating D3 Charts. With focus on small screens, these examples might help you to improve the readability and usability of your charts.

Create test data

We will need some test data that we want to show in our visualization. Therefore, I created a simple node.js script that creates a data.csv file which contains a date and a value column.

var fs = require('fs');
var startDate = new Date('2014-1-1');
var endDate = new Date('2015-1-1');
var csv = 'date,value\n';

for(var i = startDate; i < endDate; startDate.setDate(startDate.getDate() + 10)) {
  csv += '' + startDate.toString() + ',' + Math.random() + '\n';
}

fs.writeFileSync('data.csv', csv);

create-data.js

You can run the script with this command:

$ node create-data.js

After that, you will see that the file data.csv has been created. It look somewhat similar to this:

date,value
Wed Jan 01 2014 00:00:00 GMT+0100 (CET),0.505003142170608
Sat Jan 11 2014 00:00:00 GMT+0100 (CET),0.5459181617479771
Tue Jan 21 2014 00:00:00 GMT+0100 (CET),0.14592946274206042
Fri Jan 31 2014 00:00:00 GMT+0100 (CET),0.7082753519061953
...

data.csv

Setup a basic chart

To get started with the example, create a new directory and add three files: index.html, style.css and chart.js.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width" />
  <title>D3 Line Chart</title>
  <link rel="stylesheet" href="style.css">
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
  <div id="chart"></div>
  <script src="chart.js"></script>
</body>
</html>

index.html

var Chart = (function(window,d3) {

  var svg, data, x, y, xAxis, yAxis, dim, chartWrapper, line, path, margin = {}, width, height;

  d3.csv('data.csv', init); //load data, then initialize chart

  //called once the data is loaded
  function init(csv) {
    data = csv;

    //initialize scales
    xExtent = d3.extent(data, function(d,i) { return new Date(d.date) });
    yExtent = d3.extent(data, function(d,i) { return d.value });
    x = d3.time.scale().domain(xExtent);
    y = d3.scale.linear().domain(yExtent);

    //initialize axis
    xAxis = d3.svg.axis().orient('bottom');
    yAxis = d3.svg.axis().orient('left');

    //the path generator for the line chart
    line = d3.svg.line()
      .x(function(d) { return x(new Date(d.date)) })
      .y(function(d) { return y(d.value) });

    //initialize svg
    svg = d3.select('#chart').append('svg');
    chartWrapper = svg.append('g');
    path = chartWrapper.append('path').datum(data).classed('line', true);
    chartWrapper.append('g').classed('x axis', true);
    chartWrapper.append('g').classed('y axis', true);

    //render the chart
    render();
  }

  function render() {

    //get dimensions based on window size
    updateDimensions(window.innerWidth);

    //update x and y scales to new dimensions
    x.range([0, width]);
    y.range([height, 0]);

    //update svg elements to new dimensions
    svg
      .attr('width', width + margin.right + margin.left)
      .attr('height', height + margin.top + margin.bottom);
    chartWrapper.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    //update the axis and line
    xAxis.scale(x);
    yAxis.scale(y);

    svg.select('.x.axis')
      .attr('transform', 'translate(0,' + height + ')')
      .call(xAxis);

    svg.select('.y.axis')
      .call(yAxis);

    path.attr('d', line);
  }

  function updateDimensions(winWidth) {
    margin.top = 20;
    margin.right = 50;
    margin.left = 50;
    margin.bottom = 50;

    width = winWidth - margin.left - margin.right;
    height = 500 - margin.top - margin.bottom;
  }

  return {
    render : render
  }

})(window,d3);

chart.js

The script you see above is doing three things: It first loads the data from the csv file. Based on the data, it then initializes all variables which are used to display the chart. This includes creating the SVG that is needed for the chart and adding it to the DOM. All that stuff is done in the init function. After that, the chart is rendered based on the current viewport dimensions. The main thing to notice here is that the init function should only be called once, as it prepares everything for rendering, whereas render can be called many times and will render the chart according to viewport size and other parameters.

In style.css I added some CSS to make the chart look a bit better. This is just some basic styling taken from Mike Bostock’s Line Chart example.

body {
  font: 12px sans-serif;
  margin: 0;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

style.css

All this code together produces a line chart:

line-chart-step-one

Now we can add some more functionality that will improve the chart step by step.

Render the chart on resize

In order to make the chart fit the screen once you resize the browser window I usually listen to the resize event and re-render the chart once the event is fired. In code that would look like this:

window.addEventListener('resize', Chart.render);

Before:

before-resize

After:

after-resize

Use aspect ratio instead of fixed height

I have seen many interactives, where the charts are given a fixed height. As you can see above, this makes it hard to read the chart on smaller screens because the width of the chart gets smaller than its height at some point. To get around this, I added a fixed aspect ratio which is used to calculate the height of the interactive. This makes the chart behave similar to an image or video element which always keeps its aspect ratio when resized to smaller viewports. In the setup that we use for creating the chart, we have to change one line in the updateDimensions function:

height = .7 * width; //aspect ratio is 0.7

function updateDimensions(winWidth)

If you resize the chart now, you can see that it resizes similar to an image.

after-aspect-ratio

Inset y-axis labels on small viewports

As you can see above, the y-axis labels are taking a lot of space on smaller screens. To give the actual chart as much space as possible, one possible solution could be to inset the labels and remove left and right margins when the screen gets smaller than a specific breakpoint. Therefore, you have to make some small adjustments to the updateDimensions and render function.

First, you should define a breakpoint which is used to check if we are on small screens or not. Of course, you could add multiple breakpoints if you want to render multiple different views of the chart.

var breakPoint = 768;

Then, in the updateDimensions function we check, if the screen is smaller than the breakpoint and remove the horizontal margins if necessary:

margin.right = winWidth < breakPoint ? 0 : 50;
margin.left = winWidth < breakPoint ? 0 : 50;

function updateDimensions(winWidth)

After that, we need to configure the chart to inset the labels on small screens. This can be done directly in the render function by setting the orientation of the axis using D3:

yAxis.scale(y).orient(window.innerWidth < breakPoint ? 'right' : 'left');

function render()

You can see that the labels of the Y-Axis jump inside the chart, once we resize to the defined breakpoint.

after-inset

Prevent overlapping of x-axis labels

Another common problem with responsive charts and D3 is that once the screen gets smaller, the labels of the x-axis could overlap. To accomplish that, we could check for the window size again and reduce the number of ticks on the X-Axis if the screen size is small. Sticking to the example, this is how you just show a label for every second month:

if(window.innerWidth < breakPoint) {
  xAxis.ticks(d3.time.month, 2)
}
else {
  xAxis.ticks(d3.time.month, 1)
}

function render()

In the result, you can see that labels are not overlapping anymore.

Before

before-x-axis

After

after-x-axis

Add a simple swipe gesture

One thing that I often miss in interactive charts is the ability to swipe over the chart to see values or tooltips. In most cases, you have to tap exactly at the point that you want to see. In the next steps, I will explain how you can support a swipe gesture in interactive charts with few lines of code.

touch-events
By swiping over the chart, the user can control the x-position of a tooltip

At first, we create a fake tooltip in our init function:

locator = chartWrapper.append('circle')
  .style('display', 'none')
  .attr('r', 10)
  .attr('fill', '#f00');

Now, we need to listen to the mousemove event on the chart-wrapper to calculate the position of the locator.

chartWrapper.on('touchmove', onTouchMove);

In the onTouchMove function, we will get the touch position of the user. With the help of a linear scale, we map the x-coordinate to the index of the data array. With that index, we can get the data at the point that the user is currently focusing. That data is than used to position the locator:

var touchScale = d3.scale.linear().domain([0,width]).range([0,data.length-1]).clamp(true);

function onTouchMove() {
  var xPos = d3.touches(this)[0][0];
  var d = data[~~touchScale(xPos)];

  locator.attr({
    cx : x(new Date(d.date)),
    cy : y(d.value)
  })
  .style('display', 'block');
}

If we now swipe over the chart on touch devices, the locator or tooltip moves with the touch of the user.

Create generic annotations

For responsive charts, it can sometimes be a bit tricky to create annotations or labels that are keeping their position on all viewports. For achieving this, I am using the following technique. First I am creating a configuration object that stores the text and position of the labels I want to render into the chart:

var labels = [
  {
    x: new Date('03-15-2014'),
    y: .17,
    text: 'Test Label 1',
    orient: 'right'
  },
  {
    x: new Date('10-25-2014'),
    y: .24,
    text: 'Test Label 2',
    orient: 'right'
  }
]

Then, based on this configuration I render the labels using D3. Note that I am using the same scales that are used in for the whole chart. This gives you some freedom to adjust the position of the labels by adjusting the values in their config object. Another thing I am doing here is to set the orientation of the labels. This is done by simply setting the text-anchor property on the text nodes.

function renderLabels() {
  chartWrapper.selectAll('text.label')
    .data(labels)
    .enter()
    .append('text')
    .attr('x', function(d) { return x(d.x) })
    .attr('y', function(d) { return y(d.y) })
    .style('text-anchor', function(d) { return d.orient == 'right' ? 'start' : 'end' })
    .text(function(d) { return d.text });
}

If you call renderLabels inside of the render function, you will notice that everytime the browser window is resized, new labels will be appended to the SVG. To prevent this, I check if labels are already existing. If the labels already exist, we just update their position respective to the scales. Now the renderLabels function looks like this:

function renderLabels() {

  var _labels = chartWrapper.selectAll('text.label');

  if(_labels[0].length > 0) {
    //labels already exist
    _labels
      .attr('x', function(d) { return x(d.x) })
      .attr('y', function(d) { return y(d.y) })
  }
  else {
    //append labels if function is called for the first time
    _labels
      .data(labels)
      .enter()
      .append('text')
      .classed('label', true)
      .attr('x', function(d) { return x(d.x) })
      .attr('y', function(d) { return y(d.y) })
      .style('text-anchor', function(d) { return d.orient == 'right' ? 'start' : 'end' })
      .text(function(d) { return d.text });
  }
}

If you look at the chart now, you can see that you have basic annotations that scale with the chart.

after-labels

Conclusion

I have created a public Gist containing the source code of the example. You can see a live demo as well. Most of the stuff I have mentioned in the article is also used in this production example that I have developed for Berliner Morgenpost recently.

The techniques I have mentioned here are just very basic examples. As with other examples, it always depend on what you want to visualize and what your dataset looks like. Anyway, you may find it useful to have a look at working examples and may reuse bits of code in your own charts. If you have any suggestions or found a mistake in my code, you can contact me via Twitter. Thanks for reading!

comments powered by Disqus