Spaces:
Sleeping
Sleeping
| import {ascending, min, max, 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, | |
| maxPaddedSpace = 2 / 3; // Defined as a fraction of the total available space | |
| 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 L = max(columns, function(nodes) { | |
| return nodes.length; | |
| }); | |
| var maxNodePadding = maxPaddedSpace * (y1 - y0) / (L - 1); | |
| if(py > maxNodePadding) py = maxNodePadding; | |
| 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; | |
| } | |