The Times poll of polls in d3

There was quite a bit of excitement in the newsroom when news came in that prime minister Theresa May, contrary to her promises, was calling for a snap election on June 8, 2017.

For us, well, that means a slight change of plans. Throwing the forward planning stuff away and focusing on the big event to come.

For me, that meant jumping on building our lead graphics for the weeks to come: the poll of polls.

Creating the canvas

The canvas itself is nothing fancy: just a set of axes to host our visualisation.

Note that we hard-code the y axis to run between 0 and 50% polling intentions.

Also, we’re making our x axis show the abbreviated month name or the full year instead of January.

// Scales and their respective domains
const x = d3j.scaleTime().range([0, width]);
const y = d3j.scaleLinear().range([height, 0]);

const var xExtent = d3j.extent(dataset.averages, d3j.f('date'));
const var yExtent = d3j.extent([0, 50]);

x.domain(xExtent);
y.domain(yExtent);

// X-axis
g
.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(
d3j.axisBottom(x).ticks(config.ticksCount).tickFormat(function(dataset) {
const months = d3j.timeFormat('%-b')(dataset);
if (months === 'Jan') {
return d3j.timeFormat('%Y')(dataset);
} else {
return months;
}
})
);

// Y-axis
g
.append('g')
.attr('class', 'axis axis--y').call(
d3j
.axisLeft(y)
.ticks(5)
.tickSize(-width)
.tickPadding(20)
.tickFormat(d => `${d}%`)
);

The EU referendum

Annotations are important. We want to explain visually and immediately the change in the polls that followed the very important referendum on exiting the EU that took place on June 23 last year. We thus draw a vertical line at this date, with a label on top.

No interactivity needed, it’s all there on first paint.

const rule = g.append("line")
.at({ class: "rule",
x1: x(new Date("06-23-2016")),
x2: x(new Date("06-23-2016")),
y1: -20,
y2: height })
.st({stroke: "#666", strokeWidth: 1});
g.append("text")
.at({ class: "ruleLabel",
x: x(new Date("04-10-2016")),
y: -15,
dy: "-1rem" })
.html("EU referendum");

Adding the scatterplot

For each party in the dataset we append a set of dots for each poll we receive. Nothing terribly exciting here, the x/y coordinates of the dots are date and polling intentions.

for (var i = 0; i < parties.length; i++) {
g
.selectAll('dot')
.data(dataset.raw)
.attr('class', 'dots ' + parties[i].name)
.enter()
.append('circle')
.attr('r', 5)
.at({
cx: function(d) {
return x(d.date);
},
cy: function(d) {
return y(d[parties[i].name]);
},
})
.st({
fill: parties[i].color,
opacity: config.opacity,
});

Adding the line and handling missing values

d3 defined() was a life saver here.

Read Showing missing data in line charts for more about this.

var popline = d3j
.line()
.defined(d => {d.averages[parties[i].name] != null})
.x(d => x(d.date))
.y(d => y(d.averages[parties[i].name]));

var filteredData = dataset.averages.filter(popline.defined());

g
.append('path')
.datum(filteredData)
.at({
d: popline,
fill: 'none',
strokeWidth: 1.5,
stroke: parties[i].color,
})
.style('stroke-dasharray', '3, 3');
g.append('path').datum(dataset.averages).at({
fill: 'none',
stroke: parties[i].color,
strokeWidth: 1.5,
d: popline,
});

Wait, objects?

I’m using d3-jetpack to do some bits in here, and the result will look a tad confusing to those used to the rather standardised d3 code we read on bl.ocks.org every day. That said, passing object of attributes and of camel-cased CSS styles to d3 selections looks rather attractive to me.

d3-jetpack is rather brutal in inserting its methods directly onto d3 itself, but if you can live with it, you get this:

Instead of:

.attr('d', line)
.attr('fill', 'none')
.attr('stroke-width', 1.5)
.attr('stroke', parties[i].color)

We can do:

.at({
d: line,
fill: 'none',
strokeWidth: 1.5,
stroke: parties[i].color,
})

Also, d3-jetpack has a Tom Gauld cartoon in its README.


Originally published at blog.basilesimon.fr on April 27, 2017.