|
<!DOCTYPE html> |
|
<svg width="960" height="500"></svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://unpkg.com/d3-sankey@0.5"></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> |