Skip to content

Instantly share code, notes, and snippets.

@tlfrd
Last active July 11, 2018 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tlfrd/4e4be0b07ef5be6625d2c4d0322a6891 to your computer and use it in GitHub Desktop.
Save tlfrd/4e4be0b07ef5be6625d2c4d0322a6891 to your computer and use it in GitHub Desktop.
Sankey Diagram
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/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>
{
"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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment