Responsive D3.js, scales and axis

This will guide you through a script with a responsive axis. The aim is a d3js component that scales according to the width of its container while the height remains fixed. The reason for the variable width and fixed height is that it works well on a variety of screens and allows consistent font sizes.

This tuorial is based on code examples on the site d3indepth.com. You can read about the different axis on d3indepth, this article will focus on responsive axis rather than all kinds of axis. Only the horizontal axis will be made responsive. The reason for this is that it is generally much easier to have a fixed height as long as you stay within the range of what most screens can show. The width of a screen or a browser can typically vary much more.

The code examples have then been given the responsive treatment and can be found in this article. For every code example here I will point back at the d3indepth article that I stole the code from. You might want to familiarize yourself with the d3indepth content before trying this.

What are axis in D3?

An axis in D3 is a visual element that represents the range of the data and it can show what unit is used in the data. D3 has very a very flexible axis component that can represent many different types of units, and it does a good job of adding ‘ticks’ that divide the axis with regular reference points that make it easier for the user to grasp the data.

Regular linear scale based axis

The axis below is based on a scale with a domain between 0 and 1. The output range is dynamic and will change if you resize your browser. Try it now and notice how the axis resizes and how the number of ticks changes.

Axis based on a time scale

The axis below is based on a time scale. The start time of the axis is one week before you opened this webpage. The end time of the axis is the moment you opened this webpage. Again try to resize your browser and see what time unit the axis chooses to show you. These priorities are made by the d3 library and as the designer my only input to this axis is the amount if ticks I would like. That is changed based on the width of the screen.

The first article on d3indepth.com is about scales. One thing I find about this article is that it goes into depth about scales, but doesn’t mention much about axis so you will find more about that here. First we should clarify the significance scale and axis in d3js.

The scale refers to the data itself

The scale determines the the range of the input data and then maps that to a defined output range which is often mapped to pixel values. If you for example have an input range of 0 – 10000 meters you map these values to an output range of 0-600 pixels on the screen. In order to set the domain of a dataset it is useful to know about d3’s extents and the d3 min and max methods. An extent is simply an array with two values:

let extentX = [0, 10000];

In order to get some help from d3 in determining the extent, we should use the min and max methods that will return the smallest and the greatest value within a dataset.

d3.min(dataList, function (d) {return d.xVal})

The code above will return the smallest value from all the xVals in dataList. Below is an extent array that will get the smallest and the largest xVals from dataList.

//extentX defines the extent of the input data. An extent is just an array with two values. 
let extentX = d3.extent([d3.min(dataList, function (d) {return d.xVal}),d3.max(dataList, function (d) {return d.xVal})]);

In a responsive component you would map the output range to something like 0-width. That would the represent the area the component will occupy where 0 is the left boundary and width is the right boundary. This will prevent the compnent from poking outside the browser, and also ensures it fills the entire width. In code, that will look like this:

let xScale = d3.scaleLinear()
    .domain(extentX)
    //.range([60, (width - 80)])
    .range([0, width])
    .nice();

A draw function for responsive d3

Oddly enough most d3 content is not responsive. Responsive d3 components are slightly more complex, but the payoff is that they will scale to any browser size. In order for a d3 component to be responsive certain methods must be placed within a draw function. The draw function will run once when the component is opened for the first time, and then it will run if the user resizes the browser window. Here is the event listener for the reaize event:

  window.addEventListener("resize", draw);

Here is the draw function. We will pick it apart to look at the different bits that make it work.

function draw(){
        bounds = svg.node().getBoundingClientRect(),
          width = bounds.width,
          height = bounds.height;
        linearScale.rangeRound([0, (width-50)]);
        axis = d3.axisBottom(linearScale);
        let tickVal = (Math.ceil(width/66));
        if (tickVal > 32){tickVal = 32};
        if (tickVal < 2){tickVal = 2};
        axis.ticks(tickVal);
        d3.select('.axis')
            .call(axis);
    };

First notice the the getBoundingClientRect() methid. This gets a rectangle that defines the svg element and stores it in a variable named bounds. From bounds we will get width. The width will then be used to redefine the output range of the linearScale (x-scale).

 linearScale.rangeRound([0, (width-50)]);

Redefining the range means all visual elements that are redrawn based on this scal will place themselves within the new width and be visible on the screen.

Adjusting the ticks

We need to adjust the number of ticks for the axis to look good at different widths. The following code will determine how many ticks we will widh for from d3. I write wish because d3 in the end determines the exact amount of ticks based on the data in the domain. This piece of code has what is known as a magic number and the magic number is 66. Many programmers hate using magic numbers but for simple calculations like this it is useful. The magic number represents the approximate space in pixels between each tick. A higher number will result in less ticks and a lower number causes more ticks. the if statements sets the max amount of ticks to 32 and te minimum to 2.

 let tickVal = (Math.ceil(width/66));
        if (tickVal > 32){tickVal = 32};
        if (tickVal < 2){tickVal = 2};
        axis.ticks(tickVal);

We now need to call the axis to make it adapt to the new changes.

axis = d3.axisBottom(linearScale);
d3.select('.axis')
            .call(axis);

Play around with the responsive axis on Codepen

The time axis

The time axis is very similar to the linear axis. The main difference is that there is less tick due to the larger label on each tick. In its current state the time axis draws a timeline starting one week before you opened the element in the browser and until the moment you opened the browser. Try changing the firstDate value to get a different timeline. There are several commented out values you can try.

Try the time axis on codepen