block by micahstubbs b8a3f5a6d05760ec688b157151e13fd5

us-budget-sankey

Full Screen

US Federal Budget Balance, 1968-2017

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

index.html

<!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>

d3-selection-multi.js

// 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;

})));

d3-selection-multi.min.js

// 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});

d3-simple-slider.min.js

!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})});

main.js

/*
	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);

sankey.js

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;
};

us-budget-sankey-deficit.csv

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

us-budget-sankey.js

// 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
}

util.js

(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);