Skip to content

Instantly share code, notes, and snippets.

@austinczarnecki
Last active July 13, 2018 19:18
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 austinczarnecki/cc6371af0b726e61b9ab to your computer and use it in GitHub Desktop.
Save austinczarnecki/cc6371af0b726e61b9ab to your computer and use it in GitHub Desktop.
D3 sankey diagram with view options
license: gpl-3.0

This is a sankey diagram of election financing built using the Sankey plugin for D3. The buttons at the top can be used to change which section of the sankey is shown. I added this in order to make the graphic more accessible on mobile devices where tapping on individual links on the full-size diagram was too difficult. The data is up to date as of March 1, 2016.

source target value shortname
Contributions over $200 Hillary Clinton 100355684.00 Clinton
Contributions under $200 Hillary Clinton 21739942.00 Clinton
Party-affiliated PAC Hillary Clinton 1000.00 Clinton
Non-party PAC Hillary Clinton 943409.20 Clinton
Candidate contribution Hillary Clinton 468036.70 Clinton
Transfer from PAC Hillary Clinton 4440000.00 From PAC
Operating expenses offset Hillary Clinton 2478242.16 Clinton
Other income Hillary Clinton 17320.53 Clinton
Hillary Clinton Operating expenses 95891194.15 Clinton
Hillary Clinton Refunds to individuals 1599663.98 Clinton
Hillary Clinton Refunds to PACs 3000.00 Clinton
Hillary Clinton Other spending 11215.00 Clinton
Hillary Clinton Cash on hand 32938563.75 Clinton
Contributions over $200 Donald Trump 1870992.00 Trump
Contributions under $200 Donald Trump 5626992.00 Trump
Party-affiliated PAC Donald Trump 57.62 Trump
Candidate contribution Donald Trump 250318.96 Trump
Loans Donald Trump 17534058.41 Trump
Operating expenses offset Donald Trump 243899.54 Trump
Donald Trump Operating expenses 23677801.33 Trump
Donald Trump Transfer to outside PAC 173050.00 Trump
Donald Trump Refunds to individuals 90746.73 Trump
Donald Trump Cash on hand 1584720.97 Trump
Contributions over $200 Bernie Sanders 27370658.00 Sanders
Contributions under $200 Bernie Sanders 67393013.00 Sanders
Non-party PAC Bernie Sanders 3636.59 Sanders
Transfer from PAC Bernie Sanders 1500000.00 From PAC
Operating expenses offset Bernie Sanders 5336.29 Sanders
Other income Bernie Sanders 38778.56 Sanders
Bernie Sanders Operating expenses 80729110.78 Sanders
Bernie Sanders Refunds to individuals 880331.05 Sanders
Bernie Sanders Other spending 40030.00 Sanders
Bernie Sanders Cash on hand 14661951.33 Sanders
Contributions over $200 Ted Cruz 31445577.00 Cruz
Contributions under $200 Ted Cruz 22893560.00 Cruz
Non-party PAC Ted Cruz 58852.25 Cruz
Transfer from PAC Ted Cruz 250012.93 From PAC
Operating expenses offset Ted Cruz 6216.48 Cruz
Other income Ted Cruz 7288.43 Cruz
Ted Cruz Operating expenses 40744495.31 Cruz
Ted Cruz Refunds to individuals 268490.90 Cruz
Ted Cruz Refunds to PACs 400.00 Cruz
Ted Cruz Other spending 2700.00 Cruz
Ted Cruz Cash on hand 13645419.33 Cruz
Contributions over $200 John Kasich 7470876.00 Kasich
Contributions under $200 John Kasich 955219.00 Kasich
Non-party PAC John Kasich 212700.62 Kasich
Operating expenses offset John Kasich 10094.07 Kasich
John Kasich Operating expenses 7141157.38 Kasich
John Kasich Refunds to individuals 24590.00 Kasich
John Kasich Refunds to PACs 6500.00 Kasich
John Kasich Cash on hand 1476642.36 Kasich
Contributions over $200 Marco Rubio 27029786.00 Rubio
Contributions under $200 Marco Rubio 6493284.00 Rubio
Party-affiliated PAC Marco Rubio 101.68 Rubio
Non-party PAC Marco Rubio 404274.44 Rubio
Transfer from PAC Marco Rubio 662431.58 From PAC
Operating expenses offset Marco Rubio 52735.80 Rubio
Cash Marco Rubio 3338454.00 Rubio
Other income Marco Rubio 10038.31 Rubio
Marco Rubio Operating expenses 31830495.47 Rubio
Marco Rubio Refunds to individuals 897566.36 Rubio
Marco Rubio Refunds to PACs 202640.00 Rubio
Marco Rubio Other spending 5000.00 Rubio
Marco Rubio Cash on hand 5055406.64 Rubio
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="style.css">
<body>
<div class="election-wrapper">
<div id="sankey-labels">
<h4 class="sankey-label first" id="revenue-button">Revenues</h4> <!-- &#10132 -->
<h4 class="sankey-label" id="spending-button">Expenses</h4>
<h4 class="sankey-label last clicked" id="showall-button">All</h4>
</div>
<div id="sankey"></div>
</div>
</body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/d3/d3-plugins/master/sankey/sankey.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/Caged/d3-tip/master/index.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script>
var units = "USD",
linkTooltipOffset = 62,
nodeTooltipOffset = 130,
candidates = ["Bernie Sanders", "Hillary Clinton", "Marco Rubio",
"Donald Trump", "Ted Cruz", "John Kasich"];
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 900 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom;
/* Initialize tooltip */
var tipLinks = d3.tip()
.attr('class', 'd3-tip')
.offset([-10,0]);
var tipNodes = d3.tip()
.attr('class', 'd3-tip d3-tip-nodes')
.offset([-10, 0]);
function formatAmount(val) {
return val.toLocaleString("en-US", {style: 'currency', currency: "USD"}).replace(/\.[0-9]+/, "");
};
// append the svg canvas to the page
var svg = d3.select("#sankey").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("class", "sankey")
.call(tipLinks)
.call(tipNodes)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3.sankey()
.nodeWidth(36)
.nodePadding(14)
.size([width, height]);
var path = sankey.link();
d3.csv("data.csv", function(error, data) {
var currentData = data;
function processData(data) {
var graph = {"nodes" : [], "links" : []};
data.forEach(function (d) {
graph.nodes.push({ "name": d.source,
"shortname": d.shortname });
graph.nodes.push({ "name": d.target,
"shortname": d.shortname });
graph.links.push({ "source": d.source,
"target": d.target,
"value": +d.value });
});
graph.nodesNew = d3.nest()
.key(function (d) { return d.name; })
.rollup(function (d) { return d[0].shortname; }) // returns the shorname of the first element of that key
.map(graph.nodes);
// return only the distinct / unique nodes
graph.nodes = d3.keys(d3.nest()
.key(function (d) { return d.name; })
.map(graph.nodes));
// loop through each link replacing the text with its index from node
graph.links.forEach(function (d, i) {
graph.links[i].source = graph.nodes.indexOf(graph.links[i].source);
graph.links[i].target = graph.nodes.indexOf(graph.links[i].target);
});
//now loop through each nodes to make nodes an array of objects
// rather than an array of strings
graph.nodes.forEach(function (d, i) {
graph.nodes[i] = { "name": d,
"shortname": d };
});
return graph;
}
// "➡"
tipLinks.html(function(d) {
var title, candidate;
if (candidates.indexOf(d.source.name) > -1) {
candidate = d.source.name;
title = d.target.name;
var html = '<div class="table-wrapper">'+
'<h1>'+title+'</h1>'+
'<table>'+
'<tr>'+
'<td class="col-left">'+candidate+'</td>'+
'<td align="right">'+formatAmount(d.value)+'</td>'+
'</tr>'+
'</table>'+
'</div>';
} else {
candidate = d.target.name;
title = d.source.name;
var html = '<div class="table-wrapper">'+
'<h1>'+title+'</h1>'+
'<table>'+
'<tr>'+
'<td class="col-left">'+candidate+'</td>'+
'<td align="right">'+formatAmount(d.value)+'</td>'+
'</tr>'+
'</table>'+
'</div>';
}
return html;
});
tipNodes.html(function(d) {
var object = d3.entries(d),
nodeName = object[0].value,
linksTo = object[2].value,
linksFrom = object[3].value,
html;
html = '<div class="table-wrapper">'+
'<h1>'+nodeName+'</h1>'+
'<table>';
if (linksFrom.length > 0 & linksTo.length > 0) {
html+= '<tr><td><h2>Revenues:</h2></td><td></td></tr>'
}
for (i in linksFrom) {
html += '<tr>'+
'<td class="col-left">'+linksFrom[i].source.name+'</td>'+
'<td align="right">'+formatAmount(linksFrom[i].value)+'</td>'+
'</tr>';
}
if (linksFrom.length > 0 & linksTo.length > 0) {
html+= '<tr><td><h2>Spending:</h2></td><td></td></tr>'
}
for (i in linksTo) {
html += '<tr>'+
'<td class="col-left">'+linksTo[i].target.name+'</td>'+
'<td align="right">'+formatAmount(linksTo[i].value)+'</td>'+
'</tr>';
}
html += '</table></div>';
return html;
});
renderSankey();
//the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + d.x + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
function hasLinks(node, links) {
// checks if any links in links reference node
l = false;
links.forEach(function (d) {
if (d.source == node || d.target == node) {
l = true;
}
})
return l;
}
d3.select('#spending-button').on('click', function () {
d3.selectAll(".sankey-label").classed("clicked", false);
d3.select(this).classed("clicked", true);
currentData = data.filter(function (d) {
return candidates.indexOf(d.source)+1;
});
renderSankey();
});
d3.select('#revenue-button').on('click', function () {
d3.selectAll(".sankey-label").classed("clicked", false);
d3.select(this).classed("clicked", true);
currentData = data.filter(function (d) {
return candidates.indexOf(d.target)+1;
});
renderSankey();
});
d3.select('#showall-button').on('click', function () {
d3.selectAll(".sankey-label").classed("clicked", false);
d3.select(this).classed("clicked", true);
currentData = data;
renderSankey();
})
function renderSankey() {
d3.select('body').selectAll('g').remove();
graph = processData(currentData);
myLinks = graph.links;
myNodes = graph.nodes;
svg = d3.select('.sankey')
.attr("width", width)
.attr("height", height)
.append("g");
sankey = d3.sankey()
.size([width, height])
.nodes(myNodes)
.links(myLinks)
.layout(120);
path = sankey.link();
// add in the links
link = svg.append("g").selectAll(".link")
.data(myLinks)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; })
.on('mousemove', function(event) {
tipLinks
.style("top", (d3.event.pageY - linkTooltipOffset) + "px")
.style("left", function () {
var left = (Math.max(d3.event.pageX - linkTooltipOffset, 10));
left = Math.min(left, window.innerWidth - $('.d3-tip').width() - 20)
return left + "px"; })
})
.on('mouseover', tipLinks.show)
.on('mouseout', tipLinks.hide);
// add in the nodes
node = svg.append("g").selectAll(".node")
.data(myNodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; })
.on('mousemove', function(event) {
tipNodes
.style("top", (d3.event.pageY - $('.d3-tip-nodes').height() - 20) + "px")
.style("left", function () {
var left = (Math.max(d3.event.pageX - nodeTooltipOffset, 10));
left = Math.min(left, window.innerWidth - $('.d3-tip').width() - 20)
return left + "px"; })
})
.on('mouseover', tipNodes.show)
.on('mouseout', tipNodes.hide)
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() {
this.parentNode.appendChild(this); })
.on("drag", dragmove));
// add the rectangles for the nodes
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.attr("class", function(d) {
if (d.name == "Bernie Sanders" || d.name == "Hillary Clinton") { d.class = 'dem'; }
else if (candidates.indexOf(d.name) > 1) { d.class = 'rep'; }
else { d.class = 'none'; }
return d.class; });
if (true) {
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
} else {
node.append("text")
.attr("x", 6 + sankey.nodeWidth())
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "start")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", -6)
.attr("text-anchor", "end");
}
}
d3.select(window).on('resize.sankey', renderSankey);
});
</script>
svg text {
font-size: 12px;
stroke-width: 0;
fill: black;
}
body {
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
margin: 5px;
margin-bottom: 10px;
}
.election-wrapper {
display: flex;
flex-direction: column;
}
.sankey {
overflow: visible;
}
.sankey .node text {
pointer-events: none;
}
.sankey .link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
.sankey .link:hover {
stroke-opacity: .4;
}
.d3-tip h1 {
font-weight: 500;
font-size: 14px;
padding: 0;
margin-bottom: 5px;
width: 100%;
}
.d3-tip h2 {
font-weight: bold;
font-size: 12px;
padding-right: inherit;
padding-left: inherit;
padding-top: 2px;
padding-bottom: 2px;
margin: 0px;
}
.d3-tip h3 {
font-weight: normal;
font-size: 8px;
margin: 0;
padding: 0;
}
.d3-tip table {
font-weight: normal;
font-size: 12px;
padding: none;
margin: 0;
width: 100%;
border: none;
border-collapse: collapse;
}
.d3-tip td {
padding-top: 2px;
padding-bottom: 2px;
}
.d3-tip .col-left {
padding-right: 8px;
}
.d3-tip .table-wrapper {
margin: 0;
padding: inherit;
border: none;
}
.d3-tip {
line-height: 1;
font-weight: normal;
padding: 4px;
background: white;
color: black;
border-radius: 2px;
pointer-events: none;
background: white;
box-shadow: 1px 1px 4px grey;
}
#sankey-labels {
display: flex;
flex-direction: row;
align-self: center;
max-width: 900px;
justify-content: flex-start;
font-weight: bold;
font-size: 12px;
color: grey;
width: 100%;
margin-top: 8px;
margin-bottom: 8px;
margin-right: 20px;
margin-left: 20px;
}
.sankey-label {
flex: 1;
max-width: 80px;
color: #999;
font-size: 14px;
cursor: pointer;
float: left;
padding: 10px 18px;
border-top: solid 1px #CCC;
border-bottom: solid 1px #CCC;
border-left: solid 1px #CCC;
background: #f9f9f9;
margin: 0 0;
}
.sankey-label.clicked {
color: #000;
background: #e9e9e9;
border-color: #AAA;
box-shadow: inset 0px 0px 4px rgba(0,0,0,0.2);
}
.sankey-label.first {
border-radius: 4px 0 0 4px;
}
.sankey-label.last {
border-right: solid 1px #CCC;
border-radius: 0 4px 4px 0;
}
rect.dem {
fill: #3366ff;
stroke: #3366ff;
}
rect.dem:hover {
fill: #0040ff;
stroke: #0040ff;
}
rect.rep {
fill: #ff0000;
stroke: #ff0000;
}
rect.rep:hover {
fill: #cc0000;
stroke: #cc0000;
}
rect.none {
fill: #669999;
stroke: #669999;
}
rect.none:hover {
fill: #527a7a;
stroke: #527a7a;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment