Data Visualisation with d3

dbillinghamuk
7 min readSep 25, 2017

Intro

I recently decided to watch a FrontEnd Masters course by Shirley Wu on data visualisations with the d3 library version 4. This blogpost documents my key takeaways from this course.

Editor ‘Block Builder’ blockbuilder.org

Selection

Select the three rect svg elements and assign the data to create a bar graph.

<svg>
<rect />
<rect />
<rect />
</svg>
const data = [10,20,30];
const rectWidth = 100;
const height = 40;
d3.selectAll(‘rect’) //select the three elements
.data(data) //map the data values to each of the rects
.attr(‘x’, (d, i) => i * rectWidth) //set the X coordinate to the index plus the width.attr(‘y’, d => height — d) //set the Y coordinate equal to height minus the data value as otherwise the graph will look upside down.attr(‘width’, rectWidth)
.attr(‘height’, d => d)
.attr(‘fill’, ‘blue’)
.attr(‘stroke’, ‘white’);

Enter-append

This is used when we don’t know the number of rects in our data, so we just have an outer element of <svg></svg>. The enter function will create a given number of empty placeholders depicted on the length of the data, (3 in our case). And function append will assign a given element for each placeholder.

d3.select(‘svg’)
.data(data)
.enter()
.append(‘rect’)

Customising column colour

If we want to make certain columns different colours (e.g. red given a value is 30 or over and blue if under), we can just set the fill property based on the array value.


.attr(‘fill’, d => d >= 30 ? ‘red’ : ‘blue’ )

Scales

There are different types of scales but continuous is the most common. Scales map input data values to output values. So in the example above we are assuming each data value, (10, 20, 30), is a pixel, but we may want the bars height increased to say, (100, 200, 300); scales will help with this.

const yScale = d3.scaleLinear()
.domain([0, 30]) //input — our data has a min value of 0 and a max value of 30
.range([0, 300]) //output — we want the bars to display with a min value 0f 0 and a max value of 300

Min, max and extent

It is common that we will not actually know the min and max data values, we can use d3s min and max functions to get these values from our data.

const min = d3.min(data); //will set min to 10
const max = d3.max(data); //will set max to 30

Or we can use d3s extent function which, given the data, will output; [min, max], (which is want domain expects).

const extent = d3.extent(data) //will set extent to [10, 30]

The scale is just a function which when given a value will return the corresponding output value, so we call it like this;yScale(10) //output 100. So to use our yScale in our bar chart, we will assign it to the y attribute.

d3.select(‘svg’)
.data(data)
.attr(‘y’, yScale) //so for each data value we will pass it to the scale function and this will return the scaled value, same; .attr(‘y’, d => yScale(d))

Axis

You can use scales to create x and y axis based on the scale data.

const yScale = d3.scaleLinear()
.domain(d3.extent(data))
.range(0, 500);
const yAxis = d3.axisLeft() //axis is on the left
.scale(yScale);
const axis = d3.select(‘svg’)
.append(‘g’) //creates a group element so that the axis can be added to the svg. To add elements to an svg, they have to be added to a group first, then that group can be manipulated. Like adding a div within another div and manipulating it within the context of its parent.
.attr(‘transform’, ‘translate(40, 20)’) //transform the group element which will contain our axis so that it is not hidden off the left edge of the svg.call(yAxis); //selection.call(yAxis) is equal to yAxis(selection), and it adds the axis to the group selection

If you look at the DOM you will see that the axis is like any other rendered elements meaning you can make selections and edit properties as you see fit. This can be useful if you want to format the value, (e.g. if it is currency you can prepend a £ symbol). So we can set the axis text colour to red given the value is over 300.

axis.selectAll(‘text’)
.attr(‘fill’, d => d >= 300 ? ‘red’ : ‘blue’ )

The term ticks is used to depict the number of values on the axis. tickFormat can be used to format each tick.

yAxis.ticks(10) //y axis will now have 10 values, (as the range is 0 > 500), [0, 50, 100, 150, 200 …]tickFormat(d => `${d} count`) // y axis will have values [‘0 count’, ’50 count’, ‘100 count’ …]

svg path

So far we have looked at bar charts using rect svg elements but paths can be used to create lines and different shapes, (commonly line graphs). The rect element has attributes such as x, y, height and width. The path element has d which is the path to follow to create the line/curve. The svg d attribute can have a value such as M10 10 H 90 V 90. With these curves you can create simple shapes and add them on top of each other to create even more shapes. See film flowers project by Shirley Wu. You can use transform attribute to scale and rotate elements.

d3 shapes

Can be used when you want to create predefined shapes and not your own using svg path, e.g. pie charts, line graphs etc. It essentially creates the svg path d attribute for you.

d3 line

Used for line graphs

const data = [
{ key: ‘one’, value: 10 },
{ key: ‘two’, value: 20 },
{ key: ‘three’, value: 30 }
];
const line = d3.line()
.x(d => xScale(d.key)) //pass our keys through our scales
.y(d => yScale(d.value)); //pass our values through our scales
.curve(d3.curveCatmullRom) //optional — we can set what type of curve we wantd3.select(‘svg’)
.append(‘path’) //uses the enter-append pattern above, although we don’t need enter as we only have one value, (one path element)
.attr(‘d’, line(data)) //pass the data into the line function and assign it to the `d` attribute so that the line function can calculate all the `d` values for us.attr(‘fill’, ‘none’) //as the path will be closed the browser will fill the svg with the colour black.attr(‘stroke’, ‘blue’)

d3 pie

This is used to calculate the start and end angle of the pie chart given a set of data

const data = [10,20,30];const pies = d3.pie()(data); //output — [ { data: 10, value: 10, startAngle: 6.3434234, endAngle: 6.67878788 } …….]

d3 arc

Once you have the pie’s angles you can use d3.arc to work out the arc of your pie segment.

const arc = d3.arc()
.innerRadius(0)
.outerRadius(100) //radius of 100 px
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle)
arc(pies) //output — the path `d` values (e.g. M-23.3453, -97.344534)

Using pie and arc

d3.select(‘svg’)
.append(‘g’)
.attr(‘transform’, ‘translate(200, 200)’) //move so it can be seen completely on screen.selectAll(‘path’)
.data(pies)
.enter()
.append(‘path’)
.attr(‘d’, arc) //same as passing each pie value to arc, e.g. .attr(‘d’, d => arc(d))
… fill, stroke

Colours

d3 has some built in colour pallets under the scales module.

const colours = d3.scaleOrdinals().range(d3.schemeCategory10)

Enter and update

This is used for when data is constantly changing, (live data), and we need to transition into the new state. User should be able to follow what has been changed in the state, (object consistency), (used the FLIP technique previously in react; First, Last, Invert and Play).

const bars = svg.selectAll(‘rects’) //these are the updated value from the new state, so if 3 of 5 values have been updated, bars will contain 3 react values in groups.data(data, d => d) //important — each data element must have a distinct keybars.exit().remove //we want to remove any keys which are not in the new state, so if 2 of the 5 values have been removed, bars.exit will contain those 2 valuesconst enter = bars.enter() //we need to deal with any new values, so if 1 new value has been added, bars.enter will contain the 1 new value.append(‘rect’) //we want to create new rect elements for any new values in the new state.attr… //attributes that are not data value dependent (width, stroke, fill etc…)bars = enter.merge(bars) //we merge the existing updated bars into the new bars.attr … //attributes that are data value dependent (x, y, heigh etc…)

d3 transitions

When a new state comes in we need a nice way of transitioning out the old state and in the new state. We create a d3 transition and then assign that to the exit, enter and update selections. Only attributes assigned after the transition will be transitioned, if you do not want specific attributes transitioned, then these need to be declared before the transition.

const t = d3.transition()
.duration(1000);
bars.exit()
.transition(t)
.attr(‘height’, 0) //we transition the height from its original value to 0px over a duration of 1 second
.remove();
enter.merge(bars)

.transition(t)
.attr(‘height’, d => d) //we transition the height from its original value to its new value over a duration of 1 second

You may also want to specify what you want to transition from as well as to in some cases. So for example if you want new enter rects to have a from height of 100px, (instead of zero), and for it to transition to its value height, you can set the initial height attribute for the enter values.

Setting a timer and update function

const data = [
{ key: 1, name: ‘dog’, value: 10 },
{ key: 2, name: ‘dog’, value: 20 },
{ key: 3, name: ‘cat’, value: 30 },
{ key: 4, name: ‘rabbit’, value: 40 },
{ key: 5, name: ‘cat’, value: 20 },
{ key: 6, name: ‘cat’, value: 10 },
{ key: 7, name: ‘bird’, value: 10},
{ key: 8, name: ‘dog’, value: 80},
{ key: 9, name: ‘cat’, value: 10}
]
const update = (data, name) => { const newData = data.filter(d => d.name === name); //filter our data based on the name passed in const circles = svg.selectAll(‘circle’)
.data(newData, d => d.key);
circles.exit()
.remove();
const enter = circles.enter()
.append(‘circle’)
.attr(‘r’, ‘10px’)
circles = enter.merge(circles)
.attr(‘cx’, d => xScale(name)) //scales not defined in example
.attr(‘cy’, d => yScale(d.value))
}
const index = 0;
const names = [‘dog’, ‘cat’, ‘bird’, ‘rabbit’];
setInterval(() => {
update(data, names[index]); //every 2 seconds we call the update function which will re-draw our circle graph
index = index === 3 ? 0 : index + 1;
}, 2000);

Refs

--

--

dbillinghamuk

Software dev — Javascript, node, express, mongo, react, redux, rxjs, es6, ramda