Creating a chart with D3 (v4) and TypeScript (or ES6)

Introduction

A while ago, I posted a series of articles about creating a chart using D3 and TypeScript. The goal of this article will be to create the same kind of chart, using the latest versions of the two products.

At the end of this article, we should be getting something similar to this:
Chart

This chart represents the scores of the last 25 posts on reddit, using the reddit API.

Though written with TypeScript, this example should work perfectly with ES6.

Prerequisite

Before getting started, I will assume that you have a properly working TypeScript environment, and required packages installed. If you don’t, or don’t know how, check my previous article, and come back here once ready.

I also presume that you have basic knowledge of JavaScript, css, and svg.

Chart structure

Before even starting coding, I think it is worth discussing how the chart will be structured.
Structure
The chart above introduced some colors to be able to identify the main parts constituting the chart (I used coolors to generate the palette, but I am no graphics designer, sorry for that). As we can see, we have 4 (well, 5) parts on this chart:

  • the svg (dark background)
  • the plot area (light background)
  • the axes (blue text)
  • the points (orange)

This means we will have to create those different parts, in the following sequence:

  • create an svg
  • create a plot area
  • append a group for the x-axis in the plot area
  • append a group for the y-axis in the plot area
  • append a group for the points in the plot area

Why everything in the plot area?
The idea behind this structure is that everything will be related to the plot area size. That way, we will compute the plot area size, and just position everything relative to that. This will make position management way easier.

Let’s get coding!

The shell

Now that we know what we are heading towards, lets get our feet wet. In your main file (called app.ts if you followed the structure I proposed I my previous article), let’s add the following code:

const width = 960;
const height = 480;

let svg = d3.select('body')
    .append('svg')
    .attr('width', width)
    .attr('height', height);

let plotMargins = {
    top: 30,
    bottom: 30,
    left: 150,
    right: 30
};
let plotGroup = svg.append('g')
    .classed('plot', true)
    .attr('transform', `translate(${plotMargins.left},${plotMargins.top})`);

let plotWidth = width - plotMargins.left - plotMargins.right;
let plotHeight = height - plotMargins.top - plotMargins.bottom;

If you refer to the chart structure above, you will realize that this creates the first 2 parts of our chart:

  • appending an svg element with a width of 960px and a height of 480px
  • appending a group with a class of plot, located 150px form the left of the svg and 30px from the top

By doing this, we already used some core functionalities of D3:

  • d3.select grabs hold of an element on the page using a valid css selector, and returns a selection
  • selection.append creates a new element within a selection, and returns the resulting selection
  • selection.attr appends or modifies an html attribute to a given selection. I used ES6 template litterals to build the transform string

Finally, the width and height of the plot are computed based on the svg size and margins.

So far so good? Let’s append axes!

The axes

Our chart will need two axes:

  • an x-axis, at the bottom of the plot, with time increasing left to right
  • a y-axis, at the left of the plot, with score increasing top to bottom

Let’s do that:
x-axis

let xScale = d3.scaleTime()
    .range([0, plotWidth]);
let xAxis = d3.axisBottom(xScale);
let xAxisGroup = plotGroup.append('g')
    .classed('x', true)
    .classed('axis', true)
    .attr('transform', `translate(${0},${plotHeight})`)
    .call(xAxis);

y-axis

let yScale = d3.scaleLinear()
    .range([plotHeight, 0]);
let yAxis = d3.axisLeft(yScale);
let yAxisGroup = plotGroup.append('g')
    .classed('y', true)
    .classed('axis', true)
    .call(yAxis);

Though not exactly the same, the pattern to build an axis with D3 is always the same:

  • creating a scale with a given range (the output of the scale)
  • creating an axis with a given orientation, linked to the scale
  • creating a group to render the axis
  • calling the axis from the selection

As can be seen, the use of the plot to manage our layout becomes more obvious: if I decide to reduce the left margin for example, I simply need to change the plotMargins object, and everything will flow gracefully. It also made it very easy to position the x-axis at the bottom of the plot, exactly joining the y-axis. It should also be noted that the y scale range goes from plotHeight to 0. This allows to place the lower scores at the bottom and the higher at the top. If we did not do this, the axis would be in the opposite direction.

Finally, let’s add a group to contain our points:

let pointsGroup = plotGroup.append('g')
    .classed('points', true);

Still OK? Moving on!

Reading and binding to data

The reading

Now that we have our structure in place, it is time to get some data. Reading data using D3 is actually very simple:

d3.json<redditObject>('https://api.reddit.com', (error, data) => {
    if (error) {
        console.error(error);
    } else {
        console.log(data);
    }
});

Not much explanation needed: the d3.json function can be used to read data from an url (first parameter – can be a local file), and calls the callback function (second parameter), with two arguments, the first being an error if any, the second being the actual data. As we are using TypeScript, I used the generic version of d3.json to get intellisense and type checking on the data object. For this to work, you will need to import the redditFormat from a file called redditFormat.d.ts located in the same folder as your app.ts, by adding the following line at the top of app.ts:
import { redditObject } from './redditFormat';

The redditFormat.d.ts file can be found here.

The mapping

As you will see in the console, the reddit API send a lot of information back, that we do not really care about. We need a simpler object with a score and a creation date. We will therefore map these data as needed. Let’s replace console.log(data); with:

let prepared = data.data.children.map(d => {
    return {
        date: new Date(d.data.created * 1000),
        score: d.data.score
    }
}); 
console.log(prepared);

The date received from the API is a Unix timestamp, that we need to convert to a Javascript Date object.

Updating scales and axes

We can now update our scales and axes to reflect those data:

xScale.domain(d3.extent(prepared, d => d.date))
    .nice();
xAxisGroup.call(xAxis);

yScale.domain(d3.extent(prepared, d => d.score))
    .nice();
yAxisGroup.call(yAxis);

For both axes, the strategy is the same:

  • getting the min and max of data to draw (date or score) using d3.extent, which does just that, taking an array and a callback to evaluate as parameters
  • setting the domain of the scale (the input of the scale)
  • using .nice() on the scale to get a prettier rounding of the domain
  • calling the axis on each group to render

To understand how a scale works, refer to the d3-scale readme.

Drawing the points

Our scales and axes being ready and drawn, we can now draw a point per post, by binding a selection to our prepared data:

var dataBound = pointsGroup.selectAll('.post')
    .data(prepared);

// delete extra points
dataBound
    .exit()
    .remove();

// add new points
var enterSelection = dataBound
    .enter()
    .append('g')
    .classed('post', true);

// update all existing points
enterSelection.merge(dataBound)
    .attr('transform', (d, i) => `translate(${xScale(d.date)},${yScale(d.score)})`);

Updating elements based on data in D3 is a 5 steps process:

  • using selection.selectAll to retrieve elements based on a valid css selector
  • using selection.data to actually bind to the data
  • using .exit().remove() to delete extra elements
  • using .enter().append to create new elements
  • using .merge to update all elements

This means that when receiving new data, based on a given selection of element and newly provided data, D3 will automatically check if new elements need to be added or removed to/from the DOM.

When using .attr on a bound selection, the second parameter can take the form of a callback taking two arguments: the actual data point, and the index of the element in the array of data. In our case, we simply moved a group in the plot area to its position computed based on our scales. I used an arrow function to provide the callback parameter. This will not be an issue in this case, I will not expand on why here, but keep in mind that you might get errors if you need to use the this keyword in your callback.

We now need to add points to our chart. We will simply create circles, during the enter phase, as circles only need to be created for new elements:

enterSelection.append('circle')
    .attr('r', 2)
    .style('fill', 'red');

And your chart is now complete!

Conclusions

This article explored the main aspects of working with the new versions of D3, and TypeScript. We saw how to work with modules, types inference, axes, data binding, and selections.

However, this example now has a couple of issues:

  • it is not reusable
  • it is strongly linked to the data we read
  • the size of the Javascript bundle is 189KB, for a basic line chart

But fixing these issues is a subject for another article. Thanks for keeping up.

The code for this completed example can be found on GitHub.

Advertisements
Leave a comment

3 Comments

  1. Thanks for your help. I don’t know where to contact you

    I have updated the fiddle, you can find the link for the same in the comments

    https://stackoverflow.com/questions/45842149/change-bar-chart-to-line-chart-in-d3?noredirect=1#comment78669992_45842149

    Like

    Reply
  1. Creating a chart with D3 and TypeScript – part 1 | Wandering in the community
  2. D3 v4 and TypeScript: getting set up | Wandering in the community

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: