d3-sankey-diagram
Version:
Sankey diagram d3 plugin
2,749 lines • 72.7 kB
JavaScript
'use strict';
var d3Array = require('d3-array');
var pkg = require('@dagrejs/graphlib');
var d3Collection = require('d3-collection');
var d3Selection = require('d3-selection');
var d3Transition = require('d3-transition');
var d3Dispatch = require('d3-dispatch');
var d3Format = require('d3-format');
var d3Interpolate = require('d3-interpolate');
/**
* Create a new graph where nodes in the same rank set are merged into one node.
*
* Depends on the "backwards" attribute of the nodes in G, and the "delta"
* atribute of the edges.
*
*/
function groupedGraph (G, rankSets = []) {
// Not multigraph because this is only used for calculating ranks
const GG = new pkg.Graph({ directed: true });
if (G.nodes().length === 0) return GG
// Make sure there is a minimum-rank set
rankSets = ensureSmin(G, rankSets);
// Construct map of node ids to the set they are in, if any
const nodeSets = d3Collection.map();
let set;
let id;
let i;
let j;
for (i = 0; i < rankSets.length; ++i) {
set = rankSets[i];
if (!set.nodes || set.nodes.length === 0) continue
id = '' + i;
for (j = 0; j < set.nodes.length; ++j) {
nodeSets.set(set.nodes[j], id);
}
GG.setNode(id, { type: set.type, nodes: set.nodes });
}
// use i to keep counting new ids
G.nodes().forEach(u => {
if (!nodeSets.has(u)) {
id = '' + (i++);
set = { type: 'same', nodes: [u] };
nodeSets.set(u, id);
GG.setNode(id, set);
}
});
// Add edges between nodes/groups
G.edges().forEach(e => {
const sourceSet = nodeSets.get(e.v);
const targetSet = nodeSets.get(e.w);
// Minimum edge length depends on direction of nodes:
// -> to -> : 1
// -> to <- : 0
// <- to -> : 0 (in opposite direction??)
// <- to <- : 1 in opposite direction
const edge = GG.edge(sourceSet, targetSet) || { delta: 0 };
if (sourceSet === targetSet) {
edge.delta = 0;
GG.setEdge(sourceSet, targetSet, edge);
} else if (G.node(e.v).backwards) {
edge.delta = Math.max(edge.delta, G.node(e.w).backwards ? 1 : 0);
GG.setEdge(targetSet, sourceSet, edge);
} else {
edge.delta = Math.max(edge.delta, G.node(e.w).backwards ? 0 : 1);
GG.setEdge(sourceSet, targetSet, edge);
}
});
return GG
}
// export function linkDelta (nodeBackwards, link) {
// if (nodeBackwards(link.source)) {
// return nodeBackwards(link.target) ? 1 : 0
// } else {
// return nodeBackwards(link.target) ? 0 : 1
// }
// }
function ensureSmin (G, rankSets) {
for (let i = 0; i < rankSets.length; ++i) {
if (rankSets[i].type === 'min') {
return rankSets // ok
}
}
// find the first sourceSet node, or else use the first node
const sources = G.sources();
const n0 = sources.length ? sources[0] : G.nodes()[0];
return [{ type: 'min', nodes: [n0] }].concat(rankSets)
}
/**
* Reverse edges in G to make it acyclic
*/
function makeAcyclic (G, v0) {
const tree = findSpanningTree(G, v0);
G.edges().forEach(e => {
const rel = nodeRelationship(tree, e.v, e.w);
if (rel < 0) {
const label = G.edge(e) || {};
label.reversed = true;
G.removeEdge(e);
G.setEdge(e.w, e.v, label);
}
});
return G
}
// find spanning tree, starting from the given node.
// return new graph where nodes have depth and thread
function findSpanningTree (G, v0) {
const visited = d3Collection.set();
const tree = new pkg.Graph({ directed: true });
const thread = [];
if (!G.hasNode(v0)) throw Error('node not in graph')
doDfs(G, v0, visited, tree, thread);
G.nodes().forEach(u => {
if (!visited.has(u)) {
doDfs(G, u, visited, tree, thread);
}
});
thread.forEach((u, i) => {
tree.node(u).thread = (i + 1 < thread.length) ? thread[i + 1] : thread[0];
});
return tree
}
/**
* Returns 1 if w is a descendent of v, -1 if v is a descendent of w, and 0 if
* they are unrelated.
*/
function nodeRelationship (tree, v, w) {
const V = tree.node(v);
const W = tree.node(w);
if (V.depth < W.depth) {
let u = V.thread; // next node
while (tree.node(u).depth > V.depth) {
if (u === w) return 1
u = tree.node(u).thread;
}
} else if (W.depth < V.depth) {
let u = W.thread; // next node
while (tree.node(u).depth > W.depth) {
if (u === v) return -1
u = tree.node(u).thread;
}
}
return 0
}
function doDfs (G, v, visited, tree, thread, depth = 0) {
if (!visited.has(v)) {
visited.add(v);
thread.push(v);
tree.setNode(v, { depth });
// It doesn't seem to cause a problem with letters as node ids, but numbers
// are sorted when using G.successors(). So use G.outEdges() instead.
const next = G.outEdges(v).map(e => e.w);
next.forEach((w, i) => {
if (!visited.has(w)) {
tree.setEdge(v, w, { delta: 1 });
}
doDfs(G, w, visited, tree, thread, depth + 1);
});
}
}
/**
* Take an acyclic graph and assign initial ranks to the nodes
*/
function assignInitialRanks (G) {
// Place nodes on queue when they have no unmarked in-edges. Initially, this
// means sources.
const queue = G.sources();
const seen = d3Collection.set();
const marked = d3Collection.set();
// Mark any loops, since they don't affect rank assignment
G.edges().forEach(e => {
if (e.v === e.w) marked.add(edgeIdString(e));
});
G.nodes().forEach(v => {
G.node(v).rank = 0;
});
while (queue.length > 0) {
const v = queue.shift();
seen.add(v);
let V = G.node(v);
if (!V) G.setNode(v, (V = {}));
// Set rank to minimum of incoming edges
V.rank = 0;
G.inEdges(v).forEach(e => {
const delta = G.edge(e).delta === undefined ? 1 : G.edge(e).delta;
V.rank = Math.max(V.rank, G.node(e.v).rank + delta);
});
// Mark outgoing edges
G.outEdges(v).forEach(e => {
marked.add(edgeIdString(e));
});
// Add nodes to queue when they have no unmarked in-edges.
G.nodes().forEach(n => {
if (queue.indexOf(n) < 0 && !seen.has(n) &&
!G.inEdges(n).some(e => !marked.has(edgeIdString(e)))) {
queue.push(n);
}
});
}
}
function edgeIdString (e) {
return e.v + '\x01' + e.w + '\x01' + e.name
}
/**
* Assign ranks to the nodes in G, according to rankSets.
*/
function assignRanks (G, rankSets) {
// Group nodes together, and add additional edges from Smin to sources
const GG = groupedGraph(G, rankSets);
if (GG.nodeCount() === 0) return
// Add additional edges from Smin to sources
addTemporaryEdges(GG);
// Make the graph acyclic
makeAcyclic(GG, '0');
// Assign the initial ranks
assignInitialRanks(GG);
// XXX improve initial ranking...
moveSourcesRight(GG);
// Apply calculated ranks to original graph
// const ranks = []
GG.nodes().forEach(u => {
const groupedNode = GG.node(u);
// while (node.rank >= ranks.length) ranks.push([])
groupedNode.nodes.forEach(v => {
G.node(v).rank = groupedNode.rank;
});
});
// return ranks
}
// export function nodeBackwards (link) {
// if (link.source.direction === 'l') {
// return link.target.direction === 'l' ? 1 : 0
// } else {
// return link.target.direction === 'l' ? 0 : 1
// }
// }
function addTemporaryEdges (GG) {
// Add temporary edges between Smin and sources
GG.sources().forEach(u => {
if (u !== '0') {
GG.setEdge('0', u, { temp: true, delta: 0 });
}
});
// XXX Should also add edges from sinks to Smax
// G.nodes().forEach(u => {
// if (!nodeSets.has(u)) {
// GG.
// }
// });
}
function moveSourcesRight (GG) {
GG.edges().forEach(e => {
const edge = GG.edge(e);
if (edge.temp) moveRight(e.w);
});
function moveRight (v) {
const V = GG.node(v);
const rank = d3Array.min(GG.outEdges(v), e => GG.node(e.w).rank - GG.edge(e).delta);
if (rank !== undefined) V.rank = rank;
}
}
const { alg } = pkg;
function initialOrdering (G, ranks) {
const order = [];
if (ranks.length === 0) return order
// Start with sources & nodes in rank 0
const start = G.sources();
const nodeRanks = d3Collection.map();
ranks.forEach((nodes, i) => {
order.push([]);
nodes.forEach(u => {
if (i === 0 && start.indexOf(u) < 0) start.push(u);
nodeRanks.set(u, i);
});
});
alg.preorder(G, start).forEach(u => {
order[nodeRanks.get(u)].push(u);
});
return order
}
/** @module node-ordering/count-crossings */
/**
* Count the total number of crossings between 2 layers.
*
* This is the sum of the countBetweenCrossings and countLoopCrossings.
*
* @param {Graph} G - The graph.
* @param {Array} orderA - List of node ids on left side.
* @param {Array} orderB - List of node ids on right side.
*/
function countCrossings (G, orderA, orderB) {
return (
countBetweenCrossings(G, orderA, orderB) // +
// countLoopCrossings(G, orderA, orderB)
)
}
/**
* Count the number of crossings of edges passing between 2 layers.
*
* Algorithm from
* http://jgaa.info/accepted/2004/BarthMutzelJuenger2004.8.2.pdf
*
* @param {Graph} G - The graph.
* @param {Array} orderA - List of node ids on left side.
* @param {Array} orderB - List of node ids on right side.
*/
function countBetweenCrossings (G, orderA, orderB) {
let north;
let south;
if (orderA.length > orderB.length) {
north = orderA;
south = orderB;
} else {
north = orderB;
south = orderA;
}
const q = south.length;
// lexicographically sorted edges from north to south
const southSeq = [];
north.forEach(u => {
south.forEach((v, j) => {
if (G.hasEdge(u, v) || G.hasEdge(v, u)) southSeq.push(j);
});
});
// build accumulator tree
let firstIndex = 1;
while (firstIndex < q) firstIndex *= 2;
const treeSize = 2 * firstIndex - 1; // number of tree nodes
firstIndex -= 1; // index of leftmost leaf
const tree = new Array(treeSize);
for (let i = 0; i < treeSize; i++) tree[i] = 0;
// count the crossings
let count = 0;
southSeq.forEach(k => {
let index = k + firstIndex;
tree[index]++;
while (index > 0) {
if (index % 2) count += tree[index + 1];
index = Math.floor((index - 1) / 2);
tree[index]++;
}
});
return count
}
function swapNodes (G, order) {
let improved = true;
while (improved) {
improved = false;
for (let i = 0; i < order.length; ++i) {
for (let j = 0; j < order[i].length - 1; ++j) {
const count0 = allCrossings$1(G, order, i);
transpose(order[i], j, j + 1);
const count1 = allCrossings$1(G, order, i);
if (count1 < count0) {
improved = true;
} else {
transpose(order[i], j, j + 1); // put back
}
}
}
}
}
function allCrossings$1 (G, order, i) {
let count = 0;
if (i > 0) {
count += countCrossings(G, order[i - 1], order[i]);
}
if (i + 1 < order.length) {
count += countCrossings(G, order[i], order[i + 1]);
}
return count
}
function transpose (list, i, j) {
const tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
function medianValue (positions) {
const m = Math.floor(positions.length / 2);
if (positions.length === 0) {
return -1
} else if (positions.length % 2 === 1) {
return positions[m]
} else if (positions.length === 2) {
return (positions[0] + positions[1]) / 2
} else {
const left = positions[m - 1] - positions[0];
const right = positions[positions.length - 1] - positions[m];
return (positions[m - 1] * right + positions[m] * left) / (left + right)
}
}
function neighbourPositions (G, order, i, j, u, includeLoops = false) {
// current rank i
// neighbour rank j
const thisRank = order[i];
const otherRank = order[j];
const positions = [];
// neighbouring positions on other rank
otherRank.forEach((n, i) => {
if (G.nodeEdges(n, u).length > 0) {
positions.push(i);
}
});
if (positions.length === 0 && includeLoops) {
// if no neighbours in other rank, look for loops to this rank
// XXX only on one side?
thisRank.forEach((n, i) => {
if (G.nodeEdges(n, u).length > 0) {
positions.push(i + 0.5);
}
});
}
positions.sort((a, b) => a - b);
return positions
}
/**
* Sort arr according to order. -1 in order means stay in same position.
*/
function sortByPositions (arr, order) {
const origOrder = d3Collection.map(arr.map((d, i) => [d, i]), d => d[0]);
// console.log('sorting', arr, order, origOrder)
for (let i = 1; i < arr.length; ++i) {
// console.group('start', i, arr[i])
for (let k = i; k > 0; --k) {
let j = k - 1;
let a = order.get(arr[j]);
let b = order.get(arr[k]);
// count back over any fixed positions (-1)
while ((a = order.get(arr[j])) === -1 && j > 0) j--;
// console.log(j, k, arr[j], arr[k], a, b)
if (b === -1 || a === -1) {
// console.log('found -1', a, b, 'skipping', j, k)
break
}
if (a === b) {
a = origOrder.get(arr[j]);
b = origOrder.get(arr[k]);
// console.log('a == b, switching to orig order', a, b)
}
if (b >= a) {
// console.log('k > k -1, stopping')
break
}
// console.log('swapping', arr[k], arr[j])
// swap arr[k], arr[j]
[arr[k], arr[j]] = [arr[j], arr[k]];
// console.log(arr)
}
// console.groupEnd()
}
// console.log('-->', arr)
}
function sortNodes$1 (G, order, sweepDirection = 1, includeLoops = false) {
if (sweepDirection > 0) {
for (let r = 1; r < order.length; ++r) {
const medians = d3Collection.map();
order[r].forEach(u => {
const neighbour = medianValue(neighbourPositions(G, order, r, r - 1, u, includeLoops));
medians.set(u, neighbour);
});
sortByPositions(order[r], medians);
}
} else {
for (let r = order.length - 2; r >= 0; --r) {
const medians = d3Collection.map();
order[r].forEach(u => {
const neighbour = medianValue(neighbourPositions(G, order, r, r + 1, u, includeLoops));
medians.set(u, neighbour);
});
sortByPositions(order[r], medians);
}
}
}
/** @module node-ordering */
/**
* Sorts the nodes in G, setting the `depth` attribute on each.
*
* @param {Graph} G - The graph. Nodes must have a `rank` attribute.
*
*/
function sortNodes (G, maxIterations = 25) {
const ranks = getRanks(G);
const order = initialOrdering(G, ranks);
let best = order;
let i = 0;
while (i++ < maxIterations) {
sortNodes$1(G, order, (i % 2 === 0));
swapNodes(G, order);
if (allCrossings(G, order) < allCrossings(G, best)) {
// console.log('improved', allCrossings(G, order), order);
best = copy(order);
}
}
// Assign depth to nodes
// const depths = map()
best.forEach(nodes => {
nodes.forEach((u, i) => {
// depths.set(u, i)
G.node(u).depth = i;
});
});
}
function getRanks (G) {
const ranks = [];
G.nodes().forEach(u => {
const r = G.node(u).rank || 0;
while (r >= ranks.length) ranks.push([]);
ranks[r].push(u);
});
return ranks
}
function allCrossings (G, order) {
let count = 0;
for (let i = 0; i < order.length - 1; ++i) {
count += countCrossings(G, order[i], order[i + 1]);
}
return count
}
function copy (order) {
const result = [];
order.forEach(rank => {
result.push(rank.map(d => d));
});
return result
}
function addDummyNodes (G) {
// Add edges & dummy nodes
if (typeof G.graph() !== 'object') G.setGraph({});
G.graph().dummyChains = [];
G.edges().forEach(e => normaliseEdge(G, e));
}
// based on https://github.com/cpettitt/dagre/blob/master/lib/normalize.js
function normaliseEdge (G, e) {
const edge = G.edge(e);
const dummies = dummyNodes(G.node(e.v), G.node(e.w));
if (dummies.length === 0) return
G.removeEdge(e);
let v = e.v;
dummies.forEach((dummy, i) => {
const id = `__${e.v}_${e.w}_${i}`;
if (!G.hasNode(id)) {
dummy.dummy = 'edge';
G.setNode(id, dummy);
if (i === 0) {
G.graph().dummyChains.push(id);
}
}
addDummyEdge(v, (v = id));
});
addDummyEdge(v, e.w);
function addDummyEdge (v, w) {
const label = { points: [], value: edge.value, origEdge: e, origLabel: edge };
G.setEdge(v, w, label, e.name);
}
}
function removeDummyNodes (G) {
const chains = G.graph().dummyChains || [];
chains.forEach(v => {
let node = G.node(v);
let dummyEdges = G.inEdges(v).map(e => G.edge(e));
// Set dy and starting point of edge and add back to graph
dummyEdges.forEach(dummyEdge => {
dummyEdge.origLabel.dy = dummyEdge.dy;
dummyEdge.origLabel.x0 = dummyEdge.x0;
dummyEdge.origLabel.y0 = dummyEdge.y0;
dummyEdge.origLabel.r0 = dummyEdge.r0;
dummyEdge.origLabel.d0 = dummyEdge.d0;
G.setEdge(dummyEdge.origEdge, dummyEdge.origLabel);
});
let r1s = dummyEdges.map(dummyEdge => dummyEdge.r1);
// Walk through chain
let w;
while (node.dummy) {
dummyEdges = G.outEdges(v).map(e => G.edge(e));
dummyEdges.forEach((dummyEdge, i) => {
dummyEdge.origLabel.points.push({
x: (node.x0 + node.x1) / 2,
y: dummyEdge.y0,
d: dummyEdge.d0,
ro: dummyEdge.r0,
ri: r1s[i] // from last edge
});
});
r1s = dummyEdges.map(dummyEdge => dummyEdge.r1);
// move on
w = G.successors(v)[0];
G.removeNode(v);
node = G.node(v = w);
}
// Set ending point of edge
dummyEdges.forEach(dummyEdge => {
dummyEdge.origLabel.x1 = dummyEdge.x1;
dummyEdge.origLabel.y1 = dummyEdge.y1;
dummyEdge.origLabel.r1 = dummyEdge.r1;
dummyEdge.origLabel.d1 = dummyEdge.d1;
});
});
}
function dummyNodes (source, target) {
const dummyNodes = [];
let r = source.rank;
if (r + 1 <= target.rank) {
// add more to get forwards
if (source.backwards) {
dummyNodes.push({ rank: r, backwards: false }); // turn around
}
while (++r < target.rank) {
dummyNodes.push({ rank: r, backwards: false });
}
if (target.backwards) {
dummyNodes.push({ rank: r, backwards: false }); // turn around
}
} else if (r > target.rank) {
// add more to get backwards
if (!source.backwards) {
dummyNodes.push({ rank: r, backwards: true }); // turn around
}
while (r-- > target.rank + 1) {
dummyNodes.push({ rank: r, backwards: true });
}
if (!target.backwards) {
dummyNodes.push({ rank: r, backwards: true }); // turn around
}
}
return dummyNodes
}
function nestGraph (nodes) {
const maxRank = d3Array.max(nodes, d => d.rank || 0) || 0;
const maxBand = d3Array.max(nodes, d => d.band || 0) || 0;
// const nodes = graph.nodes().concat(graph.dummyNodes())
const nested = d3Collection.nest()
.key(d => d.rank || 0)
.key(d => d.band || 0)
.sortValues((a, b) => a.depth - b.depth)
.map(nodes);
const result = new Array(maxRank + 1);
let rank;
for (let i = 0; i <= maxRank; ++i) {
result[i] = new Array(maxBand + 1);
rank = nested.get(i);
if (rank) {
for (let j = 0; j <= maxBand; ++j) {
result[i][j] = rank.get(j) || [];
}
} else {
for (let j = 0; j <= maxBand; ++j) {
result[i][j] = [];
}
}
}
result.bandValues = bandValues(result);
return result
}
function bandValues (nested) {
if (nested.length === 0 || nested[0].length === 0) return []
const Nb = nested[0].length;
const values = new Array(Nb);
for (let i = 0; i < Nb; i++) values[i] = 0;
nested.forEach(rank => {
rank.forEach((band, j) => {
const total = d3Array.sum(band, d => d.value);
values[j] = Math.max(values[j], total);
});
});
return values
}
// export function minEdgeDx (w, y0, y1) {
// console.log('mindx', w, y0, y1)
// const dy = y1 - y0
// const ay = Math.abs(dy) - w // final sign doesn't matter
// const dx2 = w * w - ay * ay
// const dx = dx2 >= 0 ? Math.sqrt(dx2) : w
// return dx
// }
function positionHorizontally (G, width, nodeWidth) {
// const minWidths = new Array(maxRank).fill(0)
// G.edges().forEach(e => {
// const r0 = G.node(e.v).rank || 0
// minWidths[r0] = Math.max(minWidths[r0], minEdgeDx(G.edge(e).dy, G.node(e.v).y, G.node(e.w).y))
// })
// for (let i = 0; i < nested.length - 1; ++i) {
// minWidths[i] = 0
// nested[i].forEach(band => {
// band.forEach(d => {
// // edges for dummy nodes, outgoing for real nodes
// (d.outgoing || d.edges).forEach(e => {
// minWidths[i] = Math.max(minWidths[i], minEdgeDx(e))
// })
// })
// })
// }
// const totalMinWidth = sum(minWidths)
// let dx
// if (totalMinWidth > width) {
// // allocate fairly
// dx = minWidths.map(w => width * w / totalMinWidth)
// } else {
// const spare = (width - totalMinWidth) / (nested.length - 1)
// dx = minWidths.map(w => w + spare)
// }
const maxRank = d3Array.max(G.nodes(), u => G.node(u).rank || 0) || 0;
const dx = (width - nodeWidth) / maxRank;
G.nodes().forEach(u => {
const node = G.node(u);
node.x0 = dx * (node.rank || 0);
node.x1 = node.x0 + nodeWidth;
});
}
function defaultSeparation (a, b) {
return 1
}
function positionNodesVertically$1 () {
let separation = defaultSeparation;
function layout (nested, totalHeight, whitespace) {
nested.forEach(layer => {
let y = 0;
layer.forEach((band, j) => {
// Height of this band, based on fraction of value
const bandHeight = nested.bandValues[j] / d3Array.sum(nested.bandValues) * totalHeight;
const margin = whitespace * bandHeight / 5;
const height = bandHeight - 2 * margin;
const total = d3Array.sum(band, d => d.dy);
const gaps = band.map((d, i) => {
if (!d.value) return 0
return band[i + 1] ? separation(band[i], band[i + 1], layout) : 0
});
const space = Math.max(0, height - total);
const kg = d3Array.sum(gaps) ? space / d3Array.sum(gaps) : 0;
let yy = y + margin;
if (band.length === 1) {
// centre vertically
yy += (height - band[0].dy) / 2;
}
let prevGap = Number.MAX_VALUE ; // edge of graph
band.forEach((node, i) => {
node.y = yy;
node.spaceAbove = prevGap;
node.spaceBelow = gaps[i] * kg;
yy += node.dy + node.spaceBelow;
prevGap = node.spaceBelow;
// XXX is this a good idea?
if (node.data && node.data.forceY !== undefined) {
node.y = margin + node.data.forceY * (height - node.dy);
}
});
if (band.length > 0) {
band[band.length - 1].spaceBelow = Number.MAX_VALUE ; // edge of graph
}
y += bandHeight;
});
});
}
layout.separation = function (x) {
if (!arguments.length) return separation
separation = required$3(x);
return layout
};
return layout
}
function required$3 (f) {
if (typeof f !== 'function') throw new Error()
return f
}
function prepareNodePorts (G, sortPorts) {
G.nodes().forEach(u => {
const node = G.node(u);
const ports = d3Collection.map();
function getOrSet (id, side) {
if (ports.has(id)) return ports.get(id)
const port = { id, node: node.data, side, incoming: [], outgoing: [] };
ports.set(id, port);
return port
}
G.inEdges(u).forEach(e => {
const edge = G.edge(e);
const port = getOrSet(edge.targetPortId || 'in', node.direction !== 'l' ? 'west' : 'east');
port.incoming.push(e);
edge.targetPort = port;
});
G.outEdges(u).forEach(e => {
const edge = G.edge(e);
const port = getOrSet(edge.sourcePortId || 'out', node.direction !== 'l' ? 'east' : 'west');
port.outgoing.push(e);
edge.sourcePort = port;
});
node.ports = ports.values();
node.ports.sort(sortPorts);
// Initialise from/to elsewhere lists
// XXX need to take more care with node directions
node.fromElsewhere = node.fromElsewhere || [];
node.toElsewhere = node.toElsewhere || [];
let fromElsewhereDy = 0;
node.fromElsewhere.forEach(link => {
link.x1 = node.x0;
fromElsewhereDy += link.dy;
});
// Set positions of ports, roughly -- so the other endpoints of links are
// known approximately when being sorted.
const y = { west: fromElsewhereDy, east: 0 };
const i = { west: 0, east: 0 };
node.ports.forEach(port => {
port.y = y[port.side];
port.index = i[port.side];
port.dy = Math.max(d3Array.sum(port.incoming, e => G.edge(e).dy),
d3Array.sum(port.outgoing, e => G.edge(e).dy));
const x = (port.side === 'west' ? node.x0 : node.x1);
port.outgoing.forEach(e => {
const link = G.edge(e);
link.x0 = x;
link.y0 = node.y + port.y + link.dy / 2;
});
port.incoming.forEach(e => {
const link = G.edge(e);
link.x1 = x;
link.y1 = node.y + port.y + link.dy / 2;
});
y[port.side] += port.dy;
i[port.side] += 1;
});
node.toElsewhere.forEach(link => {
link.x0 = node.x1;
});
});
}
function linkDirection (G, e, head = true) {
if (e.v === e.w) {
// pretend self-links go downwards
return Math.PI / 2 * (head ? +1 : -1)
} else {
// const source = G.node(e.v)
// const target = G.node(e.w)
// return Math.atan2(target.y - source.y,
// target.x0 - source.x1)
const link = G.edge(e);
return Math.atan2(link.y1 - link.y0,
link.x1 - link.x0)
}
}
/** @module edge-ordering */
/**
* Order the edges at all nodes.
*/
function orderEdges (G, opts) {
G.nodes().forEach(u => orderEdgesOne(G, u));
}
/**
* Order the edges at the given node.
* The ports have already been setup and sorted.
*/
function orderEdgesOne (G, v) {
const node = G.node(v);
node.ports.forEach(port => {
port.incoming.sort(compareDirection(G, node, false));
port.outgoing.sort(compareDirection(G, node, true));
});
}
/**
* Sort links based on their endpoints & type
*/
function compareDirection (G, node, head = true) {
return function (a, b) {
const da = linkDirection(G, a, head);
const db = linkDirection(G, b, head);
const c = head ? 1 : -1;
// links between same node, sort on type
if (a.v === b.v && a.w === b.w && Math.abs(da - db) < 1e-3) {
if (typeof a.name === 'number' && typeof b.name === 'number') {
return a.name - b.name
} else if (typeof a.name === 'string' && typeof b.name === 'string') {
return a.name.localeCompare(b.name)
} else {
return 0
}
}
// loops to same slice based on y-position
if (Math.abs(da - db) < 1e-3) {
if (a.w === b.w) {
return G.node(b.v).y - G.node(a.v).y
} else if (a.v === b.v) {
return G.node(b.w).y - G.node(a.w).y
} else {
return 0
}
}
// otherwise sort by direction
return c * (da - db)
}
}
function findFirst (links, p) {
let jmid = null;
for (let j = 0; j < links.length; ++j) {
if (p(links[j])) { jmid = j; break }
}
return jmid
}
/**
* Adjust radii of curvature to avoid overlaps, as much as possible.
* @param links - the list of links, ordered from outside to inside of bend
* @param rr - "r0" or "r1", the side to work on
*/
function sweepCurvatureInwards (links, rr) {
if (links.length === 0) return
// sweep from inside of curvature towards outside
let Rmin = 0; let h;
for (let i = links.length - 1; i >= 0; --i) {
h = links[i].dy / 2;
if (links[i][rr] - h < Rmin) { // inner radius
links[i][rr] = Math.min(links[i].Rmax, Rmin + h);
}
Rmin = links[i][rr] + h;
}
// sweep from outside of curvature towards centre
let Rmax = links[0].Rmax + links[0].dy / 2;
for (let i = 0; i < links.length; ++i) {
h = links[i].dy / 2;
if (links[i][rr] + h > Rmax) { // outer radius
links[i][rr] = Math.max(h, Rmax - h);
}
Rmax = links[i][rr] - h;
}
}
/**
* Edge positioning.
*
* @module link-positioning
*/
/*
* Requires incoming and outgoing attributes on nodes
*/
function layoutLinks (G) {
setEdgeEndpoints(G);
setEdgeCurvatures(G);
return G
}
function setEdgeEndpoints (G) {
G.nodes().forEach(u => {
const node = G.node(u);
let sy = node.y;
let ty = node.y;
node.fromElsewhere.forEach(link => {
link.y1 = ty + link.dy / 2;
link.d1 = node.backwards ? 'l' : 'r';
ty += link.dy;
});
node.ports.forEach(port => {
sy = node.y + port.y;
ty = node.y + port.y;
port.outgoing.forEach(e => {
const link = G.edge(e);
// link.x0 = node.x1
link.y0 = sy + link.dy / 2;
link.d0 = node.backwards ? 'l' : 'r';
sy += link.dy;
});
port.incoming.forEach(e => {
const link = G.edge(e);
// link.x1 = node.x0
link.y1 = ty + link.dy / 2;
link.d1 = node.backwards ? 'l' : 'r';
ty += link.dy;
});
});
node.toElsewhere.forEach(link => {
link.y0 = sy + link.dy / 2;
link.d0 = node.backwards ? 'l' : 'r';
sy += link.dy;
});
});
}
function setEdgeCurvatures (G) {
G.nodes().forEach(u => {
const node = G.node(u);
setEdgeEndCurvatures(node.toElsewhere, 'r0');
setEdgeEndCurvatures(node.fromElsewhere, 'r1');
node.ports.forEach(port => {
setEdgeEndCurvatures(port.outgoing.map(e => G.edge(e)), 'r0');
setEdgeEndCurvatures(port.incoming.map(e => G.edge(e)), 'r1');
});
});
}
function maximumRadiusOfCurvature (link) {
const Dx = link.x1 - link.x0;
const Dy = link.y1 - link.y0;
if (link.d0 !== link.d1) {
return Math.abs(Dy) / 2.1
} else {
return (Dy !== 0) ? (Dx * Dx + Dy * Dy) / Math.abs(4 * Dy) : Infinity
}
}
function setEdgeEndCurvatures (links, rr) {
// initialise segments, find reversal of curvature
links.forEach(link => {
// const link = (i < 0) ? link.segments[link.segments.length + i] : link.segments[i]
link.Rmax = maximumRadiusOfCurvature(link);
link[rr] = Math.max(link.dy / 2, (link.d0 === link.d1 ? link.Rmax * 0.6 : (5 + link.dy / 2)));
});
let jmid = (rr === 'r0'
? findFirst(links, f => f.y1 > f.y0)
: findFirst(links, f => f.y0 > f.y1));
if (jmid === null) jmid = links.length;
// Set maximum radius down from middle
sweepCurvatureInwards(links.slice(jmid), rr);
// Set maximum radius up from middle
if (jmid > 0) {
const links2 = [];
for (let j = jmid - 1; j >= 0; j--) links2.push(links[j]);
sweepCurvatureInwards(links2, rr);
}
}
const { Graph } = pkg;
function buildGraph (graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue) {
const G = new Graph({ directed: true, multigraph: true });
graph.nodes.forEach(function (node, i) {
const id = nodeId(node, i);
if (G.hasNode(id)) throw new Error('duplicate: ' + id)
G.setNode(id, {
data: node,
index: i,
backwards: nodeBackwards(node, i),
fromElsewhere: node.fromElsewhere || [],
toElsewhere: node.toElsewhere || [],
// XXX don't need these now have nodePositions?
x0: node.x0,
x1: node.x1,
y: node.y0
});
});
graph.links.forEach(function (link, i) {
const v = idAndPort(sourceId(link, i));
const w = idAndPort(targetId(link, i));
const label = {
data: link,
sourcePortId: v.port,
targetPortId: w.port,
index: i,
points: [],
value: linkValue(link, i),
type: linkType(link, i)
};
if (!G.hasNode(v.id)) throw new Error('missing: ' + v.id)
if (!G.hasNode(w.id)) throw new Error('missing: ' + w.id)
G.setEdge(v.id, w.id, label, linkType(link, i));
});
G.setGraph({});
return G
}
function idAndPort (x) {
if (typeof x === 'object') return x
return { id: x, port: undefined }
}
/**
*/
function defaultNodes (graph) {
return graph.nodes
}
function defaultLinks (graph) {
return graph.links
}
function defaultNodeId (d) {
return d.id
}
function defaultNodeBackwards (d) {
return d.direction && d.direction.toLowerCase() === 'l'
}
function defaultSourceId (d) {
// return typeof d.source === 'object' ? d.source.id : d.source
return {
id: typeof d.source === 'object' ? d.source.id : d.source,
port: typeof d.sourcePort === 'object' ? d.sourcePort.id : d.sourcePort
}
}
function defaultTargetId (d) {
// return typeof d.target === 'object' ? d.target.id : d.target
return {
id: typeof d.target === 'object' ? d.target.id : d.target,
port: typeof d.targetPort === 'object' ? d.targetPort.id : d.targetPort
}
}
function defaultLinkType (d) {
return d.type
}
function defaultSortPorts (a, b) {
// XXX weighted sum
return a.id.localeCompare(b.id)
}
// function defaultNodeSubdivisions
function sankeyLayout () {
let nodes = defaultNodes;
let links = defaultLinks;
let nodeId = defaultNodeId;
let nodeBackwards = defaultNodeBackwards;
let sourceId = defaultSourceId;
let targetId = defaultTargetId;
let linkType = defaultLinkType;
let ordering = null;
let rankSets = [];
const maxIterations = 25; // XXX setter/getter
let nodePosition = null;
let sortPorts = defaultSortPorts;
// extent
let x0 = 0;
let y0 = 0;
let x1 = 1;
let y1 = 1;
// node width
let dx = 1;
let scale = null;
let linkValue = function (e) { return e.value };
let whitespace = 0.5;
let verticalLayout = positionNodesVertically$1();
function sankey () {
const graph = { nodes: nodes.apply(null, arguments), links: links.apply(null, arguments) };
const G = buildGraph(graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue);
setNodeValues(G, linkValue);
if (nodePosition) {
// hard-coded node positions
G.nodes().forEach(u => {
const node = G.node(u);
const pos = nodePosition(node.data);
node.x0 = pos[0];
node.x1 = pos[0] + dx;
node.y = pos[1];
});
setWidths(G, scale);
} else {
// calculate node positions
if (ordering !== null) {
applyOrdering(G, ordering);
} else {
assignRanks(G, rankSets);
sortNodes(G, maxIterations);
}
addDummyNodes(G);
setNodeValues(G, linkValue);
if (ordering === null) {
// XXX sort nodes?
sortNodes(G, maxIterations);
}
const nested = nestGraph(G.nodes().map(u => G.node(u)));
maybeScaleToFit(G, nested);
setWidths(G, scale);
// position nodes
verticalLayout(nested, y1 - y0, whitespace);
positionHorizontally(G, x1 - x0, dx);
// adjust origin
G.nodes().forEach(u => {
const node = G.node(u);
node.x0 += x0;
node.x1 += x0;
node.y += y0;
});
}
// sort & position links
prepareNodePorts(G, sortPorts);
orderEdges(G);
layoutLinks(G);
removeDummyNodes(G);
addLinkEndpoints(G);
copyResultsToGraph(G);
return graph
}
sankey.update = function (graph, doOrderLinks) {
const G = buildGraph(graph, nodeId, nodeBackwards, sourceId, targetId, linkType, linkValue);
setNodeValues(G, linkValue);
const nested = nestGraph(G.nodes().map(u => G.node(u)));
maybeScaleToFit(G, nested);
setWidths(G, scale);
prepareNodePorts(G, sortPorts);
orderEdges(G);
layoutLinks(G);
// removeDummyNodes(G)
addLinkEndpoints(G);
copyResultsToGraph(G);
return graph
};
// if (scale === null) sankey.scaleToFit(graph)
// // set node and edge sizes
// setNodeValues(graph, linkValue, scale)
// if (doOrderLinks) {
// orderLinks(graph)
// }
// layoutLinks(graph)
// return graph
// }
sankey.nodes = function (x) {
if (arguments.length) {
nodes = required$2(x);
return sankey
}
return nodes
};
sankey.links = function (x) {
if (arguments.length) {
links = required$2(x);
return sankey
}
return links
};
sankey.nodeId = function (x) {
if (arguments.length) {
nodeId = required$2(x);
return sankey
}
return nodeId
};
sankey.nodeBackwards = function (x) {
if (arguments.length) {
nodeBackwards = required$2(x);
return sankey
}
return nodeBackwards
};
sankey.sourceId = function (x) {
if (arguments.length) {
sourceId = required$2(x);
return sankey
}
return sourceId
};
sankey.targetId = function (x) {
if (arguments.length) {
targetId = required$2(x);
return sankey
}
return targetId
};
sankey.linkType = function (x) {
if (arguments.length) {
linkType = required$2(x);
return sankey
}
return linkType
};
sankey.sortPorts = function (x) {
if (arguments.length) {
sortPorts = required$2(x);
return sankey
}
return sortPorts
};
// sankey.scaleToFit = function (graph) {
function maybeScaleToFit (G, nested) {
if (scale !== null) return
const maxValue = d3Array.sum(nested.bandValues);
if (maxValue <= 0) {
scale = 1;
} else {
scale = (y1 - y0) / maxValue;
if (whitespace !== 1) scale *= (1 - whitespace);
}
}
sankey.ordering = function (x) {
if (!arguments.length) return ordering
ordering = x;
return sankey
};
sankey.rankSets = function (x) {
if (!arguments.length) return rankSets
rankSets = x;
return sankey
};
sankey.nodeWidth = function (x) {
if (!arguments.length) return dx
dx = x;
return sankey
};
sankey.nodePosition = function (x) {
if (!arguments.length) return nodePosition
nodePosition = x;
return sankey
};
sankey.size = function (x) {
if (!arguments.length) return [x1 - x0, y1 - y0]
x0 = y0 = 0;
x1 = +x[0];
y1 = +x[1];
return sankey
};
sankey.extent = function (x) {
if (!arguments.length) return [[x0, y0], [x1, y1]]
x0 = +x[0][0];
y0 = +x[0][1];
x1 = +x[1][0];
y1 = +x[1][1];
return sankey
};
sankey.whitespace = function (x) {
if (!arguments.length) return whitespace
whitespace = x;
return sankey
};
sankey.scale = function (x) {
if (!arguments.length) return scale
scale = x;
return sankey
};
sankey.linkValue = function (x) {
if (!arguments.length) return linkValue
linkValue = x;
return sankey
};
sankey.verticalLayout = function (x) {
if (!arguments.length) return verticalLayout
verticalLayout = required$2(x);
return sankey
};
function applyOrdering (G, ordering) {
ordering.forEach((x, i) => {
x.forEach((u, j) => {
if (u.forEach) {
u.forEach((v, k) => {
const d = G.node(v);
if (d) {
d.rank = i;
d.band = j;
d.depth = k;
}
});
} else {
const d = G.node(u);
if (d) {
d.rank = i;
// d.band = 0
d.depth = j;
}
}
});
});
}
return sankey
}
function setNodeValues (G, linkValue) {
G.nodes().forEach(u => {
const d = G.node(u);
let incoming = d3Array.sum(G.inEdges(u), e => G.edge(e).value);
let outgoing = d3Array.sum(G.outEdges(u), e => G.edge(e).value);
incoming += d3Array.sum(d.fromElsewhere || [], link => linkValue(link));
outgoing += d3Array.sum(d.toElsewhere || [], link => linkValue(link));
d.value = Math.max(incoming, outgoing);
});
}
function setWidths (G, scale) {
G.edges().forEach(e => {
const edge = G.edge(e);
edge.dy = edge.value * scale;
});
G.nodes().forEach(u => {
const node = G.node(u);
node.dy = node.value * scale;
// Initialise from/to elsewhere lists
node.fromElsewhere = (node.fromElsewhere || []);
node.toElsewhere = (node.toElsewhere || []);
node.fromElsewhere.forEach(link => {
link.dy = link.value * scale;
link.source = { id: '__from_elsewhere_' + u };
link.target = node.data;
});
node.toElsewhere.forEach(link => {
link.dy = link.value * scale;
link.source = node.data;
link.target = { id: '__to_elsewhere_' + u };
});
});
}
function required$2 (f) {
if (typeof f !== 'function') throw new Error()
return f
}
function addLinkEndpoints (G) {
G.edges().forEach(e => {
const edge = G.edge(e);
edge.points.unshift({ x: edge.x0, y: edge.y0, ro: edge.r0, d: edge.d0 });
edge.points.push({ x: edge.x1, y: edge.y1, ri: edge.r1, d: edge.d1 });
});
G.nodes().forEach(u => {
const node = G.node(u);
node.fromElsewhere.forEach(link => {
link.points = [{ x: link.x1, y: link.y1, ri: link.r1, d: link.d1, style: 'down-right' }];
});
node.toElsewhere.forEach(link => {
link.points = [{ x: link.x0, y: link.y0, ri: link.r0, d: link.d0, style: 'right-down' }];
});
});
}
function copyResultsToGraph (G, graph) {
G.nodes().forEach(u => {
const node = G.node(u);
// Build lists of edge data objects
node.data.incoming = [];
node.data.outgoing = [];
node.data.ports = node.ports;
node.data.ports.forEach(port => {
port.incoming = [];
port.outgoing = [];
});
node.data.dy = node.dy;
node.data.x0 = node.x0;
node.data.x1 = node.x1;
node.data.y0 = node.y;
node.data.y1 = node.y + node.dy;
node.data.rank = node.rank;
node.data.band = node.band;
node.data.depth = node.depth;
node.data.value = node.value;
node.data.spaceAbove = node.spaceAbove;
node.data.spaceBelow = node.spaceBelow;
});
G.edges().forEach(e => {
const edge = G.edge(e);
edge.data.source = G.node(e.v).data;
edge.data.target = G.node(e.w).data;
edge.data.sourcePort = edge.sourcePort;
edge.data.targetPort = edge.targetPort;
// console.log(edge)
edge.data.source.outgoing.push(edge.data);
edge.data.target.incoming.push(edge.data);
if (edge.data.sourcePort) edge.data.sourcePort.outgoing.push(edge.data);
if (edge.data.targetPort) edge.data.targetPort.incoming.push(edge.data);
edge.data.value = edge.value;
edge.data.type = edge.type;
edge.data.dy = edge.dy;
edge.data.points = edge.points || [];
// edge.data.id = `${e.v}-${e.w}-${e.name}`
});
}
function positionNodesVertically () {
let iterations = 25;
let nodePadding = 8;
function layout (nested, height) {
initializeNodeDepth();
resolveCollisions();
for (let alpha = 1, i = iterations; i > 0; --i) {
relaxRightToLeft(alpha *= 0.99);
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function initializeNodeDepth () {
nested.forEach(layer => {
let i = 0;
layer.forEach(band => {
// ignore bands for this layout
band.forEach(node => {
node.y = i++;
});
});
});
}
function relaxLeftToRight (alpha) {
nested.forEach(layer => {
layer.forEach(band => {
band.forEach(node => {
const edges = node.incoming || node.edges;
if (edges.length) {
const y = d3Array.sum(edges, weightedSource) / d3Array.sum(edges, value);
node.y += (y - center(node)) * alpha;
}
});
});
});
function weightedSource (link) {
return center(link.source) * link.value
}
}
function relaxRightToLeft (alpha) {
nested.slice().reverse().forEach(layer => {
layer.forEach(band => {
band.forEach(node => {
const edges = node.outgoing || node.edges;
if (edges.length) {
const y = d3Array.sum(edges, weightedTarget) / d3Array.sum(edges, value);
node.y += (y - center(node)) * alpha;
}
});
});
});
function weightedTarget (link) {
return center(link.target) * link.value
}
}
function resolveCollisions () {
nested.forEach(layer => {
layer.forEach(nodes => {
let node;
let dy;
let y0 = 0;
const n = nodes.length;
let 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 - height;
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;
}
}
});
});
}
}
layout.iterations = function (x) {
if (!arguments.length) return iterations
iterations = +x;
return layout
};
layout.padding = function (x) {
if (!arguments.length) return nodePadding
nodePadding = +x;
return layout
};
return layout
}
function center (node) {
return node.y + node.dy / 2
}
function value (link) {
return link.value
}
function ascendingDepth (a, b) {
return a.y - b.y
}
// function defaultSegments (d) {
// return d.segments
// }
function defaultMinWidth (d) {
return (d.dy === 0) ? 0 : 2
}
function sankeyLink () {
// var segments = defaultSegments
let minWidth = defaultMinWidth;
function radiusBounds (d) {
const Dx = d.x1 - d.x0;
const Dy = d.y1 - d.y0;
const Rmin = d.dy / 2;
const Rmax = (Dx * Dx + Dy * Dy) / Math.abs(4 * Dy);
return [Rmin, Rmax]
}
function link (d) {
if (d.points.length === 1) {
return toOrFromElsewherePath(d)
}
let path = '';
let seg;
for (let i = 0; i < d.points.length - 1; ++i) {
seg = {
x0: d.points[i].x,
y0: d.points[i].y,
x1: d.points[i + 1].x,
y1: d.points[i + 1].y,
r0: d.points[i].ro,
r1: d.points[i + 1].ri,
d0: d.points[i].d,
d1: d.points[i + 1].d,
dy: d.dy
};
path += segmentPath(seg);
}
return path
}
function segmentPath (d) {
const dir = (d.d0 || 'r') + (d.d1 || 'r');
if (d.source && d.source === d.target) {
return selfLink(d)
}
if (dir === 'rl') {
return fbLink(d)
}
if (dir === 'rd') {
return fdLink(d)
}
if (dir === 'dr') {
return dfLink(d)
}
if (dir === 'lr') {
return bfLink(d)
}
// Minimum thickness 2px
const h = Math.max(minWidth(d), d.dy) / 2;
let x0 = d.x0;
let x1 = d.x1;
let y0 = d.y0;
let y1 = d.y1;
if (x1 < x0) {
[x0, x1] = [x1, x0];
[y0, y1] = [y1, y0];
}
const f = y1 > y0 ? 1 : -1;
const fx = 1; // dir === 'll' ? -1 : 1;
const Rlim = radiusBounds(d);
const defaultRadius = Math.max(Rlim[0], Math.min(Rlim[1], (x1 - x0) / 3));
let r0 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r0 || defaultRadius)));
let r1 = Math.max(Rlim[0], Math.min(Rlim[1], (d.r1 || defaultRadius)));
const dcx = (x1 - x0);
const dcy = (y1 - y0) - f * (r0 + r1);
const D = Math.sqrt(dcx * dcx + dcy * dcy);
const phi = -f * Math.acos(Math.min(1, (r0 + r1) / D));
const psi = Math.atan2(dcy, dcx);
let theta = Math.PI / 2 + f * (psi + phi);
let hs = h * f * Math.sin(theta);
let hc = h * Math.cos(theta);
let x2 = x0 + fx * r0 * Math.sin(Math.abs(theta));
let x3 = x1 - fx * r1 * Math.sin(Math.abs(theta));
let y2 = y0 + r0 * f * (1 - Math.cos(theta));
let y3 = y1 - r1 * f * (1 - Math.cos(theta));
if (isNaN(theta) || Math.abs(theta) < 1e-3) {
theta = r0 = r1 = 0;
x2 = x0;
x3 = x1;
y2 = y0;
y3 = y1;
hs = 0;
hc = h;
}
function arc (dir, r) {
const f = (dir * (y1 - y0) > 0) ? 1 : 0;
let rr = (fx * dir * (y1 - y0) > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
}
// if (fx * (x2 - x3) < 0 || Math.abs(y1 - y0) > 4*h) {
// XXX this causes juddering during transitions
const path = (
'M' + [x0, y0 - h] + ' ' +
arc(+1, r0) + [x2 + hs, y2 - hc] + ' ' +
'L' + [x3 + hs, y3 - hc] + ' ' +
arc(-1, r1) + [x1, y1 - h] + ' ' +
'L' + [x1, y1 + h] + ' ' +
arc(+1, r1) + [x3 - hs, y3 + hc] + ' ' +
'L' + [x2 - hs, y2 + hc] + ' ' +
arc(-1, r0) + [x0, y0 + h] + ' ' +
'Z'
);
if (/NaN/.test(path)) {
console.error('path NaN', d, path);
}
return path
}
function selfLink (d) {
const h = Math.max(minWidth(d), d.dy) / 2;
const r = h * 1.5;
const theta = 2 * Math.PI;
const x0 = d.x0;
const y0 = d.y0;
function arc (dir) {
const f = (dir > 0) ? 1 : 0;
const rr = (dir > 0) ? (r + h) : (r - h);
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 1 ' + f + ' '
}
return ('M' + [x0 + 0.1, y0 - h] + ' ' +
arc(+1) + [x0 - 0.1, y0 - h] + ' ' +
'L' + [x0 - 0.1, y0 + h] + ' ' +
arc(-1) + [x0 + 0.1, y0 + h] + ' ' +
'Z')
}
function fbLink (d) {
// Minimum thickness 2px
const h = Math.max(minWidth(d), d.dy) / 2;
const x0 = d.x0;
const x1 = d.x1;
const y0 = d.y0;
const y1 = d.y1;
const Dx = d.x1 - d.x0;
const Dy = d.y1 - d.y0;
// Rlim = radiusBounds(d),
const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h); // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
const r = Math.min(Math.abs(y1 - y0) / 2.1, defaultRadius); // 2*(d.r || defaultRadius),
const theta = Math.atan2(Dy - 2 * r, Dx);
const f = d.y1 > d.y0 ? 1 : -1;
const hs = h * Math.sin(theta);
const hc = h * Math.cos(theta);
const x2 = d.x0 + r * Math.sin(Math.abs(theta));
const x3 = d.x1 + r * Math.sin(Math.abs(theta));
const y2 = d.y0 + r * f * (1 - Math.cos(theta));
const y3 = d.y1 - r * f * (1 - Math.cos(theta));
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0;
let rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
}
return ('M' + [x0, y0 - h] + ' ' +
arc(+1) + [x2 + hs, y2 - hc] + ' ' +
'L' + [x3 + hs, y3 - hc] + ' ' +
arc(+1) + [x1, y1 + h] + ' ' +
'L' + [x1, y1 - h] + ' ' +
arc(-1) + [x3 - hs, y3 + hc] + ' ' +
'L' + [x2 - hs, y2 + hc] + ' ' +
arc(-1) + [x0, y0 + h] + ' ' +
'Z')
}
function fdLink (d) {
// Minimum thickness 2px
const h = Math.max(minWidth(d), d.dy) / 2;
const x0 = d.x0;
const x1 = d.x1;
const y0 = d.y0;
const y1 = d.y1;
const theta = Math.PI / 2;
const r = Math.max(0, x1 - x0);
const y2 = y0 + r;
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0;
let rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
}
return ('M' + [x0, y0 - h] + ' ' +
arc(+1) + [x1 + h, y2] + ' ' +
'L' + [x1 + h, y1] + ' ' +
'' + [x1 - h, y1] + ' ' +
'' + [x1 - h, y2] + ' ' +
arc(-1) + [x0, y0 + h] + ' ' +
'Z')
}
function dfLink (d) {
// Minimum thickness 2px
const h = Math.max(minWidth(d), d.dy) / 2;
const x0 = d.x0;
const x1 = d.x1;
const y0 = d.y0;
const y1 = d.y1;
const theta = Math.PI / 2;
const r = Math.max(0, x1 - x0);
const y2 = y1 - r;
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0;
let rr = (dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
}
return ('M' + [x0 - h, y0] + ' ' +
'L' + [x0 + h, y0] + ' ' +
'' + [x0 + h, y2] + ' ' +
arc(-1) + [x1, y1 - h] + ' ' +
'L' + [x1, y1 + h] + ' ' +
arc(+1) + [x0 - h, y2] + ' ' +
'Z')
}
function bfLink (d) {
// Minimum thickness 2px
const h = Math.max(minWidth(d), d.dy) / 2;
const x0 = d.x0;
const x1 = d.x1;
const y0 = d.y0;
const y1 = d.y1;
const Dx = d.x1 - d.x0;
const Dy = d.y1 - d.y0;
// Rlim = radiusBounds(d),
const defaultRadius = ((d.r0 + d.r1) / 2) || (5 + h); // Math.max(Rlim[0], Math.min(Rlim[1], Dx/3)),
const r = Math.min(Math.abs(Dy) / 2.1, defaultRadius); // 2*(d.r || defaultRadius),
const theta = Math.atan2(Dy - 2 * r, Dx);
// const l = Math.sqrt(Math.max(0, Dx * Dx + (Dy - 2 * r) * (Dy - 2 * r)))
const f = d.y1 > d.y0 ? 1 : -1;
const hs = h * Math.sin(theta);
const hc = h * Math.cos(theta);
const x2 = d.x0 - r * Math.sin(Math.abs(theta));
const x3 = d.x1 - r * Math.sin(Math.abs(theta));
const y2 = d.y0 + r * f * (1 - Math.cos(theta));
const y3 = d.y1 - r * f * (1 - Math.cos(theta));
function arc (dir) {
const f = (dir * theta > 0) ? 1 : 0;
let rr = (-dir * theta > 0) ? (r + h) : (r - h);
// straight line
if (theta === 0) { rr = r; }
return 'A' + rr + ' ' + rr + ' ' + Math.abs(theta) + ' 0 ' + f + ' '
}
return ('M' + [x0, y0 - h] + ' ' +
arc(-1) + [x2 - hs, y2 - hc] + ' ' +
'L' + [x3 - hs, y3 - hc] + ' ' +
arc(-1) + [x1, y1 + h] + ' ' +
'L' + [x1, y1 - h] + ' ' +
arc(+1) + [x3 + hs, y3 - hc] + ' ' +
'L' + [x2 + hs, y2 - hc] + ' ' +
arc(+1) + [x0, y0 + h] + ' ' +
'Z')
}
function toOrFromElsewherePath (d) {
const p = d.points[0];
const h = Math.max(minWidth(d), d.dy) / 2;
// XXX draw these properly with curves and appropriate radii
if (p.style === 'down-right') {
return ('M' + [p.x - 20, p.y - h] + ' ' +
'L' + [p.x, p.y - h] + ' ' +
'L' + [p.x, p.y + h] + ' ' +
'L' + [p.x - 20, p.y + h] + ' ' +
'Z')
}
if (p.style === 'right-down') {
return ('M' + [p.x, p.y - h] + ' ' +
'L' + [p.x + 20, p.y - h] + ' ' +
'L' + [p.x + 20, p.y + h] + ' ' +
'L' + [p.x, p.y + h] + ' ' +
'Z')
}
}
link.minWidth = function (x) {
if (arguments.length) {
minWidth = required$1(x);
return link
}
return minWidth
};
return link
}
function required$1 (f) {
if (typeof f !== 'function') throw new Error()
return f
}
function sankeyNode () {
let nodeTitle = (d) => d.title !== undefined ? d.title : d.id;
let nodeValue = (d) => null;
let nodeVisible = (d) => !!nodeTitle(d);
function sankeyNode (context) {
const selection = context.selection ? context.selection() : context;
if (selection.select('text').empty()) {
selection.append('title');
selection.append('line')
.attr('x1', 0)
.attr('x2', 0);
selection.append('rect')
.attr('class', 'node-body');
selection.append('text')
.attr('class', 'node-value')
.attr('dy', '.35em')
.attr('text-anchor', 'middle');
selection.append('text')
.attr('class', 'node-title')
.attr('dy', '.35em');
selection.append('rect')
.attr('class', 'node-click-target')
.attr('x', -5)
.attr('y', -5)
.attr('width', 10)
.style('fill', 'none')
.style('visibility', 'hidden')
.style('pointer-events', 'all');
selection
.attr('transform', nodeTransform);
}
selection.each(function (d) {
const title = d3Selection.select(this).select('title');
const value = d3Selection.select(this).select('.node-value');
let text = d3Selection.select(this).select('.node-title');
let line = d3Selection.select(this).select('line');
let body = d3Selection.select(this).select('.node-body');
let clickTarget = d3Selection.select(this).select('.node-click-target');
// Local var for title position of each node
const layoutData = titlePosition(d);
layoutData.dy = (d.y0 === d.y1) ? 0 : Math.max(1, d.y1 - d.y0);
const separateValue = (d.x1 - d.x0) > 2;
const titleText = nodeTitle(d) + ((!separateValue && nodeValue(d))
? ' (' + nodeValue(d) + ')'
: '');
// Update un-transitioned
title
.text(titleText);
value
.text(nodeValue)
.style('display', separateValue ? 'inline' : 'none');
text
.attr('text-anchor', layoutData.right ? 'end' : 'start')
.text(titleText)
.each(wrap, 100);
// Are we in a transition?
if (context !== selection) {
text = text.transition(context);
line = line.transition(context);
body = body.transition(context);
clickTarget = clickTarget.transition(context);
}
// Update
context
.attr('transform', nodeTransform);
line
.attr('y1', function (d) { return layoutData.titleAbove ? -5 : 0 })
.attr('y2', function (d) { return layoutData.dy })
.style('display', function (d) {
return (d.y0 === d.y1 || !nodeVisible(d)) ? 'none' : 'inline'
});
clickTarget
.attr('height', function (d) { return layoutData.dy + 5 });
body
.attr('width', function (d) { return d.x1 - d.x0 })
.attr('height', function (d) { return layoutData.dy });
text
.attr('transform', textTransform)
.style('display', function (d) {
return (d.y0 === d.y1 || !nodeVisible(d)) ? 'none' : 'inline'
});
value
.style('font-size', function (d) { return Math.min(d.x1 - d.x0 - 4, d.y1 - d.y0 - 4) + 'px' })
.attr('transform', function (d) {
const dx = d.x1 - d.x0;
const dy = d.y1 - d.y0;
const theta = dx > dy ? 0 : -90;
return 'translate(' + (dx / 2) + ',' + (dy / 2) + ') rotate(' + theta + ')'
});
function textTransform (d) {
const layout = layoutData;
const y = layout.titleAbove ? -10 : (d.y1 - d.y0) / 2;
let x;
if (layout.titleAbove) {
x = (layout.right ? 4 : -4);
} else {
x = (layout.right ? -4 : d.x1 - d.x0 + 4);
}
return 'translate(' + x + ',' + y + ')'
}
});
}
sankeyNode.nodeVisible = function (x) {
if (arguments.length) {
nodeVisible = required(x);
return sankeyNode
}
return nodeVisible
};
sankeyNode.nodeTitle = function (x) {
if (arguments.length) {
nodeTitle = required(x);
return sankeyNode
}
return nodeTitle
};
sankeyNode.nodeValue = function (x) {
if (arguments.length) {
nodeValue = required(x);
return sankeyNode
}
return nodeValue
};
return sankeyNode
}
function nodeTransform (d) {
return 'translate(' + d.x0 + ',' + d.y0 + ')'
}
function titlePosition (d) {
let titleAbove = false;
let right = false;
// If thin, and there's enough space, put above
if (d.spaceAbove > 20 && d.style !== 'type') {
titleAbove = true;
} else {
titleAbove = false;
}
if (d.incoming.length === 0) {
right = true;
titleAbove = false;
} else if (d.outgoing.length === 0) {
right = false;
titleAbove = false;
}
return { titleAbove, right }
}
function wrap (d, width) {
const text = d3Selection.select(this);
const lines = text.text().split(/\n/);
const lineHeight = 1.1; // ems
if (lines.length === 1) return
text.text(null);
lines.forEach(function (line, i) {
text.append('tspan')
.attr('x', 0)
.attr('dy', (i === 0 ? 0.7 - lines.length / 2 : 1) * lineHeight + 'em')
.text(line);
});
}
function required (f) {
if (typeof f !== 'function') throw new Error()
return f
}
function positionGroup (nodes, group) {
const rect = {
top: Number.MAX_VALUE,
left: Number.MAX_VALUE,
bottom: 0,
right: 0
};
group.nodes.forEach(n => {
const node = nodes.get(n);
if (!node) return
if (node.x0 < rect.left) rect.left = node.x0;
if (node.x1 > rect.right) rect.right = node.x1;
if (node.y0 < rect.top) rect.top = node.y0;
if (node.y1 > rect.bottom) rect.bottom = node.y1;
});
group.rect = rect;
return group
}
// The reusable SVG component for the sliced Sankey diagram
function linkTitleGenerator (nodeTitle, typeTitle, fmt) {
return function (d) {
const parts = [];
const sourceTitle = nodeTitle(d.source);
const targetTitle = nodeTitle(d.target);
const matTitle = typeTitle(d);
parts.push(`${sourceTitle} → ${targetTitle}`);
if (matTitle) parts.push(matTitle);
parts.push(fmt(d.value));
return parts.join('\n')
}
}
function sankeyDiagram () {
let margin = { top: 0, right: 0, bottom: 0, left: 0 };
let selectedEdge = null;
let groups = [];
const fmt = d3Format.format('.3s');
const node = sankeyNode();
const link = sankeyLink();
let linkColor = d => null;
let linkTitle = linkTitleGenerator(node.nodeTitle(), d => d.type, fmt);
let linkLabel = defaultLinkLabel;
let linkImportance = defaultLinkImportance;
let linkImportanceAgg = defaultLinkImportanceAgg;
const listeners = d3Dispatch.dispatch('selectNode', 'selectGroup', 'selectLink');
/* Main chart */
function exports (context) {
const selection = context.selection ? context.selection() : context;
selection.each(function (G) {
// Create the skeleton, if it doesn't already exist
const svg = d3Selection.select(this);
let sankey = svg.selectAll('.sankey')
.data([{ type: 'sankey' }]);
const sankeyEnter = sankey.enter()
.append('g')
.classed('sankey', true);
sankeyEnter.append('g').classed('groups', true);
sankeyEnter.append('g').classed('links', true); // Links below nodes
sankeyEnter.append('g').classed('nodes', true);
sankeyEnter.append('g').classed('slice-titles', true); // Slice titles
sankey = sankey.merge(sankeyEnter);
// Update margins
sankey
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// .select('.slice-titles')
// .attr('transform', 'translate(' + margin.left + ',0)')
// Groups of nodes
const nodeMap = d3Collection.map(G.nodes, n => n.id);
const groupsPositioned = (groups || []).map(g => positionGroup(nodeMap, g));
// All links -- including "from elsewhere" and "to elsewhere" ones
const links = Array.from(G.links);
G.nodes.forEach(node => {
Array.prototype.push.apply(links, node.fromElsewhere || []);
Array.prototype.push.apply(links, node.toElsewhere || []);
});
// Render
updateNodes(sankey, context, G.nodes);
updateLinks(sankey, context, links);
updateGroups(svg, groupsPositioned);
// updateSlices(svg, layout.slices(nodes));
// Events
svg.on('click', function () {
listeners.call('selectNode', this, null);
listeners.call('selectLink', this, null);
});
});
}
function updateNodes (sankey, context, nodes) {
let nodeSel = sankey
.select('.nodes')
.selectAll('.node')
.data(nodes, d => d.id);
// EXIT
nodeSel.exit().remove();
nodeSel = nodeSel.merge(
nodeSel.enter()
.append('g')
.attr('class', 'node')
.call(node)
.on('click', selectNode));
if (context instanceof d3Transition.transition) {
nodeSel.transition(context)
.call(node);
} else {
nodeSel.call(node);
}
}
function updateLinks (sankey, context, edges) {
let linkSel = sankey
.select('.links')
.selectAll('.link')
.data(edges, d => d.source.id + '-' + d.target.id + '-' + d.type);
// EXIT
linkSel.exit().remove();
// ENTER
const linkEnter = linkSel.enter()
.append('g')
.attr('class', 'link')
.on('click', selectLink);
linkEnter.append('path')
.attr('d', link)
.style('fill', 'white')
.each(function (d) { this._current = d; });
linkEnter.append('title');
linkEnter.append('text')
.attr('class', 'label')
.attr('dy', '0.35em')
.attr('x', d => d.points[0].x + 4)
.attr('y', d => d.points[0].y);
// UPDATE
linkSel = linkSel.merge(linkEnter);
// Calculate group importance for sorting
calculateGroupImportance(edges);
// Non-transition updates
linkSel.classed('selected', (d) => d.id === selectedEdge);
linkSel.sort(linkOrder);
// Transition updates, if available
if (context instanceof d3Transition.transition) {
linkSel = linkSel.transition(context);
linkSel
.select('path')
.style('fill', linkColor)
.each(function (d) {
d3Selection.select(this)
.transition(context)
.attrTween('d', interpolateLink);
});
} else {
linkSel
.select('path')
.style('fill', linkColor)
.attr('d', link);
}
linkSel.select('title')
.text(linkTitle);
linkSel.select('.label')
.text(linkLabel)
.attr('x', d => d.points[0].x + 4)
.attr('y', d => d.points[0].y);
}
function calculateGroupImportance (edges) {
// Group links by source-target pair
const groups = new Map();
// Calculate individual importance for each link
edges.forEach(edge => {
const groupKey = edge.source.id + '-' + edge.target.id;
if (!groups.has(groupKey)) {
groups.set(groupKey, []);
}
const importance = linkImportance(edge);
edge._importance = importance;
groups.get(groupKey).push({ edge, importance });
});
// Calculate aggregated importance for each group and assign to all links in group
groups.forEach(linkGroup => {
const importanceValues = linkGroup.map(item => item.importance);
const groupImportance = linkImportanceAgg(importanceValues);
linkGroup.forEach(item => {
item.edge._groupImportance = groupImportance;
});
});
}
// function updateSlices(svg, slices) {
// var slice = svg.select('.slice-titles').selectAll('.slice')
// .data(slices, function(d) { return d.id; });
// var textWidth = (slices.length > 1 ?
// 0.9 * (slices[1].x - slices[0].x) :
// null);
// slice.enter().append('g')
// .attr('class', 'slice')
// .append('foreignObject')
// .attr('requiredFeatures',
// 'http://www.w3.org/TR/SVG11/feature#Extensibility')
// .attr('height', margin.top)
// .attr('class', 'title')
// .append('xhtml:div')
// .style('text-align', 'center')
// .style('word-wrap', 'break-word');
// // .text(pprop('sliceMetadata', 'title'));
// slice
// .attr('transform', function(d) {
// return 'translate(' + (d.x - textWidth / 2) + ',0)'; })
// .select('foreignObject')
// .attr('width', textWidth)
// .select('div');
// // .text(pprop('sliceMetadata', 'title'));
// slice.exit().remove();
// }
function updateGroups (svg, groups) {
let group = svg.select('.groups').selectAll('.group')
.data(groups);
// EXIT
group.exit().remove();
// ENTER
const enter = group.enter().append('g')
.attr('class', 'group');
// .on('click', selectGroup);
enter.append('rect');
enter.append('text')
.attr('x', -10)
.attr('y', -25);
group = group.merge(enter);
group
.style('display', d => d.title ? 'inline' : 'none')
.attr('transform', d => `translate(${d.rect.left},${d.rect.top})`)
.select('rect')
.attr('x', -10)
.attr('y', -20)
.attr('width', d => d.rect.right - d.rect.left + 20)
.attr('height', d => d.rect.bottom - d.rect.top + 30);
group.select('text')
.text(d => d.title);
}
function interpolateLink (b) {
// XXX should limit radius better
b.points.forEach(function (p) {
if (p.ri > 1e3) p.ri = 1e3;
if (p.ro > 1e3) p.ro = 1e3;
});
const interp = d3Interpolate.interpolate(linkGeom(this._current), b);
const that = this;
return function (t) {
that._current = interp(t);
return link(that._current)
}
}
function linkGeom (l) {
return {
points: l.points,
dy: l.dy
}
}
function linkOrder (a, b) {
// Selected links always on top
if (a.id === selectedEdge) return +1
if (b.id === selectedEdge) return -1
// All other sorting based on group importance
return a._groupImportance - b._groupImportance
}
function selectLink (d) {
d3Selection.event.stopPropagation();
const el = d3Selection.select(this).node();
listeners.call('selectLink', el, d);
}
function selectNode (d) {
d3Selection.event.stopPropagation();
const el = d3Selection.select(this).node();
listeners.call('selectNode', el, d);
}
// function selectGroup(d) {
// d3.event.stopPropagation();
// var el = d3.select(this)[0][0];
// dispatch.selectGroup.call(el, d);
// }
exports.margins = function (_x) {
if (!arguments.length) return margin
margin = {
top: _x.top === undefined ? margin.top : _x.top,
left: _x.left === undefined ? margin.left : _x.left,
bottom: _x.bottom === undefined ? margin.bottom : _x.bottom,
right: _x.right === undefined ? margin.right : _x.right
};
return this
};
exports.groups = function (_x) {
if (!arguments.length) return groups
groups = _x;
return this
};
// Node styles and title
exports.nodeTitle = function (_x) {
if (!arguments.length) return node.nodeTitle()
node.nodeTitle(_x);
linkTitle = linkTitleGenerator(_x, d => d.type, fmt);
return this
};
exports.nodeValue = function (_x) {
if (!arguments.length) return node.nodeValue()
node.nodeValue(_x);
return this
};
// Link styles and titles
exports.linkTitle = function (_x) {
if (!arguments.length) return linkTitle
linkTitle = _x;
return this
};
exports.linkLabel = function (_x) {
if (!arguments.length) return linkLabel
linkLabel = _x;
return this
};
exports.linkColor = function (_x) {
if (!arguments.length) return linkColor
linkColor = _x;
return this
};
exports.linkMinWidth = function (_x) {
if (!arguments.length) return link.minWidth()
link.minWidth(_x);
return this
};
exports.linkImportance = function (_x) {
if (!arguments.length) return linkImportance
linkImportance = _x;
return this
};
exports.linkImportanceAgg = function (_x) {
if (!arguments.length) return linkImportanceAgg
linkImportanceAgg = _x;
return this
};
exports.selectNode = function (_x) {
return this
};
exports.selectLink = function (_x) {
selectedEdge = _x;
return this
};
exports.on = function () {
const value = listeners.on.apply(listeners, arguments);
return value === listeners ? exports : value
};
return exports
}
function defaultLinkLabel (d) {
return null
}
function defaultLinkImportance (d) {
// Return negative values for special cases to put them at the bottom
if (!d.source || (d.target && d.target.direction === 'd')) return -2
if (!d.target || (d.source && d.source.direction === 'd')) return -1
// Return dy (visual width) for normal links
return d.dy
}
function defaultLinkImportanceAgg (values) {
// Sum of importance values for all links in the group
return values.reduce((a, b) => a + b, 0)
}
exports.sankey = sankeyLayout;
exports.sankeyDiagram = sankeyDiagram;
exports.sankeyLink = sankeyLink;
exports.sankeyLinkTitle = linkTitleGenerator;
exports.sankeyNode = sankeyNode;
exports.sankeyPositionJustified = positionNodesVertically$1;
exports.sankeyPositionRelaxation = positionNodesVertically;