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