Remaining goals for this piece:
1) animated transitions between years
2) line or bar chart below which will show a time series of the budget item selected by the user by clicking on the sankey
3) adjust discretionary/mandatory arrangement so the net interest link doesnt overlap
a visualization by MasonChinkin, posted together as a #d3js learning exercise on night after a d3 meetup
see the example in webpage form as well at
https://masonchinkin.github.io/us-budget-sankey.html#portfolio
<!DOCTYPE HTML>
<html>
<head>
<title>US Federal Budget Balance, 1968-2017</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=1000" />
<script src="d3.js"></script>
<script src="sankey.js"></script>
<script src="d3-annotation.min.js"></script>
<script src="d3-simple-slider.min.js"></script>
<style type="text/css">
.node rect {
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0px 0px 2px #fff;
font-weight: 1000;
}
.link {
fill: none;
stroke: #000;
stroke-opacity: .5;
}
.link:hover {
stroke-opacity: 1;
}
</style>
<link rel="stylesheet" href="main.css" />
</head>
<body>
<!-- Main -->
<div id="main">
<section id="portfolio" class="two">
<div id="container" class="container">
<div class="row align-items-center">
<div id="slider"></div>
</div>
<script src="us-budget-sankey.js"></script>
</div>
<!-- <div id="timeline-container" class="container">
<script src="d3-visualizations/XXX.js"></script>
</div>
<div id="line-container" class="container">
<script src="d3-visualizations/XXX.js"></script>
</div> -->
</section>
</div>
</body>
</html>
// https://github.com/d3/d3-selection-multi Version 1.0.1. Copyright 2017 Mike Bostock.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('d3-selection'), require('d3-transition')) :
typeof define === 'function' && define.amd ? define(['d3-selection', 'd3-transition'], factory) :
(factory(global.d3,global.d3));
}(this, (function (d3Selection,d3Transition) { 'use strict';
function attrsFunction(selection$$1, map) {
return selection$$1.each(function() {
var x = map.apply(this, arguments), s = d3Selection.select(this);
for (var name in x) s.attr(name, x[name]);
});
}
function attrsObject(selection$$1, map) {
for (var name in map) selection$$1.attr(name, map[name]);
return selection$$1;
}
var selection_attrs = function(map) {
return (typeof map === "function" ? attrsFunction : attrsObject)(this, map);
};
function stylesFunction(selection$$1, map, priority) {
return selection$$1.each(function() {
var x = map.apply(this, arguments), s = d3Selection.select(this);
for (var name in x) s.style(name, x[name], priority);
});
}
function stylesObject(selection$$1, map, priority) {
for (var name in map) selection$$1.style(name, map[name], priority);
return selection$$1;
}
var selection_styles = function(map, priority) {
return (typeof map === "function" ? stylesFunction : stylesObject)(this, map, priority == null ? "" : priority);
};
function propertiesFunction(selection$$1, map) {
return selection$$1.each(function() {
var x = map.apply(this, arguments), s = d3Selection.select(this);
for (var name in x) s.property(name, x[name]);
});
}
function propertiesObject(selection$$1, map) {
for (var name in map) selection$$1.property(name, map[name]);
return selection$$1;
}
var selection_properties = function(map) {
return (typeof map === "function" ? propertiesFunction : propertiesObject)(this, map);
};
function attrsFunction$1(transition$$1, map) {
return transition$$1.each(function() {
var x = map.apply(this, arguments), t = d3Selection.select(this).transition(transition$$1);
for (var name in x) t.attr(name, x[name]);
});
}
function attrsObject$1(transition$$1, map) {
for (var name in map) transition$$1.attr(name, map[name]);
return transition$$1;
}
var transition_attrs = function(map) {
return (typeof map === "function" ? attrsFunction$1 : attrsObject$1)(this, map);
};
function stylesFunction$1(transition$$1, map, priority) {
return transition$$1.each(function() {
var x = map.apply(this, arguments), t = d3Selection.select(this).transition(transition$$1);
for (var name in x) t.style(name, x[name], priority);
});
}
function stylesObject$1(transition$$1, map, priority) {
for (var name in map) transition$$1.style(name, map[name], priority);
return transition$$1;
}
var transition_styles = function(map, priority) {
return (typeof map === "function" ? stylesFunction$1 : stylesObject$1)(this, map, priority == null ? "" : priority);
};
d3Selection.selection.prototype.attrs = selection_attrs;
d3Selection.selection.prototype.styles = selection_styles;
d3Selection.selection.prototype.properties = selection_properties;
d3Transition.transition.prototype.attrs = transition_attrs;
d3Transition.transition.prototype.styles = transition_styles;
})));
// https://github.com/d3/d3-selection-multi Version 1.0.1. Copyright 2017 Mike Bostock.
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(require("d3-selection"),require("d3-transition")):"function"==typeof define&&define.amd?define(["d3-selection","d3-transition"],n):n(t.d3,t.d3)}(this,function(t,n){"use strict";function r(n,r){return n.each(function(){var n=r.apply(this,arguments),e=t.select(this);for(var i in n)e.attr(i,n[i])})}function e(t,n){for(var r in n)t.attr(r,n[r]);return t}function i(n,r,e){return n.each(function(){var n=r.apply(this,arguments),i=t.select(this);for(var o in n)i.style(o,n[o],e)})}function o(t,n,r){for(var e in n)t.style(e,n[e],r);return t}function f(n,r){return n.each(function(){var n=r.apply(this,arguments),e=t.select(this);for(var i in n)e.property(i,n[i])})}function u(t,n){for(var r in n)t.property(r,n[r]);return t}function s(n,r){return n.each(function(){var e=r.apply(this,arguments),i=t.select(this).transition(n);for(var o in e)i.attr(o,e[o])})}function c(t,n){for(var r in n)t.attr(r,n[r]);return t}function a(n,r,e){return n.each(function(){var i=r.apply(this,arguments),o=t.select(this).transition(n);for(var f in i)o.style(f,i[f],e)})}function p(t,n,r){for(var e in n)t.style(e,n[e],r);return t}var l=function(t){return("function"==typeof t?r:e)(this,t)},y=function(t,n){return("function"==typeof t?i:o)(this,t,null==n?"":n)},h=function(t){return("function"==typeof t?f:u)(this,t)},v=function(t){return("function"==typeof t?s:c)(this,t)},d=function(t,n){return("function"==typeof t?a:p)(this,t,null==n?"":n)};t.selection.prototype.attrs=l,t.selection.prototype.styles=y,t.selection.prototype.properties=h,n.transition.prototype.attrs=v,n.transition.prototype.styles=d});
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("d3-array"),require("d3-axis"),require("d3-dispatch"),require("d3-drag"),require("d3-ease"),require("d3-scale"),require("d3-selection")):"function"==typeof define&&define.amd?define(["exports","d3-array","d3-axis","d3-dispatch","d3-drag","d3-ease","d3-scale","d3-selection"],e):e(t.d3=t.d3||{},t.d3,t.d3,t.d3,t.d3,t.d3,t.d3,t.d3)}(this,function(t,e,a,r,n,l,i,s){"use strict";function c(){function t(t){z=t.selection?t.selection():t,M=v[0]instanceof Date?i.scaleTime():i.scaleLinear(),M=M.domain(v).range([0,h]).clamp(!0),D=i.scaleLinear().range(M.range()).domain(M.range()).clamp(!0),p=i.scaleLinear().range(v).domain(v).clamp(!0)(p),q=q||M.tickFormat(),z.selectAll(".axis").data([null]).enter().append("g").attr("transform","translate(0,7)").attr("class","axis");var e=z.selectAll(".slider").data([null]),r=e.enter().append("g").attr("class","slider").attr("cursor","ew-resize").attr("transform","translate(0,0)").call(n.drag().on("start",function(){s.select(this).classed("active",!0);var t=D(s.event.x),a=u(M.invert(t));f(a),A.call("start",e,a),d(a)}).on("drag",function(){var t=D(s.event.x),a=u(M.invert(t));f(a),A.call("drag",e,a),d(a)}).on("end",function(){s.select(this).classed("active",!1);var t=D(s.event.x),a=u(M.invert(t));f(a),A.call("end",e,a),d(a)}));r.append("line").attr("class","track").attr("x1",0).attr("y1",0).attr("y2",0).attr("stroke","#bbb").attr("stroke-width",6).attr("stroke-linecap","round"),r.append("line").attr("class","track-inset").attr("x1",0).attr("y1",0).attr("y2",0).attr("stroke","#eee").attr("stroke-width",4).attr("stroke-linecap","round"),r.append("line").attr("class","track-overlay").attr("x1",0).attr("y1",0).attr("y2",0).attr("stroke","transparent").attr("stroke-width",40).attr("stroke-linecap","round").merge(e.select(".track-overlay"));var l=r.append("g").attr("class","parameter-value").attr("transform","translate("+M(p)+",0)").attr("font-family","sans-serif").attr("text-anchor","middle");l.append("path").attr("d",x).attr("fill","white").attr("stroke","#777"),g&&l.append("text").attr("font-size",30).attr("y",27).attr("dy",".71em").text(q(p)),t.select(".track").attr("x2",M.range()[1]),t.select(".track-inset").attr("x2",M.range()[1]),t.select(".track-overlay").attr("x2",M.range()[1]),t.select(".axis").call(a.axisBottom(M).tickFormat(q).ticks(w).tickValues(y)),z.select(".axis").select(".domain").remove(),t.select(".axis").attr("transform","translate(0,7)"),t.selectAll(".axis text").attr("fill","#aaa").attr("y",20).attr("dy",".71em").attr("text-anchor","middle"),t.selectAll(".axis line").attr("stroke","#aaa"),t.select(".parameter-value").attr("transform","translate("+M(p)+",0)"),c()}function c(){if(g){var t=[];z.selectAll(".axis .tick").each(function(e){t.push(Math.abs(e-p))});var a=e.scan(t);z.selectAll(".axis .tick text").attr("opacity",function(t,e){return e===a?0:1})}}function u(t){if(k){var a=(t-v[0])%k,r=t-a;return 2*a>k&&(r+=k),t instanceof Date?new Date(r):r}if(b){var n=e.scan(b.map(function(e){return Math.abs(t-e)}));return b[n]}return t}function d(e){p!==e&&(p=e,A.call("onchange",t,e),c())}function f(t,e){e=void 0!==e&&e;var a=z.select(".parameter-value");e&&(a=a.transition().ease(l.easeQuadOut).duration(o)),a.attr("transform","translate("+M(t)+",0)"),g&&z.select(".parameter-value text").text(q(t))}var p=0,m=0,v=[0,10],h=100,g=!0,x="M-5.5,-5.5v10l6,5.5l6,-5.5v-10z",k=null,y=null,b=null,q=null,w=null,A=r.dispatch("onchange","start","end","drag"),z=null,M=null,D=null;return t.min=function(e){return arguments.length?(v[0]=e,t):v[0]},t.max=function(e){return arguments.length?(v[1]=e,t):v[1]},t.domain=function(e){return arguments.length?(v=e,t):v},t.width=function(e){return arguments.length?(h=e,t):h},t.tickFormat=function(e){return arguments.length?(q=e,t):q},t.ticks=function(e){return arguments.length?(w=e,t):w},t.value=function(e){if(!arguments.length)return p;var a=D(M(e)),r=u(M.invert(a));return f(r,!0),d(r),t},t.default=function(e){return arguments.length?(m=e,p=e,t):m},t.step=function(e){return arguments.length?(k=e,t):k},t.tickValues=function(e){return arguments.length?(y=e,t):y},t.marks=function(e){return arguments.length?(b=e,t):b},t.handle=function(e){return arguments.length?(x=e,t):x},t.displayValue=function(e){return arguments.length?(g=e,t):g},t.on=function(){var e=A.on.apply(A,arguments);return e===A?t:e},t}var o=200;t.sliderHorizontal=function(){return c()},Object.defineProperty(t,"__esModule",{value:!0})});
/*
Prologue by HTML5 UP
html5up.net | @ajlkn
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/
(function($) {
skel.breakpoints({
wide: '(min-width: 961px) and (max-width: 1880px)',
normal: '(min-width: 961px) and (max-width: 1620px)',
narrow: '(min-width: 961px) and (max-width: 1320px)',
narrower: '(max-width: 960px)',
mobile: '(max-width: 736px)'
});
$(function() {
var $window = $(window),
$body = $('body');
// Disable animations/transitions until the page has loaded.
$body.addClass('is-loading');
$window.on('load', function() {
$body.removeClass('is-loading');
});
// CSS polyfills (IE<9).
if (skel.vars.IEVersion < 9)
$(':last-child').addClass('last-child');
// Fix: Placeholder polyfill.
$('form').placeholder();
// Prioritize "important" elements on mobile.
skel.on('+mobile -mobile', function() {
$.prioritize(
'.important\\28 mobile\\29',
skel.breakpoint('mobile').active
);
});
// Scrolly links.
$('.scrolly').scrolly();
// Nav.
var $nav_a = $('#nav a');
// Scrolly-fy links.
$nav_a
.scrolly()
.on('click', function(e) {
var t = $(this),
href = t.attr('href');
if (href[0] != '#')
return;
e.preventDefault();
// Clear active and lock scrollzer until scrolling has stopped
$nav_a
.removeClass('active')
.addClass('scrollzer-locked');
// Set this link to active
t.addClass('active');
});
// Initialize scrollzer.
var ids = [];
$nav_a.each(function() {
var href = $(this).attr('href');
if (href[0] != '#')
return;
ids.push(href.substring(1));
});
$.scrollzer(ids, { pad: 200, lastHack: true });
// Header (narrower + mobile).
// Toggle.
$(
'<div id="headerToggle">' +
'<a href="#header" class="toggle"></a>' +
'</div>'
)
.appendTo($body);
// Header.
$('#header')
.panel({
delay: 500,
hideOnClick: true,
hideOnSwipe: true,
resetScroll: true,
resetForms: true,
side: 'left',
target: $body,
visibleClass: 'header-visible'
});
// Fix: Remove transitions on WP<10 (poor/buggy performance).
if (skel.vars.os == 'wp' && skel.vars.osVersion < 10)
$('#headerToggle, #header, #main')
.css('transition', 'none');
});
})(jQuery);
d3.sankey = function() {
var sankey = {},
nodeWidth = 24,
nodePadding = 8,
size = [1, 1],
nodes = [],
links = [];
sankey.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return sankey;
};
sankey.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return sankey;
};
sankey.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return sankey;
};
sankey.links = function(_) {
if (!arguments.length) return links;
links = _;
return sankey;
};
sankey.size = function(_) {
if (!arguments.length) return size;
size = _;
return sankey;
};
sankey.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
computeNodeDepths(iterations);
computeLinkDepths();
return sankey;
};
sankey.relayout = function() {
computeLinkDepths();
return sankey;
};
sankey.link = function() {
var curvature = .5;
function link(d) {
var x0 = d.source.x + d.source.dx,
x1 = d.target.x,
xi = d3.interpolateNumber(x0, x1),
x2 = xi(curvature),
x3 = xi(1 - curvature),
y0 = d.source.y + d.sy + d.dy / 2,
y1 = d.target.y + d.ty + d.dy / 2;
return "M" + x0 + "," + y0
+ "C" + x2 + "," + y0
+ " " + x3 + "," + y1
+ " " + x1 + "," + y1;
}
link.curvature = function(_) {
if (!arguments.length) return curvature;
curvature = +_;
return link;
};
return link;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach(function(node) {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach(function(link) {
var source = link.source,
target = link.target;
if (typeof source === "number") source = link.source = nodes[link.source];
if (typeof target === "number") target = link.target = nodes[link.target];
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach(function(node) {
node.value = Math.max(
d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, value)
);
});
}
// Iteratively assign the breadth (x-position) for each node.
// Nodes are assigned the maximum breadth of incoming neighbors plus one;
// nodes with no incoming links are assigned breadth zero, while
// nodes with no outgoing links are assigned the maximum breadth.
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
if (nextNodes.indexOf(link.target) < 0) {
nextNodes.push(link.target);
}
});
});
remainingNodes = nextNodes;
++x;
}
//
moveSinksRight(x);
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
}
function moveSourcesRight() {
nodes.forEach(function(node) {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
}
});
}
function moveSinksRight(x) {
nodes.forEach(function(node) {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
});
}
function scaleNodeBreadths(kx) {
nodes.forEach(function(node) {
node.x *= kx;
});
}
function computeNodeDepths(iterations) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.sortKeys(d3.ascending)
.entries(nodes)
.map(function(d) { return d.values; });
//
initializeNodeDepth();
resolveCollisions();
for (var alpha = 1; iterations > 0; --iterations) {
relaxRightToLeft(alpha *= .99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeDepth() {
var ky = d3.min(nodesByBreadth, function(nodes) {
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
});
nodesByBreadth.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach(function(link) {
link.dy = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
nodesByBreadth.forEach(function(nodes, breadth) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedSource(link) {
return center(link.source) * link.value;
}
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
}
}
function resolveCollisions() {
nodesByBreadth.forEach(function(nodes) {
var node,
dy,
y0 = 0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingDepth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y0 - node.y;
if (dy > 0) node.y += dy;
y0 = node.y + node.dy + nodePadding;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y0 - nodePadding - size[1];
if (dy > 0) {
y0 = node.y -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y + node.dy + nodePadding - y0;
if (dy > 0) node.y -= dy;
y0 = node.y;
}
}
});
}
function ascendingDepth(a, b) {
return a.y - b.y;
}
}
function computeLinkDepths() {
nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach(function(node) {
var sy = 0, ty = 0;
node.sourceLinks.forEach(function(link) {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach(function(link) {
link.ty = ty;
ty += link.dy;
});
});
function ascendingSourceDepth(a, b) {
return a.source.y - b.source.y;
}
function ascendingTargetDepth(a, b) {
return a.target.y - b.target.y;
}
}
function center(node) {
return node.y + node.dy / 2;
}
function value(link) {
return link.value;
}
return sankey;
};
deficit,year
-2.798,1968
0.33,1969
-0.271,1970
-2.058,1971
-1.917,1972
-1.099,1973
-0.413,1974
-3.306,1975
-4.118,1976
-2.645,1977
-2.598,1978
-1.585,1979
-2.64,1980
-2.516,1981
-3.862,1982
-5.868,1983
-4.689,1984
-4.972,1985
-4.877,1986
-3.131,1987
-3.01,1988
-2.74,1989
-3.737,1990
-4.406,1991
-4.512,1992
-3.754,1993
-2.823,1994
-2.162,1995
-1.347,1996
-0.258,1997
0.774,1998
1.321,1999
2.328,2000
1.214,2001
-1.45,2002
-3.332,2003
-3.414,2004
-2.47,2005
-1.814,2006
-1.122,2007
-3.108,2008
-9.8,2009
-8.747,2010
-8.45,2011
-6.782,2012
-4.114,2013
-2.81,2014
-2.438,2015
-3.165,2016
-3.469,2017
// set the dimensions and margins of the graph
var margin = { top: 40, right: 10, bottom: 100, left: 10 },
width = container.offsetWidth - margin.left - margin.right,
height = 600 - margin.top - margin.bottom
var fontScale = d3.scaleLinear().range([14, 22])
// format variables
var formatNumber = d3.format('.1f'), // zero decimal places
format = function(d) {
return formatNumber(d)
},
color = d3.scaleOrdinal(d3.schemeCategory20)
// append the svg object to the body of the page
var svg = d3
.select('#container')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.style('background', '#e8e8e8')
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
//transition time
transition = 1000
//starting year
thisYear = 1968
// Set the sankey diagram properties
var sankey = d3.sankey().nodeWidth(60).nodePadding(20).size([width, height])
var path = sankey.link()
// load the data
d3.csv('us-budget-sankey-years-col.csv', function(error, csv) {
if (error) throw error
// load deficit data
d3.csv('us-budget-sankey-deficit.csv', function(error, deficit) {
if (error) throw error
newData(csv, deficit, thisYear)
drawSankey()
drawDeficit()
drawNotes()
drawSlider()
})
})
function newData(csv, deficit, thisYear) {
thisYearCsv = csv.filter(function(d) {
if (d['year'] == thisYear) {
return d
}
})
thisYearDeficit = deficit.filter(function(d) {
if (d['year'] == thisYear) {
return d
}
})
//console.log(thisYearDeficit)
// create an array to push all sources and targets, before making them unique
arr = []
thisYearCsv.forEach(function(d) {
arr.push(d.source)
arr.push(d.target)
}) //console.log(arr)
// create nodes array
nodes = arr.filter(onlyUnique).map(function(d, i) {
return {
node: i,
name: d
}
})
//console.log(nodes)
// create links array
links = thisYearCsv.map(function(thisYearCsv_row) {
return {
source: getNode('source'),
target: getNode('target'),
value: +thisYearCsv_row.value
}
function getNode(type) {
return nodes.filter(function(node_object) {
return node_object.name == thisYearCsv_row[type]
})[0].node
}
})
//console.log(links)
}
function drawSankey() {
d3.selectAll('.node').remove()
d3.selectAll('.link').remove()
d3.selectAll('.deficitLabel').remove()
sankey.nodes(nodes).links(links).layout(1000)
fontScale.domain(
d3.extent(nodes, function(d) {
return d.value
})
)
// add in the links
link = svg
.append('g')
.selectAll('.link')
.data(links, function(d) {
return d.id
})
.enter()
.append('path')
.attr('class', 'link')
.attr('d', path)
.style('stroke', function(d) {
return color(d.source.name.replace(/ .*/, ''))
})
.style('stroke-width', function(d) {
return Math.max(1, d.dy)
})
// add in the nodes
var node = svg
.append('g')
.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')'
})
// add the rectangles for the nodes
node
.append('rect')
.attr('height', function(d) {
return d.dy < 0 ? 0.1 : d.dy
})
.attr('width', sankey.nodeWidth())
.attr('class', function(d) {
return d.name
})
.attr('value', function(d) {
return d.value
})
.style('fill', 'lightgrey')
.style('opacity', 0.4)
.style('stroke', function(d) {
return d3.rgb(d.color).darker(2)
})
// title for the nodes
node
.append('text')
.attr('x', -6)
.attr('y', function(d) {
return d.dy / 2
})
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.style('font-size', function(d) {
return Math.floor(fontScale(d.value)) + 'px'
})
.text(function(d) {
return d.name
})
.filter(function(d) {
return d.x < width / 2
})
.attr('x', 6 + sankey.nodeWidth())
.attr('text-anchor', 'start')
.attr('class', 'nodeLabel')
// % for the nodes
node
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 30)
.attr('y', function(d) {
return d.dy / 2
})
.style('font-size', 18)
.attr('dy', '.35em')
.filter(function(d) {
return d.value > 1
})
.text(function(d) {
return format(d.value) + '%'
})
.attr('class', 'nodePercent')
}
function drawDeficit() {
//highlight deficit
barHeight = d3.select('.Spending').attr('height')
barVal = d3.select('.Spending').attr('value')
deficitVal = thisYearDeficit[0].deficit
//get deficit bar size with ratio of spending value to bar height
deficitBarRatio = barHeight * deficitVal / barVal
//console.log(deficitBarRatio)
deficitBar = d3
.select('.Spending')
.select(function() {
return this.parentNode
})
.append('rect')
.attr('height', function() {
if (deficitBarRatio < 0) {
return -deficitBarRatio
} else {
return deficitBarRatio
}
})
.attr('width', sankey.nodeWidth())
.attr('y', function(d) {
if (deficitBarRatio < 0) {
return d.dy + deficitBarRatio
} else {
return d.dy - deficitBarRatio
}
})
.style('fill', function() {
if (deficitBarRatio < 0) {
return 'red'
} else {
return 'blue'
}
})
.style('opacity', 0.8)
.attr('class', 'deficit')
function deficitType() {
if (thisYearDeficit[0].deficit < 0) {
return 'Deficit'
} else {
return 'Surplus'
}
}
svg
.append('text')
.attr('text-anchor', 'middle')
.attr('x', width / 2)
.attr('y', height * 0.92)
.style('font-size', 25)
.style('font-weight', 'bold')
.attr('class', 'deficitLabel')
.text(function() {
if (thisYearDeficit[0].deficit < 0) {
return format(-thisYearDeficit[0].deficit) + '% ' + 'Deficit'
} else {
return format(thisYearDeficit[0].deficit) + '% ' + 'Surplus'
}
})
.style('fill', function() {
if (deficitBarRatio < 0) {
return 'red'
} else {
return 'blue'
}
})
}
//animated update is WIP, labels arent repositioning correctly
// function updateSankey() {
// sankey.nodes(nodes)
// .links(links)
// .layout(1000);
// //sankey.relayout(); PURPOSE???
// fontScale.domain(d3.extent(nodes, function(d) { return d.value }));
// // add in the links
// svg.selectAll(".link")
// .data(links)
// .transition()
// .duration(transition)
// .attr("d", path)
// .style("stroke-width", function(d) { return Math.max(1, d.dy); });
// // add in the nodes
// svg.selectAll(".node")
// .data(nodes)
// .transition()
// .duration(transition)
// .attr("transform", function(d) {
// return "translate(" + d.x + "," + d.y + ")"
// });
// // add the rectangles for the nodes
// svg.selectAll(".node rect")
// .data(nodes)
// .transition()
// .duration(transition)
// .attr("height", function(d) {
// return d.dy < 0 ? .1 : d.dy;
// });
// // // title for the nodes
// // svg.selectAll(".nodeLabel")
// // .data(nodes)
// // .transition()
// // .duration(transition)
// // .style("font-size", function(d) {
// // return Math.floor(fontScale(d.value)) + "px";
// // });
// // // % for the nodes
// // svg.selectAll(".nodePercent")
// // .data(nodes)
// // .text(function(d) { return format(d.value) + "%" });
// }
function drawSlider() {
//Slider
var slider = d3
.sliderHorizontal()
.min(1968)
.max(2017)
.step(1)
.width(container.offsetWidth - 75)
.tickFormat(d3.format('.4'))
.on('end', val => {
//use end instead of onchange, is when user releases mouse
thisYear = val
d3.csv('us-budget-sankey-years-col.csv', function(error, csv) {
if (error) throw error
d3.csv('us-budget-sankey-deficit.csv', function(
error,
deficit
) {
if (error) throw error
newData(csv, deficit, thisYear)
drawSankey()
drawDeficit()
})
})
})
var g = d3
.select('div#slider')
.append('svg')
.attr('width', container.offsetWidth)
.attr('height', 100)
.append('g')
.attr('transform', 'translate(30,30)')
g.call(slider)
d3.selectAll('#slider').style('font-size', 20)
}
function drawNotes() {
//PERCENT OF GDP
svg
.append('text')
.attr('x', 0)
.attr('y', -15)
.attr('dy', '0em')
.text('Percent of GDP (May not add up due to rounding)')
.attr('font-size', 25)
.attr('font-weight', 'bold')
.attr('class', 'percent')
//Source and * and ** notes
svg
.append('text')
.attr('x', width * 0.65)
.attr('y', height + 50)
.attr('dy', '0em')
.text(
'* Originally in the spending side of the data as a negative value'
)
.attr('class', 'legend')
.attr('font-size', 16)
svg
.append('text')
.attr('x', width * 0.65)
.attr('y', height + 70)
.attr('dy', '0em')
.text('** Called "Programmatic" in the dataset')
.attr('class', 'legend')
.attr('font-size', 16)
svg
.append('text')
.attr('x', width * 0.65)
.attr('y', height + 90)
.attr('dy', '0em')
.text('Source: OMB')
.attr('class', 'legend')
.attr('font-size', 16)
}
// unique values of an array
function onlyUnique(value, index, self) {
return self.indexOf(value) === index
}
(function($) {
/**
* Generate an indented list of links from a nav. Meant for use with panel().
* @return {jQuery} jQuery object.
*/
$.fn.navList = function() {
var $this = $(this);
$a = $this.find('a'),
b = [];
$a.each(function() {
var $this = $(this),
indent = Math.max(0, $this.parents('li').length - 1),
href = $this.attr('href'),
target = $this.attr('target');
b.push(
'<a ' +
'class="link depth-' + indent + '"' +
( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
'>' +
'<span class="indent-' + indent + '"></span>' +
$this.text() +
'</a>'
);
});
return b.join('');
};
/**
* Panel-ify an element.
* @param {object} userConfig User config.
* @return {jQuery} jQuery object.
*/
$.fn.panel = function(userConfig) {
// No elements?
if (this.length == 0)
return $this;
// Multiple elements?
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).panel(userConfig);
return $this;
}
// Vars.
var $this = $(this),
$body = $('body'),
$window = $(window),
id = $this.attr('id'),
config;
// Config.
config = $.extend({
// Delay.
delay: 0,
// Hide panel on link click.
hideOnClick: false,
// Hide panel on escape keypress.
hideOnEscape: false,
// Hide panel on swipe.
hideOnSwipe: false,
// Reset scroll position on hide.
resetScroll: false,
// Reset forms on hide.
resetForms: false,
// Side of viewport the panel will appear.
side: null,
// Target element for "class".
target: $this,
// Class to toggle.
visibleClass: 'visible'
}, userConfig);
// Expand "target" if it's not a jQuery object already.
if (typeof config.target != 'jQuery')
config.target = $(config.target);
// Panel.
// Methods.
$this._hide = function(event) {
// Already hidden? Bail.
if (!config.target.hasClass(config.visibleClass))
return;
// If an event was provided, cancel it.
if (event) {
event.preventDefault();
event.stopPropagation();
}
// Hide.
config.target.removeClass(config.visibleClass);
// Post-hide stuff.
window.setTimeout(function() {
// Reset scroll position.
if (config.resetScroll)
$this.scrollTop(0);
// Reset forms.
if (config.resetForms)
$this.find('form').each(function() {
this.reset();
});
}, config.delay);
};
// Vendor fixes.
$this
.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
.css('-webkit-overflow-scrolling', 'touch');
// Hide on click.
if (config.hideOnClick) {
$this.find('a')
.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
$this
.on('click', 'a', function(event) {
var $a = $(this),
href = $a.attr('href'),
target = $a.attr('target');
if (!href || href == '#' || href == '' || href == '#' + id)
return;
// Cancel original event.
event.preventDefault();
event.stopPropagation();
// Hide panel.
$this._hide();
// Redirect to href.
window.setTimeout(function() {
if (target == '_blank')
window.open(href);
else
window.location.href = href;
}, config.delay + 10);
});
}
// Event: Touch stuff.
$this.on('touchstart', function(event) {
$this.touchPosX = event.originalEvent.touches[0].pageX;
$this.touchPosY = event.originalEvent.touches[0].pageY;
})
$this.on('touchmove', function(event) {
if ($this.touchPosX === null
|| $this.touchPosY === null)
return;
var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
th = $this.outerHeight(),
ts = ($this.get(0).scrollHeight - $this.scrollTop());
// Hide on swipe?
if (config.hideOnSwipe) {
var result = false,
boundary = 20,
delta = 50;
switch (config.side) {
case 'left':
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
break;
case 'right':
result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
break;
case 'top':
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
break;
case 'bottom':
result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
break;
default:
break;
}
if (result) {
$this.touchPosX = null;
$this.touchPosY = null;
$this._hide();
return false;
}
}
// Prevent vertical scrolling past the top or bottom.
if (($this.scrollTop() < 0 && diffY < 0)
|| (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
event.preventDefault();
event.stopPropagation();
}
});
// Event: Prevent certain events inside the panel from bubbling.
$this.on('click touchend touchstart touchmove', function(event) {
event.stopPropagation();
});
// Event: Hide panel if a child anchor tag pointing to its ID is clicked.
$this.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.removeClass(config.visibleClass);
});
// Body.
// Event: Hide panel on body click/tap.
$body.on('click touchend', function(event) {
$this._hide(event);
});
// Event: Toggle.
$body.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.toggleClass(config.visibleClass);
});
// Window.
// Event: Hide on ESC.
if (config.hideOnEscape)
$window.on('keydown', function(event) {
if (event.keyCode == 27)
$this._hide(event);
});
return $this;
};
/**
* Apply "placeholder" attribute polyfill to one or more forms.
* @return {jQuery} jQuery object.
*/
$.fn.placeholder = function() {
// Browser natively supports placeholders? Bail.
if (typeof (document.createElement('input')).placeholder != 'undefined')
return $(this);
// No elements?
if (this.length == 0)
return $this;
// Multiple elements?
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).placeholder();
return $this;
}
// Vars.
var $this = $(this);
// Text, TextArea.
$this.find('input[type=text],textarea')
.each(function() {
var i = $(this);
if (i.val() == ''
|| i.val() == i.attr('placeholder'))
i
.addClass('polyfill-placeholder')
.val(i.attr('placeholder'));
})
.on('blur', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
return;
if (i.val() == '')
i
.addClass('polyfill-placeholder')
.val(i.attr('placeholder'));
})
.on('focus', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
return;
if (i.val() == i.attr('placeholder'))
i
.removeClass('polyfill-placeholder')
.val('');
});
// Password.
$this.find('input[type=password]')
.each(function() {
var i = $(this);
var x = $(
$('<div>')
.append(i.clone())
.remove()
.html()
.replace(/type="password"/i, 'type="text"')
.replace(/type=password/i, 'type=text')
);
if (i.attr('id') != '')
x.attr('id', i.attr('id') + '-polyfill-field');
if (i.attr('name') != '')
x.attr('name', i.attr('name') + '-polyfill-field');
x.addClass('polyfill-placeholder')
.val(x.attr('placeholder')).insertAfter(i);
if (i.val() == '')
i.hide();
else
x.hide();
i
.on('blur', function(event) {
event.preventDefault();
var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') {
i.hide();
x.show();
}
});
x
.on('focus', function(event) {
event.preventDefault();
var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
x.hide();
i
.show()
.focus();
})
.on('keypress', function(event) {
event.preventDefault();
x.val('');
});
});
// Events.
$this
.on('submit', function() {
$this.find('input[type=text],input[type=password],textarea')
.each(function(event) {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/))
i.attr('name', '');
if (i.val() == i.attr('placeholder')) {
i.removeClass('polyfill-placeholder');
i.val('');
}
});
})
.on('reset', function(event) {
event.preventDefault();
$this.find('select')
.val($('option:first').val());
$this.find('input,textarea')
.each(function() {
var i = $(this),
x;
i.removeClass('polyfill-placeholder');
switch (this.type) {
case 'submit':
case 'reset':
break;
case 'password':
i.val(i.attr('defaultValue'));
x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') {
i.hide();
x.show();
}
else {
i.show();
x.hide();
}
break;
case 'checkbox':
case 'radio':
i.attr('checked', i.attr('defaultValue'));
break;
case 'text':
case 'textarea':
i.val(i.attr('defaultValue'));
if (i.val() == '') {
i.addClass('polyfill-placeholder');
i.val(i.attr('placeholder'));
}
break;
default:
i.val(i.attr('defaultValue'));
break;
}
});
});
return $this;
};
/**
* Moves elements to/from the first positions of their respective parents.
* @param {jQuery} $elements Elements (or selector) to move.
* @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
*/
$.prioritize = function($elements, condition) {
var key = '__prioritize';
// Expand $elements if it's not already a jQuery object.
if (typeof $elements != 'jQuery')
$elements = $($elements);
// Step through elements.
$elements.each(function() {
var $e = $(this), $p,
$parent = $e.parent();
// No parent? Bail.
if ($parent.length == 0)
return;
// Not moved? Move it.
if (!$e.data(key)) {
// Condition is false? Bail.
if (!condition)
return;
// Get placeholder (which will serve as our point of reference for when this element needs to move back).
$p = $e.prev();
// Couldn't find anything? Means this element's already at the top, so bail.
if ($p.length == 0)
return;
// Move element to top of parent.
$e.prependTo($parent);
// Mark element as moved.
$e.data(key, $p);
}
// Moved already?
else {
// Condition is true? Bail.
if (condition)
return;
$p = $e.data(key);
// Move element back to its original location (using our placeholder).
$e.insertAfter($p);
// Unmark element as moved.
$e.removeData(key);
}
});
};
})(jQuery);