Data Visualisation with d3
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);