Skip to content

Instantly share code, notes, and snippets.

@jasonhodges
Last active June 16, 2017 01:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonhodges/799025f6d1a1dfa2460f6de7d3fa46fa to your computer and use it in GitHub Desktop.
Save jasonhodges/799025f6d1a1dfa2460f6de7d3fa46fa to your computer and use it in GitHub Desktop.
Sankey Diagram
license: mit
import {min} from "d3-collection";
function targetDepth(d) {
return d.target.depth;
}
export function left(node) {
return node.depth;
}
export function right(node, n) {
return n - 1 - node.height;
}
export function justify(node, n) {
return node.sourceLinks.length ? node.depth : n - 1;
}
export function center(node) {
return node.targetLinks.length ? node.depth
: node.sourceLinks.length ? min(node.sourceLinks, targetDepth) - 1
: 0;
}
export default function constant(x) {
return function() {
return x;
};
}
{
"sankey": {
"nodes": [{
"name": "34367",
"node": 0
}, {
"name": "34346",
"node": 1
}, {
"name": "34332",
"node": 2
}, {
"name": "34348",
"node": 3
}, {
"name": "34404",
"node": 4
}, {
"name": "34326",
"node": 5
}, {
"name": "34352",
"node": 6
}, {
"name": "34391",
"node": 7
}, {
"name": "34352",
"node": 8
}, {
"name": "34357",
"node": 9
}, {
"name": "34368",
"node": 10
}, {
"name": "34358",
"node": 11
}, {
"name": "34411",
"node": 12
}, {
"name": "34368",
"node": 13
}, {
"name": "34368",
"node": 14
}, {
"name": "34334",
"node": 15
}, {
"name": "34388",
"node": 16
}, {
"name": "34376",
"node": 17
}, {
"name": "34376",
"node": 18
}, {
"name": "34388",
"node": 19
}, {
"name": "34381",
"node": 20
}, {
"name": "34408",
"node": 21
}, {
"name": "34390",
"node": 22
}, {
"name": "34418",
"node": 23
}, {
"name": "34324",
"node": 24
}, {
"name": "34399",
"node": 25
}, {
"name": "34400",
"node": 26
}, {
"name": "34387",
"node": 27
}, {
"name": "34393",
"node": 28
}, {
"name": "34326",
"node": 29
}, {
"name": "34391",
"node": 30
}, {
"name": "34352",
"node": 31
}, {
"name": "34340",
"node": 32
}, {
"name": "34347",
"node": 33
}, {
"name": "34332",
"node": 34
}, {
"name": "34348",
"node": 35
}, {
"name": "34404",
"node": 36
}],
"links": [{
"source": 0,
"target": 1,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 1,
"target": 2,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 2,
"target": 3,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 3,
"target": 4,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 4,
"target": 5,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 5,
"target": 6,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 6,
"target": 7,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 7,
"target": 8,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 8,
"target": 9,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 9,
"target": 10,
"value": 0.11320754716981132,
"rank": 1
}, {
"source": 0,
"target": 1,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 1,
"target": 2,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 2,
"target": 3,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 3,
"target": 4,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 4,
"target": 5,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 5,
"target": 6,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 6,
"target": 7,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 7,
"target": 8,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 8,
"target": 11,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 11,
"target": 12,
"value": 0.05660377358490566,
"rank": 2
}, {
"source": 0,
"target": 1,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 1,
"target": 2,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 2,
"target": 3,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 3,
"target": 4,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 4,
"target": 5,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 5,
"target": 13,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 13,
"target": 7,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 7,
"target": 14,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 14,
"target": 15,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 15,
"target": 16,
"value": 0.05660377358490566,
"rank": 3
}, {
"source": 0,
"target": 1,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 1,
"target": 2,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 2,
"target": 3,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 3,
"target": 4,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 4,
"target": 5,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 5,
"target": 17,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 17,
"target": 7,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 7,
"target": 18,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 18,
"target": 19,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 19,
"target": 20,
"value": 0.05660377358490566,
"rank": 4
}, {
"source": 0,
"target": 1,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 1,
"target": 2,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 2,
"target": 3,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 3,
"target": 4,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 4,
"target": 21,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 21,
"target": 22,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 22,
"target": 23,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 23,
"target": 24,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 24,
"target": 25,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 25,
"target": 26,
"value": 0.05660377358490566,
"rank": 5
}, {
"source": 0,
"target": 1,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 1,
"target": 2,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 2,
"target": 3,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 3,
"target": 4,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 4,
"target": 5,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 5,
"target": 17,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 17,
"target": 7,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 7,
"target": 18,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 18,
"target": 19,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 19,
"target": 27,
"value": 0.03773584905660377,
"rank": 6
}, {
"source": 0,
"target": 1,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 1,
"target": 2,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 2,
"target": 3,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 3,
"target": 4,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 4,
"target": 5,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 5,
"target": 28,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 28,
"target": 29,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 29,
"target": 8,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 8,
"target": 30,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 30,
"target": 31,
"value": 0.03773584905660377,
"rank": 7
}, {
"source": 0,
"target": 1,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 1,
"target": 32,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 32,
"target": 33,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 33,
"target": 34,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 34,
"target": 35,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 35,
"target": 36,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 36,
"target": 29,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 29,
"target": 8,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 8,
"target": 30,
"value": 0.03773584905660377,
"rank": 8
}, {
"source": 30,
"target": 31,
"value": 0.03773584905660377,
"rank": 8
}]
},
"params": [0.25, 0.25, 0, 0, 0]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="sankey.js"></script>
<script src="align.js"></script>
<script src="sankeyLinkHorizontal.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<div style="overflow-x: auto;">
<svg width="1860" height="700"></svg>
</div>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) },
color = d3.scaleOrdinal(d3.schemeCategory10);
var sankey = d3.sankey()
.nodeId(function id(d) {console.log(d.node, d.name); return d.node})
.nodeWidth(35)
.nodePadding(60)
.nodeAlign(d3.sankeyJustify)
.iterations(20)
.extent([[1,1], [width - 1, height - 6]]);
var link = svg.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll("path");
var node = svg.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
d3.json("data.json", function(error, data) {
if (error) throw error;
sankey(data.sankey);
link = link
.data(data.sankey.links)
.enter().append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke-width", function(d) {
return Math.max(1, d.width / 2);
});
link.append("title")
.text(function(d) {
return d.source.name + " → " + d.target.name + "\n" + format(d.value);
});
node = node
.data(data.sankey.nodes)
.enter().append("g");
node.append("rect")
.attr("x", function(d) { return d.x0; })
.attr("y", function(d) { return d.y0; })
.attr("height", function(d) { return d.y1 - d.y0; })
.attr("width", function(d) { return d.x1 - d.x0; })
.attr("fill", function(d) { return color(d.name.replace(/ .*/, "")); })
.attr("stroke", "#000");
node.append("text")
.attr("x", function(d) { return d.x0 - 6; })
.attr("y", function(d) { return (d.y1 + d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < width / 2; })
.attr("x", function(d) { return d.x1 + 6; })
.attr("text-anchor", "start");
node.append("title")
.text(function(d) { return d.name + "\n" + format(d.value); });
});
</script>
</body>
export {default as sankey} from "sankey.js";
export {center as sankeyCenter, left as sankeyLeft, right as sankeyRight, justify as sankeyJustify} from "align.js";
export {default as sankeyLinkHorizontal} from "sankeyLinkHorizontal.js";
import {ascending, min, sum} from "d3-array";
import {map, nest} from "d3-collection";
import {justify} from "./align";
import constant from "./constant";
function ascendingSourceBreadth(a, b) {
return ascendingBreadth(a.source, b.source) || a.index - b.index;
}
function ascendingTargetBreadth(a, b) {
return ascendingBreadth(a.target, b.target) || a.index - b.index;
}
function ascendingBreadth(a, b) {
return a.y0 - b.y0;
}
function value(d) {
return d.value;
}
function nodeCenter(node) {
return (node.y0 + node.y1) / 2;
}
function weightedSource(link) {
return nodeCenter(link.source) * link.value;
}
function weightedTarget(link) {
return nodeCenter(link.target) * link.value;
}
function defaultId(d) {
return d.index;
}
function defaultNodes(graph) {
return graph.nodes;
}
function defaultLinks(graph) {
return graph.links;
}
function find(nodeById, id) {
var node = nodeById.get(id);
if (!node) throw new Error("missing: " + id);
return node;
}
export default function() {
var x0 = 0, y0 = 0, x1 = 1, y1 = 1, // extent
dx = 24, // nodeWidth
py = 8, // nodePadding
id = defaultId,
align = justify,
nodes = defaultNodes,
links = defaultLinks,
iterations = 32;
function sankey() {
var graph = {nodes: nodes.apply(null, arguments), links: links.apply(null, arguments)};
computeNodeLinks(graph);
computeNodeValues(graph);
computeNodeDepths(graph);
computeNodeBreadths(graph, iterations);
computeLinkBreadths(graph);
return graph;
}
sankey.update = function(graph) {
computeLinkBreadths(graph);
return graph;
};
sankey.nodeId = function(_) {
return arguments.length ? (id = typeof _ === "function" ? _ : constant(_), sankey) : id;
};
sankey.nodeAlign = function(_) {
return arguments.length ? (align = typeof _ === "function" ? _ : constant(_), sankey) : align;
};
sankey.nodeWidth = function(_) {
return arguments.length ? (dx = +_, sankey) : dx;
};
sankey.nodePadding = function(_) {
return arguments.length ? (py = +_, sankey) : py;
};
sankey.nodes = function(_) {
return arguments.length ? (nodes = typeof _ === "function" ? _ : constant(_), sankey) : nodes;
};
sankey.links = function(_) {
return arguments.length ? (links = typeof _ === "function" ? _ : constant(_), sankey) : links;
};
sankey.size = function(_) {
return arguments.length ? (x0 = y0 = 0, x1 = +_[0], y1 = +_[1], sankey) : [x1 - x0, y1 - y0];
};
sankey.extent = function(_) {
return arguments.length ? (x0 = +_[0][0], x1 = +_[1][0], y0 = +_[0][1], y1 = +_[1][1], sankey) : [[x0, y0], [x1, y1]];
};
sankey.iterations = function(_) {
return arguments.length ? (iterations = +_, sankey) : iterations;
};
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks(graph) {
graph.nodes.forEach(function(node, i) {
node.index = i;
node.sourceLinks = [];
node.targetLinks = [];
});
var nodeById = map(graph.nodes, id);
graph.links.forEach(function(link, i) {
link.index = i;
var source = link.source, target = link.target;
if (typeof source !== "object") source = link.source = find(nodeById, source);
if (typeof target !== "object") target = link.target = find(nodeById, target);
source.sourceLinks.push(link);
target.targetLinks.push(link);
});
}
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues(graph) {
graph.nodes.forEach(function(node) {
node.value = Math.max(
sum(node.sourceLinks, value),
sum(node.targetLinks, value)
);
});
}
// Iteratively assign the depth (x-position) for each node.
// Nodes are assigned the maximum depth of incoming neighbors plus one;
// nodes with no incoming links are assigned depth zero, while
// nodes with no outgoing links are assigned the maximum depth.
function computeNodeDepths(graph) {
var nodes, next, x;
for (nodes = graph.nodes, next = [], x = 0; nodes.length; ++x, nodes = next, next = []) {
nodes.forEach(function(node) {
node.depth = x;
node.sourceLinks.forEach(function(link) {
if (next.indexOf(link.target) < 0) {
next.push(link.target);
}
});
});
}
for (nodes = graph.nodes, next = [], x = 0; nodes.length; ++x, nodes = next, next = []) {
nodes.forEach(function(node) {
node.height = x;
node.targetLinks.forEach(function(link) {
if (next.indexOf(link.source) < 0) {
next.push(link.source);
}
});
});
}
var kx = (x1 - x0 - dx) / (x - 1);
graph.nodes.forEach(function(node) {
node.x1 = (node.x0 = x0 + Math.max(0, Math.min(x - 1, Math.floor(align.call(null, node, x)))) * kx) + dx;
});
}
function computeNodeBreadths(graph) {
var columns = nest()
.key(function(d) { return d.x0; })
.sortKeys(ascending)
.entries(graph.nodes)
.map(function(d) { return d.values; });
//
initializeNodeBreadth();
resolveCollisions();
for (var alpha = 1, n = iterations; n > 0; --n) {
relaxRightToLeft(alpha *= 0.99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeBreadth() {
var ky = min(columns, function(nodes) {
return (y1 - y0 - (nodes.length - 1) * py) / sum(nodes, value);
});
columns.forEach(function(nodes) {
nodes.forEach(function(node, i) {
node.y1 = (node.y0 = i) + node.value * ky;
});
});
graph.links.forEach(function(link) {
link.width = link.value * ky;
});
}
function relaxLeftToRight(alpha) {
columns.forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.targetLinks.length) {
var dy = (sum(node.targetLinks, weightedSource) / sum(node.targetLinks, value) - nodeCenter(node)) * alpha;
node.y0 += dy, node.y1 += dy;
}
});
});
}
function relaxRightToLeft(alpha) {
columns.slice().reverse().forEach(function(nodes) {
nodes.forEach(function(node) {
if (node.sourceLinks.length) {
var dy = (sum(node.sourceLinks, weightedTarget) / sum(node.sourceLinks, value) - nodeCenter(node)) * alpha;
node.y0 += dy, node.y1 += dy;
}
});
});
}
function resolveCollisions() {
columns.forEach(function(nodes) {
var node,
dy,
y = y0,
n = nodes.length,
i;
// Push any overlapping nodes down.
nodes.sort(ascendingBreadth);
for (i = 0; i < n; ++i) {
node = nodes[i];
dy = y - node.y0;
if (dy > 0) node.y0 += dy, node.y1 += dy;
y = node.y1 + py;
}
// If the bottommost node goes outside the bounds, push it back up.
dy = y - py - y1;
if (dy > 0) {
y = (node.y0 -= dy), node.y1 -= dy;
// Push any overlapping nodes back up.
for (i = n - 2; i >= 0; --i) {
node = nodes[i];
dy = node.y1 + py - y;
if (dy > 0) node.y0 -= dy, node.y1 -= dy;
y = node.y0;
}
}
});
}
}
function computeLinkBreadths(graph) {
graph.nodes.forEach(function(node) {
node.sourceLinks.sort(ascendingTargetBreadth);
node.targetLinks.sort(ascendingSourceBreadth);
});
graph.nodes.forEach(function(node) {
var y0 = node.y0, y1 = y0;
node.sourceLinks.forEach(function(link) {
link.y0 = y0 + link.width / 2, y0 += link.width;
});
node.targetLinks.forEach(function(link) {
link.y1 = y1 + link.width / 2, y1 += link.width;
});
});
}
return sankey;
}
import {linkHorizontal} from "d3-shape";
function horizontalSource(d) {
return [d.source.x1, d.y0];
}
function horizontalTarget(d) {
return [d.target.x0, d.y1];
}
export default function() {
return linkHorizontal()
.source(horizontalSource)
.target(horizontalTarget);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment