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.
Last active
July 13, 2018 19:18
-
-
Save austinczarnecki/cc6371af0b726e61b9ab to your computer and use it in GitHub Desktop.
D3 sankey diagram with view options
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> <!-- ➔ --> | |
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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