A sankey diagram of a poll I conducted for Felix, Imperial's Student Newspaper. We asked students who voted in 2015 who they intended to vote for in the 2017 general election. Featured in Issue 1666 of Felix.
Uses d3-sankey.
license: mit |
A sankey diagram of a poll I conducted for Felix, Imperial's Student Newspaper. We asked students who voted in 2015 who they intended to vote for in the 2017 general election. Featured in Issue 1666 of Felix.
Uses d3-sankey.
<!DOCTYPE html> | |
<svg width="960" height="500"></svg> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://unpkg.com/[email protected]"></script> | |
<script> | |
var colours = { | |
"Conservative_2015": "#0087DC", | |
"Conservative_2017": "#0087DC", | |
"Labour_2015": "#DC241f", | |
"Labour_2017": "#DC241f", | |
"Green_2015": "#6AB023", | |
"Green_2017": "#6AB023", | |
"UKIP_2015": "#70147A", | |
"UKIP_2017": "#70147A", | |
"LiberalDemocrat_2015": "#FDBB30", | |
"LiberalDemocrat_2017": "#FDBB30", | |
"SNP_2015": "#FFFF00", | |
"SNP_2017": "#FFFF00", | |
"Abstained_2015": "#614126", | |
"Spoiled_2015": "#C3834C", | |
"Other_2015": "#7F7F7F", | |
"Other_2017": "#7F7F7F" | |
} | |
var svg = d3.select("svg"), | |
margin = {top: 50, right: 160, bottom: 50, left: 180}, | |
width = +svg.attr("width") -margin.left - margin.right, | |
height = +svg.attr("height") - margin.top - margin.bottom; | |
var g = svg.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var formatNumber = d3.format(",.0f"), | |
color = d3.scaleOrdinal(d3.schemeCategory10); | |
var sankey = d3.sankey() | |
.nodeWidth(15) | |
.nodePadding(10) | |
.iterations(1) | |
.extent([[1, 1], [width - 1, height - 6]]); | |
var link = g.append("g") | |
.attr("class", "links") | |
.attr("fill", "none") | |
.attr("stroke", "#000") | |
.attr("stroke-opacity", 0.2) | |
.selectAll("path"); | |
var node = g.append("g") | |
.attr("class", "nodes") | |
.attr("font-family", "sans-serif") | |
.attr("font-size", 10) | |
.selectAll("g"); | |
var indexLookup = {}; | |
var pollNodes = { | |
"nodes": [], | |
"links": [] | |
}; | |
d3.json("poll.json", function(error, poll) { | |
if (error) throw error; | |
// first convert the data into a suitable format for generating a sankey diagram | |
// generate 2015 nodes | |
for (var party2017 in poll) { | |
if (party2017 == "Conservative") { | |
for (var party2015 in poll[party2017]) { | |
if (party2015 !== "Total" && party2015 !== "ICouldNotVote") { | |
pollNodes["nodes"].push({ | |
"name": party2015 + "_2015" | |
}); | |
var currentSize = pollNodes["nodes"].length - 1; | |
indexLookup[party2015 + "_2015"] = currentSize; | |
} | |
} | |
} | |
if (party2017 !== "UKIP" && party2017 !== "Green" && party2017 !== "SNP" | |
&& party2017 !== "ICannotVote" && party2017 !== "IWillSpoilMyBallot" | |
&& party2017 !== "IDoNotIntendToVote(ButIAmEligibleTo)") { | |
// generate 2017 nodes | |
pollNodes["nodes"].push({ | |
"name": party2017 + "_2017" | |
}); | |
} | |
var currentSize = pollNodes["nodes"].length - 1; | |
indexLookup[party2017 + "_2017"] = currentSize; | |
} | |
var total = 0; | |
for (var party2017 in poll) { | |
for (var party2015 in poll[party2017]) { | |
if (party2015 !== "Total" && party2015 !== "ICouldNotVote") { | |
if (poll[party2017][party2015] !== 0) { | |
pollNodes["links"].push({ | |
"source": indexLookup[party2015 + "_2015"], | |
"target": indexLookup[party2017 + "_2017"], | |
"value": poll[party2017][party2015] | |
}); | |
total += poll[party2017][party2015]; | |
} | |
} | |
} | |
} | |
// generate sankey layout | |
sankey(pollNodes); | |
link = link | |
.data(pollNodes.links) | |
.enter().append("path") | |
.attr("d", d3.sankeyLinkHorizontal()) | |
.attr("stroke-width", function(d) { return Math.max(1, d.dy); }) | |
.attr("stroke", function(d) { | |
return colours[d.source.name]; | |
}) | |
.on("mouseover", function() { | |
d3.select(this) | |
.attr("stroke-opacity", 0.6); | |
}) | |
.on("mouseout", function() { | |
d3.select(this) | |
.attr("stroke-opacity", 0.2); | |
}) | |
link.append("title") | |
.text(function(d) { return d.value; }); | |
node = node | |
.data(pollNodes.nodes) | |
.enter().append("g") | |
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); | |
node.append("rect") | |
.attr("height", function(d) { return d.dy; }) | |
.attr("width", sankey.nodeWidth()) | |
.attr("fill", function(d) { | |
return colours[d.name] || "#D3D3D3"; | |
}) | |
.append("title") | |
.text(function(d) { return d.name + "\n" + d.value; }); | |
node.append("text") | |
.attr("x", sankey.nodeWidth() + 10) | |
.attr("y", function(d) { return d.dy / 2; }) | |
.attr("dy", "0.35em") | |
.attr("text-anchor", "start") | |
.attr("transform", null) | |
.text(function(d) { return d.name.slice(0, -5) + ": " + Math.round(d.value / total * 100) + "%"; }) | |
.filter(function(d) { return d.x < width / 2; }) | |
.attr("x", sankey.nodeWidth() - 25) | |
.attr("text-anchor", "end"); | |
}); | |
</script> |
{ | |
"Labour": { | |
"Conservative": 9, | |
"Labour": 21, | |
"LiberalDemocrat": 11, | |
"UKIP": 1, | |
"Green": 9, | |
"SNP": 1, | |
"Other": 2, | |
"ICouldNotVote": 18, | |
"Abstained": 6, | |
"Spoiled": 1, | |
"Total": 79 | |
}, | |
"Conservative": { | |
"Conservative": 15, | |
"Labour": 2, | |
"LiberalDemocrat": 0, | |
"UKIP": 2, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 8, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 27 | |
}, | |
"LiberalDemocrat": { | |
"Conservative": 6, | |
"Labour": 0, | |
"LiberalDemocrat": 2, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 2, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 10 | |
}, | |
"Other": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 1, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 1, | |
"Spoiled": 0, | |
"Total": 2 | |
}, | |
"UKIP": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"Green": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"SNP": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"ICannotVote": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"IDoNotIntendToVote(ButIAmEligibleTo)": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"IWillSpoilMyBallot": { | |
"Conservative": 0, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 0, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 0 | |
}, | |
"Undecided": { | |
"Conservative": 1, | |
"Labour": 0, | |
"LiberalDemocrat": 0, | |
"UKIP": 0, | |
"Green": 0, | |
"SNP": 0, | |
"Other": 0, | |
"ICouldNotVote": 2, | |
"Abstained": 0, | |
"Spoiled": 0, | |
"Total": 3 | |
} | |
} |