d3-sankey-diagram
Version:
Sankey diagram d3 plugin
4,126 lines • 110 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-array'), require('d3-collection'), require('d3-selection'), require('d3-transition'), require('d3-dispatch'), require('d3-format'), require('d3-interpolate')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-array', 'd3-collection', 'd3-selection', 'd3-transition', 'd3-dispatch', 'd3-format', 'd3-interpolate'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {}, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3, global.d3));
})(this, (function (exports, d3Array, d3Collection, d3Selection, d3Transition, d3Dispatch, d3Format, d3Interpolate) { 'use strict';
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var DEFAULT_EDGE_NAME = "\x00";
var GRAPH_NODE = "\x00";
var EDGE_KEY_DELIM = "\x01";
// Implementation notes:
//
// * Node id query functions should return string ids for the nodes
// * Edge id query functions should return an "edgeObj", edge object, that is
// composed of enough information to uniquely identify an edge: {v, w, name}.
// * Internally we use an "edgeId", a stringified form of the edgeObj, to
// reference edges. This is because we need a performant way to look these
// edges up and, object properties, which have string keys, are the closest
// we're going to get to a performant hashtable in JavaScript.
let Graph$3 = class Graph {
_isDirected = true;
_isMultigraph = false;
_isCompound = false;
// Label for the graph itself
_label;
// Defaults to be set when creating a new node
_defaultNodeLabelFn = () => undefined;
// Defaults to be set when creating a new edge
_defaultEdgeLabelFn = () => undefined;
// v -> label
_nodes = {};
// v -> edgeObj
_in = {};
// u -> v -> Number
_preds = {};
// v -> edgeObj
_out = {};
// v -> w -> Number
_sucs = {};
// e -> edgeObj
_edgeObjs = {};
// e -> label
_edgeLabels = {};
/* Number of nodes in the graph. Should only be changed by the implementation. */
_nodeCount = 0;
/* Number of edges in the graph. Should only be changed by the implementation. */
_edgeCount = 0;
_parent;
_children;
constructor(opts) {
if (opts) {
this._isDirected = opts.hasOwnProperty("directed") ? opts.directed : true;
this._isMultigraph = opts.hasOwnProperty("multigraph") ? opts.multigraph : false;
this._isCompound = opts.hasOwnProperty("compound") ? opts.compound : false;
}
if (this._isCompound) {
// v -> parent
this._parent = {};
// v -> children
this._children = {};
this._children[GRAPH_NODE] = {};
}
}
/* === Graph functions ========= */
/**
* Whether graph was created with 'directed' flag set to true or not.
*/
isDirected() {
return this._isDirected;
}
/**
* Whether graph was created with 'multigraph' flag set to true or not.
*/
isMultigraph() {
return this._isMultigraph;
}
/**
* Whether graph was created with 'compound' flag set to true or not.
*/
isCompound() {
return this._isCompound;
}
/**
* Sets the label of the graph.
*/
setGraph(label) {
this._label = label;
return this;
}
/**
* Gets the graph label.
*/
graph() {
return this._label;
}
/* === Node functions ========== */
/**
* Sets the default node label. If newDefault is a function, it will be
* invoked ach time when setting a label for a node. Otherwise, this label
* will be assigned as default label in case if no label was specified while
* setting a node.
* Complexity: O(1).
*/
setDefaultNodeLabel(newDefault) {
this._defaultNodeLabelFn = newDefault;
if (typeof newDefault !== 'function') {
this._defaultNodeLabelFn = () => newDefault;
}
return this;
}
/**
* Gets the number of nodes in the graph.
* Complexity: O(1).
*/
nodeCount() {
return this._nodeCount;
}
/**
* Gets all nodes of the graph. Note, the in case of compound graph subnodes are
* not included in list.
* Complexity: O(1).
*/
nodes() {
return Object.keys(this._nodes);
}
/**
* Gets list of nodes without in-edges.
* Complexity: O(|V|).
*/
sources() {
var self = this;
return this.nodes().filter(v => Object.keys(self._in[v]).length === 0);
}
/**
* Gets list of nodes without out-edges.
* Complexity: O(|V|).
*/
sinks() {
var self = this;
return this.nodes().filter(v => Object.keys(self._out[v]).length === 0);
}
/**
* Invokes setNode method for each node in names list.
* Complexity: O(|names|).
*/
setNodes(vs, value) {
var args = arguments;
var self = this;
vs.forEach(function(v) {
if (args.length > 1) {
self.setNode(v, value);
} else {
self.setNode(v);
}
});
return this;
}
/**
* Creates or updates the value for the node v in the graph. If label is supplied
* it is set as the value for the node. If label is not supplied and the node was
* created by this call then the default node label will be assigned.
* Complexity: O(1).
*/
setNode(v, value) {
if (this._nodes.hasOwnProperty(v)) {
if (arguments.length > 1) {
this._nodes[v] = value;
}
return this;
}
this._nodes[v] = arguments.length > 1 ? value : this._defaultNodeLabelFn(v);
if (this._isCompound) {
this._parent[v] = GRAPH_NODE;
this._children[v] = {};
this._children[GRAPH_NODE][v] = true;
}
this._in[v] = {};
this._preds[v] = {};
this._out[v] = {};
this._sucs[v] = {};
++this._nodeCount;
return this;
}
/**
* Gets the label of node with specified name.
* Complexity: O(|V|).
*/
node(v) {
return this._nodes[v];
}
/**
* Detects whether graph has a node with specified name or not.
*/
hasNode(v) {
return this._nodes.hasOwnProperty(v);
}
/**
* Remove the node with the name from the graph or do nothing if the node is not in
* the graph. If the node was removed this function also removes any incident
* edges.
* Complexity: O(1).
*/
removeNode(v) {
var self = this;
if (this._nodes.hasOwnProperty(v)) {
var removeEdge = e => self.removeEdge(self._edgeObjs[e]);
delete this._nodes[v];
if (this._isCompound) {
this._removeFromParentsChildList(v);
delete this._parent[v];
this.children(v).forEach(function(child) {
self.setParent(child);
});
delete this._children[v];
}
Object.keys(this._in[v]).forEach(removeEdge);
delete this._in[v];
delete this._preds[v];
Object.keys(this._out[v]).forEach(removeEdge);
delete this._out[v];
delete this._sucs[v];
--this._nodeCount;
}
return this;
}
/**
* Sets node p as a parent for node v if it is defined, or removes the
* parent for v if p is undefined. Method throws an exception in case of
* invoking it in context of noncompound graph.
* Average-case complexity: O(1).
*/
setParent(v, parent) {
if (!this._isCompound) {
throw new Error("Cannot set parent in a non-compound graph");
}
if (parent === undefined) {
parent = GRAPH_NODE;
} else {
// Coerce parent to string
parent += "";
for (var ancestor = parent; ancestor !== undefined; ancestor = this.parent(ancestor)) {
if (ancestor === v) {
throw new Error("Setting " + parent+ " as parent of " + v +
" would create a cycle");
}
}
this.setNode(parent);
}
this.setNode(v);
this._removeFromParentsChildList(v);
this._parent[v] = parent;
this._children[parent][v] = true;
return this;
}
_removeFromParentsChildList(v) {
delete this._children[this._parent[v]][v];
}
/**
* Gets parent node for node v.
* Complexity: O(1).
*/
parent(v) {
if (this._isCompound) {
var parent = this._parent[v];
if (parent !== GRAPH_NODE) {
return parent;
}
}
}
/**
* Gets list of direct children of node v.
* Complexity: O(1).
*/
children(v = GRAPH_NODE) {
if (this._isCompound) {
var children = this._children[v];
if (children) {
return Object.keys(children);
}
} else if (v === GRAPH_NODE) {
return this.nodes();
} else if (this.hasNode(v)) {
return [];
}
}
/**
* Return all nodes that are predecessors of the specified node or undefined if node v is not in
* the graph. Behavior is undefined for undirected graphs - use neighbors instead.
* Complexity: O(|V|).
*/
predecessors(v) {
var predsV = this._preds[v];
if (predsV) {
return Object.keys(predsV);
}
}
/**
* Return all nodes that are successors of the specified node or undefined if node v is not in
* the graph. Behavior is undefined for undirected graphs - use neighbors instead.
* Complexity: O(|V|).
*/
successors(v) {
var sucsV = this._sucs[v];
if (sucsV) {
return Object.keys(sucsV);
}
}
/**
* Return all nodes that are predecessors or successors of the specified node or undefined if
* node v is not in the graph.
* Complexity: O(|V|).
*/
neighbors(v) {
var preds = this.predecessors(v);
if (preds) {
const union = new Set(preds);
for (var succ of this.successors(v)) {
union.add(succ);
}
return Array.from(union.values());
}
}
isLeaf(v) {
var neighbors;
if (this.isDirected()) {
neighbors = this.successors(v);
} else {
neighbors = this.neighbors(v);
}
return neighbors.length === 0;
}
/**
* Creates new graph with nodes filtered via filter. Edges incident to rejected node
* are also removed. In case of compound graph, if parent is rejected by filter,
* than all its children are rejected too.
* Average-case complexity: O(|E|+|V|).
*/
filterNodes(filter) {
var copy = new this.constructor({
directed: this._isDirected,
multigraph: this._isMultigraph,
compound: this._isCompound
});
copy.setGraph(this.graph());
var self = this;
Object.entries(this._nodes).forEach(function([v, value]) {
if (filter(v)) {
copy.setNode(v, value);
}
});
Object.values(this._edgeObjs).forEach(function(e) {
if (copy.hasNode(e.v) && copy.hasNode(e.w)) {
copy.setEdge(e, self.edge(e));
}
});
var parents = {};
function findParent(v) {
var parent = self.parent(v);
if (parent === undefined || copy.hasNode(parent)) {
parents[v] = parent;
return parent;
} else if (parent in parents) {
return parents[parent];
} else {
return findParent(parent);
}
}
if (this._isCompound) {
copy.nodes().forEach(v => copy.setParent(v, findParent(v)));
}
return copy;
}
/* === Edge functions ========== */
/**
* Sets the default edge label or factory function. This label will be
* assigned as default label in case if no label was specified while setting
* an edge or this function will be invoked each time when setting an edge
* with no label specified and returned value * will be used as a label for edge.
* Complexity: O(1).
*/
setDefaultEdgeLabel(newDefault) {
this._defaultEdgeLabelFn = newDefault;
if (typeof newDefault !== 'function') {
this._defaultEdgeLabelFn = () => newDefault;
}
return this;
}
/**
* Gets the number of edges in the graph.
* Complexity: O(1).
*/
edgeCount() {
return this._edgeCount;
}
/**
* Gets edges of the graph. In case of compound graph subgraphs are not considered.
* Complexity: O(|E|).
*/
edges() {
return Object.values(this._edgeObjs);
}
/**
* Establish an edges path over the nodes in nodes list. If some edge is already
* exists, it will update its label, otherwise it will create an edge between pair
* of nodes with label provided or default label if no label provided.
* Complexity: O(|nodes|).
*/
setPath(vs, value) {
var self = this;
var args = arguments;
vs.reduce(function(v, w) {
if (args.length > 1) {
self.setEdge(v, w, value);
} else {
self.setEdge(v, w);
}
return w;
});
return this;
}
/**
* Creates or updates the label for the edge (v, w) with the optionally supplied
* name. If label is supplied it is set as the value for the edge. If label is not
* supplied and the edge was created by this call then the default edge label will
* be assigned. The name parameter is only useful with multigraphs.
*/
setEdge() {
var v, w, name, value;
var valueSpecified = false;
var arg0 = arguments[0];
if (typeof arg0 === "object" && arg0 !== null && "v" in arg0) {
v = arg0.v;
w = arg0.w;
name = arg0.name;
if (arguments.length === 2) {
value = arguments[1];
valueSpecified = true;
}
} else {
v = arg0;
w = arguments[1];
name = arguments[3];
if (arguments.length > 2) {
value = arguments[2];
valueSpecified = true;
}
}
v = "" + v;
w = "" + w;
if (name !== undefined) {
name = "" + name;
}
var e = edgeArgsToId(this._isDirected, v, w, name);
if (this._edgeLabels.hasOwnProperty(e)) {
if (valueSpecified) {
this._edgeLabels[e] = value;
}
return this;
}
if (name !== undefined && !this._isMultigraph) {
throw new Error("Cannot set a named edge when isMultigraph = false");
}
// It didn't exist, so we need to create it.
// First ensure the nodes exist.
this.setNode(v);
this.setNode(w);
this._edgeLabels[e] = valueSpecified ? value : this._defaultEdgeLabelFn(v, w, name);
var edgeObj = edgeArgsToObj(this._isDirected, v, w, name);
// Ensure we add undirected edges in a consistent way.
v = edgeObj.v;
w = edgeObj.w;
Object.freeze(edgeObj);
this._edgeObjs[e] = edgeObj;
incrementOrInitEntry(this._preds[w], v);
incrementOrInitEntry(this._sucs[v], w);
this._in[w][e] = edgeObj;
this._out[v][e] = edgeObj;
this._edgeCount++;
return this;
}
/**
* Gets the label for the specified edge.
* Complexity: O(1).
*/
edge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
return this._edgeLabels[e];
}
/**
* Gets the label for the specified edge and converts it to an object.
* Complexity: O(1)
*/
edgeAsObj() {
const edge = this.edge(...arguments);
if (typeof edge !== "object") {
return {label: edge};
}
return edge;
}
/**
* Detects whether the graph contains specified edge or not. No subgraphs are considered.
* Complexity: O(1).
*/
hasEdge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
return this._edgeLabels.hasOwnProperty(e);
}
/**
* Removes the specified edge from the graph. No subgraphs are considered.
* Complexity: O(1).
*/
removeEdge(v, w, name) {
var e = (arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name));
var edge = this._edgeObjs[e];
if (edge) {
v = edge.v;
w = edge.w;
delete this._edgeLabels[e];
delete this._edgeObjs[e];
decrementOrRemoveEntry(this._preds[w], v);
decrementOrRemoveEntry(this._sucs[v], w);
delete this._in[w][e];
delete this._out[v][e];
this._edgeCount--;
}
return this;
}
/**
* Return all edges that point to the node v. Optionally filters those edges down to just those
* coming from node u. Behavior is undefined for undirected graphs - use nodeEdges instead.
* Complexity: O(|E|).
*/
inEdges(v, u) {
var inV = this._in[v];
if (inV) {
var edges = Object.values(inV);
if (!u) {
return edges;
}
return edges.filter(edge => edge.v === u);
}
}
/**
* Return all edges that are pointed at by node v. Optionally filters those edges down to just
* those point to w. Behavior is undefined for undirected graphs - use nodeEdges instead.
* Complexity: O(|E|).
*/
outEdges(v, w) {
var outV = this._out[v];
if (outV) {
var edges = Object.values(outV);
if (!w) {
return edges;
}
return edges.filter(edge => edge.w === w);
}
}
/**
* Returns all edges to or from node v regardless of direction. Optionally filters those edges
* down to just those between nodes v and w regardless of direction.
* Complexity: O(|E|).
*/
nodeEdges(v, w) {
var inEdges = this.inEdges(v, w);
if (inEdges) {
return inEdges.concat(this.outEdges(v, w));
}
}
};
function incrementOrInitEntry(map, k) {
if (map[k]) {
map[k]++;
} else {
map[k] = 1;
}
}
function decrementOrRemoveEntry(map, k) {
if (!--map[k]) { delete map[k]; }
}
function edgeArgsToId(isDirected, v_, w_, name) {
var v = "" + v_;
var w = "" + w_;
if (!isDirected && v > w) {
var tmp = v;
v = w;
w = tmp;
}
return v + EDGE_KEY_DELIM + w + EDGE_KEY_DELIM +
(name === undefined ? DEFAULT_EDGE_NAME : name);
}
function edgeArgsToObj(isDirected, v_, w_, name) {
var v = "" + v_;
var w = "" + w_;
if (!isDirected && v > w) {
var tmp = v;
v = w;
w = tmp;
}
var edgeObj = { v: v, w: w };
if (name) {
edgeObj.name = name;
}
return edgeObj;
}
function edgeObjToId(isDirected, edgeObj) {
return edgeArgsToId(isDirected, edgeObj.v, edgeObj.w, edgeObj.name);
}
var graph = Graph$3;
var version = '2.2.3';
// Includes only the "core" of graphlib
var lib$1 = {
Graph: graph,
version: version
};
var Graph$2 = graph;
var json = {
write: write,
read: read
};
/**
* Creates a JSON representation of the graph that can be serialized to a string with
* JSON.stringify. The graph can later be restored using json.read.
*/
function write(g) {
var json = {
options: {
directed: g.isDirected(),
multigraph: g.isMultigraph(),
compound: g.isCompound()
},
nodes: writeNodes(g),
edges: writeEdges(g)
};
if (g.graph() !== undefined) {
json.value = structuredClone(g.graph());
}
return json;
}
function writeNodes(g) {
return g.nodes().map(function(v) {
var nodeValue = g.node(v);
var parent = g.parent(v);
var node = { v: v };
if (nodeValue !== undefined) {
node.value = nodeValue;
}
if (parent !== undefined) {
node.parent = parent;
}
return node;
});
}
function writeEdges(g) {
return g.edges().map(function(e) {
var edgeValue = g.edge(e);
var edge = { v: e.v, w: e.w };
if (e.name !== undefined) {
edge.name = e.name;
}
if (edgeValue !== undefined) {
edge.value = edgeValue;
}
return edge;
});
}
/**
* Takes JSON as input and returns the graph representation.
*
* @example
* var g2 = graphlib.json.read(JSON.parse(str));
* g2.nodes();
* // ['a', 'b']
* g2.edges()
* // [ { v: 'a', w: 'b' } ]
*/
function read(json) {
var g = new Graph$2(json.options).setGraph(json.value);
json.nodes.forEach(function(entry) {
g.setNode(entry.v, entry.value);
if (entry.parent) {
g.setParent(entry.v, entry.parent);
}
});
json.edges.forEach(function(entry) {
g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value);
});
return g;
}
var components_1 = components;
function components(g) {
var visited = {};
var cmpts = [];
var cmpt;
function dfs(v) {
if (visited.hasOwnProperty(v)) return;
visited[v] = true;
cmpt.push(v);
g.successors(v).forEach(dfs);
g.predecessors(v).forEach(dfs);
}
g.nodes().forEach(function(v) {
cmpt = [];
dfs(v);
if (cmpt.length) {
cmpts.push(cmpt);
}
});
return cmpts;
}
/**
* A min-priority queue data structure. This algorithm is derived from Cormen,
* et al., "Introduction to Algorithms". The basic idea of a min-priority
* queue is that you can efficiently (in O(1) time) get the smallest key in
* the queue. Adding and removing elements takes O(log n) time. A key can
* have its priority decreased in O(log n) time.
*/
let PriorityQueue$2 = class PriorityQueue {
_arr = [];
_keyIndices = {};
/**
* Returns the number of elements in the queue. Takes `O(1)` time.
*/
size() {
return this._arr.length;
}
/**
* Returns the keys that are in the queue. Takes `O(n)` time.
*/
keys() {
return this._arr.map(function(x) { return x.key; });
}
/**
* Returns `true` if **key** is in the queue and `false` if not.
*/
has(key) {
return this._keyIndices.hasOwnProperty(key);
}
/**
* Returns the priority for **key**. If **key** is not present in the queue
* then this function returns `undefined`. Takes `O(1)` time.
*
* @param {Object} key
*/
priority(key) {
var index = this._keyIndices[key];
if (index !== undefined) {
return this._arr[index].priority;
}
}
/**
* Returns the key for the minimum element in this queue. If the queue is
* empty this function throws an Error. Takes `O(1)` time.
*/
min() {
if (this.size() === 0) {
throw new Error("Queue underflow");
}
return this._arr[0].key;
}
/**
* Inserts a new key into the priority queue. If the key already exists in
* the queue this function returns `false`; otherwise it will return `true`.
* Takes `O(n)` time.
*
* @param {Object} key the key to add
* @param {Number} priority the initial priority for the key
*/
add(key, priority) {
var keyIndices = this._keyIndices;
key = String(key);
if (!keyIndices.hasOwnProperty(key)) {
var arr = this._arr;
var index = arr.length;
keyIndices[key] = index;
arr.push({key: key, priority: priority});
this._decrease(index);
return true;
}
return false;
}
/**
* Removes and returns the smallest key in the queue. Takes `O(log n)` time.
*/
removeMin() {
this._swap(0, this._arr.length - 1);
var min = this._arr.pop();
delete this._keyIndices[min.key];
this._heapify(0);
return min.key;
}
/**
* Decreases the priority for **key** to **priority**. If the new priority is
* greater than the previous priority, this function will throw an Error.
*
* @param {Object} key the key for which to raise priority
* @param {Number} priority the new priority for the key
*/
decrease(key, priority) {
var index = this._keyIndices[key];
if (priority > this._arr[index].priority) {
throw new Error("New priority is greater than current priority. " +
"Key: " + key + " Old: " + this._arr[index].priority + " New: " + priority);
}
this._arr[index].priority = priority;
this._decrease(index);
}
_heapify(i) {
var arr = this._arr;
var l = 2 * i;
var r = l + 1;
var largest = i;
if (l < arr.length) {
largest = arr[l].priority < arr[largest].priority ? l : largest;
if (r < arr.length) {
largest = arr[r].priority < arr[largest].priority ? r : largest;
}
if (largest !== i) {
this._swap(i, largest);
this._heapify(largest);
}
}
}
_decrease(index) {
var arr = this._arr;
var priority = arr[index].priority;
var parent;
while (index !== 0) {
parent = index >> 1;
if (arr[parent].priority < priority) {
break;
}
this._swap(index, parent);
index = parent;
}
}
_swap(i, j) {
var arr = this._arr;
var keyIndices = this._keyIndices;
var origArrI = arr[i];
var origArrJ = arr[j];
arr[i] = origArrJ;
arr[j] = origArrI;
keyIndices[origArrJ.key] = i;
keyIndices[origArrI.key] = j;
}
};
var priorityQueue = PriorityQueue$2;
var PriorityQueue$1 = priorityQueue;
var dijkstra_1 = dijkstra$1;
var DEFAULT_WEIGHT_FUNC$1 = () => 1;
function dijkstra$1(g, source, weightFn, edgeFn) {
return runDijkstra(g, String(source),
weightFn || DEFAULT_WEIGHT_FUNC$1,
edgeFn || function(v) { return g.outEdges(v); });
}
function runDijkstra(g, source, weightFn, edgeFn) {
var results = {};
var pq = new PriorityQueue$1();
var v, vEntry;
var updateNeighbors = function(edge) {
var w = edge.v !== v ? edge.v : edge.w;
var wEntry = results[w];
var weight = weightFn(edge);
var distance = vEntry.distance + weight;
if (weight < 0) {
throw new Error("dijkstra does not allow negative edge weights. " +
"Bad edge: " + edge + " Weight: " + weight);
}
if (distance < wEntry.distance) {
wEntry.distance = distance;
wEntry.predecessor = v;
pq.decrease(w, distance);
}
};
g.nodes().forEach(function(v) {
var distance = v === source ? 0 : Number.POSITIVE_INFINITY;
results[v] = { distance: distance };
pq.add(v, distance);
});
while (pq.size() > 0) {
v = pq.removeMin();
vEntry = results[v];
if (vEntry.distance === Number.POSITIVE_INFINITY) {
break;
}
edgeFn(v).forEach(updateNeighbors);
}
return results;
}
var dijkstra = dijkstra_1;
var dijkstraAll_1 = dijkstraAll;
function dijkstraAll(g, weightFunc, edgeFunc) {
return g.nodes().reduce(function(acc, v) {
acc[v] = dijkstra(g, v, weightFunc, edgeFunc);
return acc;
}, {});
}
var tarjan_1 = tarjan$1;
function tarjan$1(g) {
var index = 0;
var stack = [];
var visited = {}; // node id -> { onStack, lowlink, index }
var results = [];
function dfs(v) {
var entry = visited[v] = {
onStack: true,
lowlink: index,
index: index++
};
stack.push(v);
g.successors(v).forEach(function(w) {
if (!visited.hasOwnProperty(w)) {
dfs(w);
entry.lowlink = Math.min(entry.lowlink, visited[w].lowlink);
} else if (visited[w].onStack) {
entry.lowlink = Math.min(entry.lowlink, visited[w].index);
}
});
if (entry.lowlink === entry.index) {
var cmpt = [];
var w;
do {
w = stack.pop();
visited[w].onStack = false;
cmpt.push(w);
} while (v !== w);
results.push(cmpt);
}
}
g.nodes().forEach(function(v) {
if (!visited.hasOwnProperty(v)) {
dfs(v);
}
});
return results;
}
var tarjan = tarjan_1;
var findCycles_1 = findCycles;
function findCycles(g) {
return tarjan(g).filter(function(cmpt) {
return cmpt.length > 1 || (cmpt.length === 1 && g.hasEdge(cmpt[0], cmpt[0]));
});
}
var floydWarshall_1 = floydWarshall;
var DEFAULT_WEIGHT_FUNC = () => 1;
function floydWarshall(g, weightFn, edgeFn) {
return runFloydWarshall(g,
weightFn || DEFAULT_WEIGHT_FUNC,
edgeFn || function(v) { return g.outEdges(v); });
}
function runFloydWarshall(g, weightFn, edgeFn) {
var results = {};
var nodes = g.nodes();
nodes.forEach(function(v) {
results[v] = {};
results[v][v] = { distance: 0 };
nodes.forEach(function(w) {
if (v !== w) {
results[v][w] = { distance: Number.POSITIVE_INFINITY };
}
});
edgeFn(v).forEach(function(edge) {
var w = edge.v === v ? edge.w : edge.v;
var d = weightFn(edge);
results[v][w] = { distance: d, predecessor: v };
});
});
nodes.forEach(function(k) {
var rowK = results[k];
nodes.forEach(function(i) {
var rowI = results[i];
nodes.forEach(function(j) {
var ik = rowI[k];
var kj = rowK[j];
var ij = rowI[j];
var altDistance = ik.distance + kj.distance;
if (altDistance < ij.distance) {
ij.distance = altDistance;
ij.predecessor = kj.predecessor;
}
});
});
});
return results;
}
function topsort$1(g) {
var visited = {};
var stack = {};
var results = [];
function visit(node) {
if (stack.hasOwnProperty(node)) {
throw new CycleException();
}
if (!visited.hasOwnProperty(node)) {
stack[node] = true;
visited[node] = true;
g.predecessors(node).forEach(visit);
delete stack[node];
results.push(node);
}
}
g.sinks().forEach(visit);
if (Object.keys(visited).length !== g.nodeCount()) {
throw new CycleException();
}
return results;
}
class CycleException extends Error {
constructor() {
super(...arguments);
}
}
var topsort_1 = topsort$1;
topsort$1.CycleException = CycleException;
var topsort = topsort_1;
var isAcyclic_1 = isAcyclic;
function isAcyclic(g) {
try {
topsort(g);
} catch (e) {
if (e instanceof topsort.CycleException) {
return false;
}
throw e;
}
return true;
}
var dfs_1 = dfs$2;
/*
* A helper that preforms a pre- or post-order traversal on the input graph
* and returns the nodes in the order they were visited. If the graph is
* undirected then this algorithm will navigate using neighbors. If the graph
* is directed then this algorithm will navigate using successors.
*
* If the order is not "post", it will be treated as "pre".
*/
function dfs$2(g, vs, order) {
if (!Array.isArray(vs)) {
vs = [vs];
}
var navigation = g.isDirected() ? v => g.successors(v) : v => g.neighbors(v);
var orderFunc = order === "post" ? postOrderDfs : preOrderDfs;
var acc = [];
var visited = {};
vs.forEach(v => {
if (!g.hasNode(v)) {
throw new Error("Graph does not have node: " + v);
}
orderFunc(v, navigation, visited, acc);
});
return acc;
}
function postOrderDfs(v, navigation, visited, acc) {
var stack = [[v, false]];
while (stack.length > 0) {
var curr = stack.pop();
if (curr[1]) {
acc.push(curr[0]);
} else {
if (!visited.hasOwnProperty(curr[0])) {
visited[curr[0]] = true;
stack.push([curr[0], true]);
forEachRight(navigation(curr[0]), w => stack.push([w, false]));
}
}
}
}
function preOrderDfs(v, navigation, visited, acc) {
var stack = [v];
while (stack.length > 0) {
var curr = stack.pop();
if (!visited.hasOwnProperty(curr)) {
visited[curr] = true;
acc.push(curr);
forEachRight(navigation(curr), w => stack.push(w));
}
}
}
function forEachRight(array, iteratee) {
var length = array.length;
while (length--) {
iteratee(array[length], length, array);
}
return array;
}
var dfs$1 = dfs_1;
var postorder_1 = postorder;
function postorder(g, vs) {
return dfs$1(g, vs, "post");
}
var dfs = dfs_1;
var preorder_1 = preorder;
function preorder(g, vs) {
return dfs(g, vs, "pre");
}
var Graph$1 = graph;
var PriorityQueue = priorityQueue;
var prim_1 = prim;
function prim(g, weightFunc) {
var result = new Graph$1();
var parents = {};
var pq = new PriorityQueue();
var v;
function updateNeighbors(edge) {
var w = edge.v === v ? edge.w : edge.v;
var pri = pq.priority(w);
if (pri !== undefined) {
var edgeWeight = weightFunc(edge);
if (edgeWeight < pri) {
parents[w] = v;
pq.decrease(w, edgeWeight);
}
}
}
if (g.nodeCount() === 0) {
return result;
}
g.nodes().forEach(function(v) {
pq.add(v, Number.POSITIVE_INFINITY);
result.setNode(v);
});
// Start from an arbitrary node
pq.decrease(g.nodes()[0], 0);
var init = false;
while (pq.size() > 0) {
v = pq.removeMin();
if (parents.hasOwnProperty(v)) {
result.setEdge(v, parents[v]);
} else if (init) {
throw new Error("Input graph is not connected: " + g);
} else {
init = true;
}
g.nodeEdges(v).forEach(updateNeighbors);
}
return result;
}
var alg$1 = {
components: components_1,
dijkstra: dijkstra_1,
dijkstraAll: dijkstraAll_1,
findCycles: findCycles_1,
floydWarshall: floydWarshall_1,
isAcyclic: isAcyclic_1,
postorder: postorder_1,
preorder: preorder_1,
prim: prim_1,
tarjan: tarjan_1,
topsort: topsort_1
};
/**
* Copyright (c) 2014, Chris Pettitt
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var lib = lib$1;
var graphlib = {
Graph: lib.Graph,
json: json,
alg: alg$1,
version: lib.version
};
var pkg = /*@__PURE__*/getDefaultExportFromCjs(graphlib);
/**
* 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 graphlib.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 graphlib.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;
}));