@dagrejs/dagre 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +10 -2
- package/lib/acyclic.js +10 -18
- package/lib/add-border-segments.js +19 -11
- package/lib/coordinate-system.js +5 -15
- package/lib/data/list.js +8 -7
- package/lib/debug.js +25 -14
- package/lib/greedy-fas.js +35 -30
- package/lib/index.js +38 -0
- package/lib/layout.js +105 -102
- package/lib/nesting-graph.js +18 -21
- package/lib/normalize.js +22 -18
- package/lib/order/add-subgraph-constraints.js +6 -2
- package/lib/order/barycenter.js +14 -6
- package/lib/order/build-layer-graph.js +19 -13
- package/lib/order/cross-count.js +13 -10
- package/lib/order/index.js +33 -24
- package/lib/order/init-order.js +8 -7
- package/lib/order/resolve-conflicts.js +9 -19
- package/lib/order/sort-subgraph.js +16 -22
- package/lib/order/sort.js +13 -12
- package/lib/parent-dummy-chains.js +17 -19
- package/lib/position/bk.js +42 -84
- package/lib/position/index.js +10 -9
- package/lib/rank/feasible-tree.js +14 -17
- package/lib/rank/index.js +25 -15
- package/lib/rank/network-simplex.js +18 -39
- package/lib/rank/util.js +6 -12
- package/lib/util.js +42 -57
- package/lib/version.js +8 -1
- package/mjs-lib/acyclic.js +62 -0
- package/mjs-lib/add-border-segments.js +35 -0
- package/mjs-lib/coordinate-system.js +65 -0
- package/mjs-lib/data/list.js +56 -0
- package/mjs-lib/debug.js +30 -0
- package/mjs-lib/greedy-fas.js +125 -0
- package/mjs-lib/index.js +9 -0
- package/mjs-lib/layout.js +405 -0
- package/mjs-lib/nesting-graph.js +120 -0
- package/mjs-lib/normalize.js +84 -0
- package/mjs-lib/order/add-subgraph-constraints.js +49 -0
- package/mjs-lib/order/barycenter.js +24 -0
- package/mjs-lib/order/build-layer-graph.js +71 -0
- package/mjs-lib/order/cross-count.js +64 -0
- package/mjs-lib/order/index.js +70 -0
- package/mjs-lib/order/init-order.js +34 -0
- package/mjs-lib/order/resolve-conflicts.js +116 -0
- package/mjs-lib/order/sort-subgraph.js +71 -0
- package/mjs-lib/order/sort.js +54 -0
- package/mjs-lib/parent-dummy-chains.js +82 -0
- package/mjs-lib/position/bk.js +409 -0
- package/mjs-lib/position/index.js +30 -0
- package/mjs-lib/rank/feasible-tree.js +93 -0
- package/mjs-lib/rank/index.js +46 -0
- package/mjs-lib/rank/network-simplex.js +233 -0
- package/mjs-lib/rank/util.js +58 -0
- package/mjs-lib/util.js +305 -0
- package/mjs-lib/version.js +1 -0
- package/package.json +14 -3
package/mjs-lib/debug.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as util from "./util.js";
|
|
2
|
+
|
|
3
|
+
// web-devs write a <script type="importmap"> to map
|
|
4
|
+
// nodejs paths to actual http://paths
|
|
5
|
+
import { Graph as Graph } from "@dagrejs/graphlib";
|
|
6
|
+
|
|
7
|
+
/* istanbul ignore next */
|
|
8
|
+
export default function debugOrdering(g) {
|
|
9
|
+
let layerMatrix = util.buildLayerMatrix(g);
|
|
10
|
+
|
|
11
|
+
let h = new Graph({ compound: true, multigraph: true }).setGraph({});
|
|
12
|
+
|
|
13
|
+
g.nodes().forEach(v => {
|
|
14
|
+
h.setNode(v, { label: v });
|
|
15
|
+
h.setParent(v, "layer" + g.node(v).rank);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
g.edges().forEach(e => h.setEdge(e.v, e.w, {}, e.name));
|
|
19
|
+
|
|
20
|
+
layerMatrix.forEach((layer, i) => {
|
|
21
|
+
let layerV = "layer" + i;
|
|
22
|
+
h.setNode(layerV, { rank: "same" });
|
|
23
|
+
layer.reduce((u, v) => {
|
|
24
|
+
h.setEdge(u, v, { style: "invis" });
|
|
25
|
+
return v;
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return h;
|
|
30
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// web-devs write a <script type="importmap"> to map
|
|
2
|
+
// nodejs paths to actual http://paths
|
|
3
|
+
import { Graph as Graph } from "@dagrejs/graphlib";
|
|
4
|
+
import { default as List } from "./data/list.js";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A greedy heuristic for finding a feedback arc set for a graph. A feedback
|
|
8
|
+
* arc set is a set of edges that can be removed to make a graph acyclic.
|
|
9
|
+
* The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and
|
|
10
|
+
* effective heuristic for the feedback arc set problem." This implementation
|
|
11
|
+
* adjusts that from the paper to allow for weighted edges.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let DEFAULT_WEIGHT_FN = () => 1;
|
|
15
|
+
|
|
16
|
+
export default function greedyFAS(g, weightFn) {
|
|
17
|
+
if (g.nodeCount() <= 1) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
let state = buildState(g, weightFn || DEFAULT_WEIGHT_FN);
|
|
21
|
+
let results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx);
|
|
22
|
+
|
|
23
|
+
// Expand multi-edges
|
|
24
|
+
return results.flatMap(e => g.outEdges(e.v, e.w));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function doGreedyFAS(g, buckets, zeroIdx) {
|
|
28
|
+
let results = [];
|
|
29
|
+
let sources = buckets[buckets.length - 1];
|
|
30
|
+
let sinks = buckets[0];
|
|
31
|
+
|
|
32
|
+
let entry;
|
|
33
|
+
while (g.nodeCount()) {
|
|
34
|
+
while ((entry = sinks.dequeue())) { removeNode(g, buckets, zeroIdx, entry); }
|
|
35
|
+
while ((entry = sources.dequeue())) { removeNode(g, buckets, zeroIdx, entry); }
|
|
36
|
+
if (g.nodeCount()) {
|
|
37
|
+
for (let i = buckets.length - 2; i > 0; --i) {
|
|
38
|
+
entry = buckets[i].dequeue();
|
|
39
|
+
if (entry) {
|
|
40
|
+
results = results.concat(removeNode(g, buckets, zeroIdx, entry, true));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeNode(g, buckets, zeroIdx, entry, collectPredecessors) {
|
|
51
|
+
let results = collectPredecessors ? [] : undefined;
|
|
52
|
+
|
|
53
|
+
g.inEdges(entry.v).forEach(edge => {
|
|
54
|
+
let weight = g.edge(edge);
|
|
55
|
+
let uEntry = g.node(edge.v);
|
|
56
|
+
|
|
57
|
+
if (collectPredecessors) {
|
|
58
|
+
results.push({ v: edge.v, w: edge.w });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
uEntry.out -= weight;
|
|
62
|
+
assignBucket(buckets, zeroIdx, uEntry);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
g.outEdges(entry.v).forEach(edge => {
|
|
66
|
+
let weight = g.edge(edge);
|
|
67
|
+
let w = edge.w;
|
|
68
|
+
let wEntry = g.node(w);
|
|
69
|
+
wEntry["in"] -= weight;
|
|
70
|
+
assignBucket(buckets, zeroIdx, wEntry);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
g.removeNode(entry.v);
|
|
74
|
+
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildState(g, weightFn) {
|
|
79
|
+
let fasGraph = new Graph();
|
|
80
|
+
let maxIn = 0;
|
|
81
|
+
let maxOut = 0;
|
|
82
|
+
|
|
83
|
+
g.nodes().forEach(v => {
|
|
84
|
+
fasGraph.setNode(v, { v: v, "in": 0, out: 0 });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Aggregate weights on nodes, but also sum the weights across multi-edges
|
|
88
|
+
// into a single edge for the fasGraph.
|
|
89
|
+
g.edges().forEach(e => {
|
|
90
|
+
let prevWeight = fasGraph.edge(e.v, e.w) || 0;
|
|
91
|
+
let weight = weightFn(e);
|
|
92
|
+
let edgeWeight = prevWeight + weight;
|
|
93
|
+
fasGraph.setEdge(e.v, e.w, edgeWeight);
|
|
94
|
+
maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight);
|
|
95
|
+
maxIn = Math.max(maxIn, fasGraph.node(e.w)["in"] += weight);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let buckets = range(maxOut + maxIn + 3).map(() => new List());
|
|
99
|
+
let zeroIdx = maxIn + 1;
|
|
100
|
+
|
|
101
|
+
fasGraph.nodes().forEach(v => {
|
|
102
|
+
assignBucket(buckets, zeroIdx, fasGraph.node(v));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assignBucket(buckets, zeroIdx, entry) {
|
|
109
|
+
if (!entry.out) {
|
|
110
|
+
buckets[0].enqueue(entry);
|
|
111
|
+
} else if (!entry["in"]) {
|
|
112
|
+
buckets[buckets.length - 1].enqueue(entry);
|
|
113
|
+
} else {
|
|
114
|
+
buckets[entry.out - entry["in"] + zeroIdx].enqueue(entry);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function range(limit) {
|
|
119
|
+
const range = [];
|
|
120
|
+
for (let i = 0; i < limit; i++) {
|
|
121
|
+
range.push(i);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return range;
|
|
125
|
+
}
|
package/mjs-lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as graphlib from "@dagrejs/graphlib";
|
|
2
|
+
export { default as layout } from "./layout.js";
|
|
3
|
+
export { default as debug } from "./debug.js";
|
|
4
|
+
import { time, notime } from "./util.js";
|
|
5
|
+
export const util = {
|
|
6
|
+
time: time,
|
|
7
|
+
notime: notime
|
|
8
|
+
};
|
|
9
|
+
export { default as version } from "./version.js";
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import * as acyclic from "./acyclic.js";
|
|
4
|
+
import * as normalize from "./normalize.js";
|
|
5
|
+
import { default as rank } from "./rank/index.js";
|
|
6
|
+
import { default as parentDummyChains } from "./parent-dummy-chains.js";
|
|
7
|
+
import * as nestingGraph from "./nesting-graph.js";
|
|
8
|
+
import { default as addBorderSegments } from "./add-border-segments.js";
|
|
9
|
+
import * as coordinateSystem from "./coordinate-system.js";
|
|
10
|
+
import { default as order } from "./order/index.js";
|
|
11
|
+
import { default as position } from "./position/index.js";
|
|
12
|
+
import * as util from "./util.js";
|
|
13
|
+
const removeEmptyRanks = util.removeEmptyRanks;
|
|
14
|
+
const normalizeRanks = util.normalizeRanks;
|
|
15
|
+
import { Graph as Graph } from "@dagrejs/graphlib";
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export default function layout(g, opts) {
|
|
20
|
+
let time = opts && opts.debugTiming ? util.time : util.notime;
|
|
21
|
+
time("layout", () => {
|
|
22
|
+
let layoutGraph =
|
|
23
|
+
time(" buildLayoutGraph", () => buildLayoutGraph(g));
|
|
24
|
+
time(" runLayout", () => runLayout(layoutGraph, time));
|
|
25
|
+
time(" updateInputGraph", () => updateInputGraph(g, layoutGraph));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runLayout(g, time) {
|
|
30
|
+
time(" makeSpaceForEdgeLabels", () => makeSpaceForEdgeLabels(g));
|
|
31
|
+
time(" removeSelfEdges", () => removeSelfEdges(g));
|
|
32
|
+
time(" acyclic", () => acyclic.run(g));
|
|
33
|
+
time(" nestingGraph.run", () => nestingGraph.run(g));
|
|
34
|
+
time(" rank", () => rank(util.asNonCompoundGraph(g)));
|
|
35
|
+
time(" injectEdgeLabelProxies", () => injectEdgeLabelProxies(g));
|
|
36
|
+
time(" removeEmptyRanks", () => removeEmptyRanks(g));
|
|
37
|
+
time(" nestingGraph.cleanup", () => nestingGraph.cleanup(g));
|
|
38
|
+
time(" normalizeRanks", () => normalizeRanks(g));
|
|
39
|
+
time(" assignRankMinMax", () => assignRankMinMax(g));
|
|
40
|
+
time(" removeEdgeLabelProxies", () => removeEdgeLabelProxies(g));
|
|
41
|
+
time(" normalize.run", () => normalize.run(g));
|
|
42
|
+
time(" parentDummyChains", () => parentDummyChains(g));
|
|
43
|
+
time(" addBorderSegments", () => addBorderSegments(g));
|
|
44
|
+
time(" order", () => order(g));
|
|
45
|
+
time(" insertSelfEdges", () => insertSelfEdges(g));
|
|
46
|
+
time(" adjustCoordinateSystem", () => coordinateSystem.adjust(g));
|
|
47
|
+
time(" position", () => position(g));
|
|
48
|
+
time(" positionSelfEdges", () => positionSelfEdges(g));
|
|
49
|
+
time(" removeBorderNodes", () => removeBorderNodes(g));
|
|
50
|
+
time(" normalize.undo", () => normalize.undo(g));
|
|
51
|
+
time(" fixupEdgeLabelCoords", () => fixupEdgeLabelCoords(g));
|
|
52
|
+
time(" undoCoordinateSystem", () => coordinateSystem.undo(g));
|
|
53
|
+
time(" translateGraph", () => translateGraph(g));
|
|
54
|
+
time(" assignNodeIntersects", () => assignNodeIntersects(g));
|
|
55
|
+
time(" reversePoints", () => reversePointsForReversedEdges(g));
|
|
56
|
+
time(" acyclic.undo", () => acyclic.undo(g));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Copies final layout information from the layout graph back to the input
|
|
61
|
+
* graph. This process only copies whitelisted attributes from the layout graph
|
|
62
|
+
* to the input graph, so it serves as a good place to determine what
|
|
63
|
+
* attributes can influence layout.
|
|
64
|
+
*/
|
|
65
|
+
function updateInputGraph(inputGraph, layoutGraph) {
|
|
66
|
+
inputGraph.nodes().forEach(v => {
|
|
67
|
+
let inputLabel = inputGraph.node(v);
|
|
68
|
+
let layoutLabel = layoutGraph.node(v);
|
|
69
|
+
|
|
70
|
+
if (inputLabel) {
|
|
71
|
+
inputLabel.x = layoutLabel.x;
|
|
72
|
+
inputLabel.y = layoutLabel.y;
|
|
73
|
+
inputLabel.rank = layoutLabel.rank;
|
|
74
|
+
|
|
75
|
+
if (layoutGraph.children(v).length) {
|
|
76
|
+
inputLabel.width = layoutLabel.width;
|
|
77
|
+
inputLabel.height = layoutLabel.height;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
inputGraph.edges().forEach(e => {
|
|
83
|
+
let inputLabel = inputGraph.edge(e);
|
|
84
|
+
let layoutLabel = layoutGraph.edge(e);
|
|
85
|
+
|
|
86
|
+
inputLabel.points = layoutLabel.points;
|
|
87
|
+
if (layoutLabel.hasOwnProperty("x")) {
|
|
88
|
+
inputLabel.x = layoutLabel.x;
|
|
89
|
+
inputLabel.y = layoutLabel.y;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
inputGraph.graph().width = layoutGraph.graph().width;
|
|
94
|
+
inputGraph.graph().height = layoutGraph.graph().height;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let graphNumAttrs = ["nodesep", "edgesep", "ranksep", "marginx", "marginy"];
|
|
98
|
+
let graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: "tb" };
|
|
99
|
+
let graphAttrs = ["acyclicer", "ranker", "rankdir", "align"];
|
|
100
|
+
let nodeNumAttrs = ["width", "height"];
|
|
101
|
+
let nodeDefaults = { width: 0, height: 0 };
|
|
102
|
+
let edgeNumAttrs = ["minlen", "weight", "width", "height", "labeloffset"];
|
|
103
|
+
let edgeDefaults = {
|
|
104
|
+
minlen: 1, weight: 1, width: 0, height: 0,
|
|
105
|
+
labeloffset: 10, labelpos: "r"
|
|
106
|
+
};
|
|
107
|
+
let edgeAttrs = ["labelpos"];
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* Constructs a new graph from the input graph, which can be used for layout.
|
|
111
|
+
* This process copies only whitelisted attributes from the input graph to the
|
|
112
|
+
* layout graph. Thus this function serves as a good place to determine what
|
|
113
|
+
* attributes can influence layout.
|
|
114
|
+
*/
|
|
115
|
+
function buildLayoutGraph(inputGraph) {
|
|
116
|
+
let g = new Graph({ multigraph: true, compound: true });
|
|
117
|
+
let graph = canonicalize(inputGraph.graph());
|
|
118
|
+
|
|
119
|
+
g.setGraph(Object.assign({},
|
|
120
|
+
graphDefaults,
|
|
121
|
+
selectNumberAttrs(graph, graphNumAttrs),
|
|
122
|
+
util.pick(graph, graphAttrs)));
|
|
123
|
+
|
|
124
|
+
inputGraph.nodes().forEach(v => {
|
|
125
|
+
let node = canonicalize(inputGraph.node(v));
|
|
126
|
+
const newNode = selectNumberAttrs(node, nodeNumAttrs);
|
|
127
|
+
Object.keys(nodeDefaults).forEach(k => {
|
|
128
|
+
if (newNode[k] === undefined) {
|
|
129
|
+
newNode[k] = nodeDefaults[k];
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
g.setNode(v, newNode);
|
|
134
|
+
g.setParent(v, inputGraph.parent(v));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
inputGraph.edges().forEach(e => {
|
|
138
|
+
let edge = canonicalize(inputGraph.edge(e));
|
|
139
|
+
g.setEdge(e, Object.assign({},
|
|
140
|
+
edgeDefaults,
|
|
141
|
+
selectNumberAttrs(edge, edgeNumAttrs),
|
|
142
|
+
util.pick(edge, edgeAttrs)));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return g;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* This idea comes from the Gansner paper: to account for edge labels in our
|
|
150
|
+
* layout we split each rank in half by doubling minlen and halving ranksep.
|
|
151
|
+
* Then we can place labels at these mid-points between nodes.
|
|
152
|
+
*
|
|
153
|
+
* We also add some minimal padding to the width to push the label for the edge
|
|
154
|
+
* away from the edge itself a bit.
|
|
155
|
+
*/
|
|
156
|
+
function makeSpaceForEdgeLabels(g) {
|
|
157
|
+
let graph = g.graph();
|
|
158
|
+
graph.ranksep /= 2;
|
|
159
|
+
g.edges().forEach(e => {
|
|
160
|
+
let edge = g.edge(e);
|
|
161
|
+
edge.minlen *= 2;
|
|
162
|
+
if (edge.labelpos.toLowerCase() !== "c") {
|
|
163
|
+
if (graph.rankdir === "TB" || graph.rankdir === "BT") {
|
|
164
|
+
edge.width += edge.labeloffset;
|
|
165
|
+
} else {
|
|
166
|
+
edge.height += edge.labeloffset;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Creates temporary dummy nodes that capture the rank in which each edge's
|
|
174
|
+
* label is going to, if it has one of non-zero width and height. We do this
|
|
175
|
+
* so that we can safely remove empty ranks while preserving balance for the
|
|
176
|
+
* label's position.
|
|
177
|
+
*/
|
|
178
|
+
function injectEdgeLabelProxies(g) {
|
|
179
|
+
g.edges().forEach(e => {
|
|
180
|
+
let edge = g.edge(e);
|
|
181
|
+
if (edge.width && edge.height) {
|
|
182
|
+
let v = g.node(e.v);
|
|
183
|
+
let w = g.node(e.w);
|
|
184
|
+
let label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e };
|
|
185
|
+
util.addDummyNode(g, "edge-proxy", label, "_ep");
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function assignRankMinMax(g) {
|
|
191
|
+
let maxRank = 0;
|
|
192
|
+
g.nodes().forEach(v => {
|
|
193
|
+
let node = g.node(v);
|
|
194
|
+
if (node.borderTop) {
|
|
195
|
+
node.minRank = g.node(node.borderTop).rank;
|
|
196
|
+
node.maxRank = g.node(node.borderBottom).rank;
|
|
197
|
+
maxRank = Math.max(maxRank, node.maxRank);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
g.graph().maxRank = maxRank;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function removeEdgeLabelProxies(g) {
|
|
204
|
+
g.nodes().forEach(v => {
|
|
205
|
+
let node = g.node(v);
|
|
206
|
+
if (node.dummy === "edge-proxy") {
|
|
207
|
+
g.edge(node.e).labelRank = node.rank;
|
|
208
|
+
g.removeNode(v);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function translateGraph(g) {
|
|
214
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
215
|
+
let maxX = 0;
|
|
216
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
217
|
+
let maxY = 0;
|
|
218
|
+
let graphLabel = g.graph();
|
|
219
|
+
let marginX = graphLabel.marginx || 0;
|
|
220
|
+
let marginY = graphLabel.marginy || 0;
|
|
221
|
+
|
|
222
|
+
function getExtremes(attrs) {
|
|
223
|
+
let x = attrs.x;
|
|
224
|
+
let y = attrs.y;
|
|
225
|
+
let w = attrs.width;
|
|
226
|
+
let h = attrs.height;
|
|
227
|
+
minX = Math.min(minX, x - w / 2);
|
|
228
|
+
maxX = Math.max(maxX, x + w / 2);
|
|
229
|
+
minY = Math.min(minY, y - h / 2);
|
|
230
|
+
maxY = Math.max(maxY, y + h / 2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
g.nodes().forEach(v => getExtremes(g.node(v)));
|
|
234
|
+
g.edges().forEach(e => {
|
|
235
|
+
let edge = g.edge(e);
|
|
236
|
+
if (edge.hasOwnProperty("x")) {
|
|
237
|
+
getExtremes(edge);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
minX -= marginX;
|
|
242
|
+
minY -= marginY;
|
|
243
|
+
|
|
244
|
+
g.nodes().forEach(v => {
|
|
245
|
+
let node = g.node(v);
|
|
246
|
+
node.x -= minX;
|
|
247
|
+
node.y -= minY;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
g.edges().forEach(e => {
|
|
251
|
+
let edge = g.edge(e);
|
|
252
|
+
edge.points.forEach(p => {
|
|
253
|
+
p.x -= minX;
|
|
254
|
+
p.y -= minY;
|
|
255
|
+
});
|
|
256
|
+
if (edge.hasOwnProperty("x")) { edge.x -= minX; }
|
|
257
|
+
if (edge.hasOwnProperty("y")) { edge.y -= minY; }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
graphLabel.width = maxX - minX + marginX;
|
|
261
|
+
graphLabel.height = maxY - minY + marginY;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function assignNodeIntersects(g) {
|
|
265
|
+
g.edges().forEach(e => {
|
|
266
|
+
let edge = g.edge(e);
|
|
267
|
+
let nodeV = g.node(e.v);
|
|
268
|
+
let nodeW = g.node(e.w);
|
|
269
|
+
let p1, p2;
|
|
270
|
+
if (!edge.points) {
|
|
271
|
+
edge.points = [];
|
|
272
|
+
p1 = nodeW;
|
|
273
|
+
p2 = nodeV;
|
|
274
|
+
} else {
|
|
275
|
+
p1 = edge.points[0];
|
|
276
|
+
p2 = edge.points[edge.points.length - 1];
|
|
277
|
+
}
|
|
278
|
+
edge.points.unshift(util.intersectRect(nodeV, p1));
|
|
279
|
+
edge.points.push(util.intersectRect(nodeW, p2));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fixupEdgeLabelCoords(g) {
|
|
284
|
+
g.edges().forEach(e => {
|
|
285
|
+
let edge = g.edge(e);
|
|
286
|
+
if (edge.hasOwnProperty("x")) {
|
|
287
|
+
if (edge.labelpos === "l" || edge.labelpos === "r") {
|
|
288
|
+
edge.width -= edge.labeloffset;
|
|
289
|
+
}
|
|
290
|
+
switch (edge.labelpos) {
|
|
291
|
+
case "l": edge.x -= edge.width / 2 + edge.labeloffset; break;
|
|
292
|
+
case "r": edge.x += edge.width / 2 + edge.labeloffset; break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function reversePointsForReversedEdges(g) {
|
|
299
|
+
g.edges().forEach(e => {
|
|
300
|
+
let edge = g.edge(e);
|
|
301
|
+
if (edge.reversed) {
|
|
302
|
+
edge.points.reverse();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function removeBorderNodes(g) {
|
|
308
|
+
g.nodes().forEach(v => {
|
|
309
|
+
if (g.children(v).length) {
|
|
310
|
+
let node = g.node(v);
|
|
311
|
+
let t = g.node(node.borderTop);
|
|
312
|
+
let b = g.node(node.borderBottom);
|
|
313
|
+
let l = g.node(node.borderLeft[node.borderLeft.length - 1]);
|
|
314
|
+
let r = g.node(node.borderRight[node.borderRight.length - 1]);
|
|
315
|
+
|
|
316
|
+
node.width = Math.abs(r.x - l.x);
|
|
317
|
+
node.height = Math.abs(b.y - t.y);
|
|
318
|
+
node.x = l.x + node.width / 2;
|
|
319
|
+
node.y = t.y + node.height / 2;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
g.nodes().forEach(v => {
|
|
324
|
+
if (g.node(v).dummy === "border") {
|
|
325
|
+
g.removeNode(v);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function removeSelfEdges(g) {
|
|
331
|
+
g.edges().forEach(e => {
|
|
332
|
+
if (e.v === e.w) {
|
|
333
|
+
var node = g.node(e.v);
|
|
334
|
+
if (!node.selfEdges) {
|
|
335
|
+
node.selfEdges = [];
|
|
336
|
+
}
|
|
337
|
+
node.selfEdges.push({ e: e, label: g.edge(e) });
|
|
338
|
+
g.removeEdge(e);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function insertSelfEdges(g) {
|
|
344
|
+
var layers = util.buildLayerMatrix(g);
|
|
345
|
+
layers.forEach(layer => {
|
|
346
|
+
var orderShift = 0;
|
|
347
|
+
layer.forEach((v, i) => {
|
|
348
|
+
var node = g.node(v);
|
|
349
|
+
node.order = i + orderShift;
|
|
350
|
+
(node.selfEdges || []).forEach(selfEdge => {
|
|
351
|
+
util.addDummyNode(g, "selfedge", {
|
|
352
|
+
width: selfEdge.label.width,
|
|
353
|
+
height: selfEdge.label.height,
|
|
354
|
+
rank: node.rank,
|
|
355
|
+
order: i + (++orderShift),
|
|
356
|
+
e: selfEdge.e,
|
|
357
|
+
label: selfEdge.label
|
|
358
|
+
}, "_se");
|
|
359
|
+
});
|
|
360
|
+
delete node.selfEdges;
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function positionSelfEdges(g) {
|
|
366
|
+
g.nodes().forEach(v => {
|
|
367
|
+
var node = g.node(v);
|
|
368
|
+
if (node.dummy === "selfedge") {
|
|
369
|
+
var selfNode = g.node(node.e.v);
|
|
370
|
+
var x = selfNode.x + selfNode.width / 2;
|
|
371
|
+
var y = selfNode.y;
|
|
372
|
+
var dx = node.x - x;
|
|
373
|
+
var dy = selfNode.height / 2;
|
|
374
|
+
g.setEdge(node.e, node.label);
|
|
375
|
+
g.removeNode(v);
|
|
376
|
+
node.label.points = [
|
|
377
|
+
{ x: x + 2 * dx / 3, y: y - dy },
|
|
378
|
+
{ x: x + 5 * dx / 6, y: y - dy },
|
|
379
|
+
{ x: x + dx , y: y },
|
|
380
|
+
{ x: x + 5 * dx / 6, y: y + dy },
|
|
381
|
+
{ x: x + 2 * dx / 3, y: y + dy }
|
|
382
|
+
];
|
|
383
|
+
node.label.x = node.x;
|
|
384
|
+
node.label.y = node.y;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function selectNumberAttrs(obj, attrs) {
|
|
390
|
+
return util.mapValues(util.pick(obj, attrs), Number);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function canonicalize(attrs) {
|
|
394
|
+
var newAttrs = {};
|
|
395
|
+
if (attrs) {
|
|
396
|
+
Object.entries(attrs).forEach(([k, v]) => {
|
|
397
|
+
if (typeof k === "string") {
|
|
398
|
+
k = k.toLowerCase();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
newAttrs[k] = v;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return newAttrs;
|
|
405
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as util from "./util.js";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,
|
|
5
|
+
* adds appropriate edges to ensure that all cluster nodes are placed between
|
|
6
|
+
* these boundaries, and ensures that the graph is connected.
|
|
7
|
+
*
|
|
8
|
+
* In addition we ensure, through the use of the minlen property, that nodes
|
|
9
|
+
* and subgraph border nodes to not end up on the same rank.
|
|
10
|
+
*
|
|
11
|
+
* Preconditions:
|
|
12
|
+
*
|
|
13
|
+
* 1. Input graph is a DAG
|
|
14
|
+
* 2. Nodes in the input graph has a minlen attribute
|
|
15
|
+
*
|
|
16
|
+
* Postconditions:
|
|
17
|
+
*
|
|
18
|
+
* 1. Input graph is connected.
|
|
19
|
+
* 2. Dummy nodes are added for the tops and bottoms of subgraphs.
|
|
20
|
+
* 3. The minlen attribute for nodes is adjusted to ensure nodes do not
|
|
21
|
+
* get placed on the same rank as subgraph border nodes.
|
|
22
|
+
*
|
|
23
|
+
* The nesting graph idea comes from Sander, "Layout of Compound Directed
|
|
24
|
+
* Graphs."
|
|
25
|
+
*/
|
|
26
|
+
export function run(g) {
|
|
27
|
+
let root = util.addDummyNode(g, "root", {}, "_root");
|
|
28
|
+
let depths = treeDepths(g);
|
|
29
|
+
let height = Math.max(...Object.values(depths)) - 1; // Note: depths is an Object not an array
|
|
30
|
+
let nodeSep = 2 * height + 1;
|
|
31
|
+
|
|
32
|
+
g.graph().nestingRoot = root;
|
|
33
|
+
|
|
34
|
+
// Multiply minlen by nodeSep to align nodes on non-border ranks.
|
|
35
|
+
g.edges().forEach(e => g.edge(e).minlen *= nodeSep);
|
|
36
|
+
|
|
37
|
+
// Calculate a weight that is sufficient to keep subgraphs vertically compact
|
|
38
|
+
let weight = sumWeights(g) + 1;
|
|
39
|
+
|
|
40
|
+
// Create border nodes and link them up
|
|
41
|
+
g.children().forEach(child => dfs(g, root, nodeSep, weight, height, depths, child));
|
|
42
|
+
|
|
43
|
+
// Save the multiplier for node layers for later removal of empty border
|
|
44
|
+
// layers.
|
|
45
|
+
g.graph().nodeRankFactor = nodeSep;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dfs(g, root, nodeSep, weight, height, depths, v) {
|
|
49
|
+
let children = g.children(v);
|
|
50
|
+
if (!children.length) {
|
|
51
|
+
if (v !== root) {
|
|
52
|
+
g.setEdge(root, v, { weight: 0, minlen: nodeSep });
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let top = util.addBorderNode(g, "_bt");
|
|
58
|
+
let bottom = util.addBorderNode(g, "_bb");
|
|
59
|
+
let label = g.node(v);
|
|
60
|
+
|
|
61
|
+
g.setParent(top, v);
|
|
62
|
+
label.borderTop = top;
|
|
63
|
+
g.setParent(bottom, v);
|
|
64
|
+
label.borderBottom = bottom;
|
|
65
|
+
|
|
66
|
+
children.forEach(child => {
|
|
67
|
+
dfs(g, root, nodeSep, weight, height, depths, child);
|
|
68
|
+
|
|
69
|
+
let childNode = g.node(child);
|
|
70
|
+
let childTop = childNode.borderTop ? childNode.borderTop : child;
|
|
71
|
+
let childBottom = childNode.borderBottom ? childNode.borderBottom : child;
|
|
72
|
+
let thisWeight = childNode.borderTop ? weight : 2 * weight;
|
|
73
|
+
let minlen = childTop !== childBottom ? 1 : height - depths[v] + 1;
|
|
74
|
+
|
|
75
|
+
g.setEdge(top, childTop, {
|
|
76
|
+
weight: thisWeight,
|
|
77
|
+
minlen: minlen,
|
|
78
|
+
nestingEdge: true
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
g.setEdge(childBottom, bottom, {
|
|
82
|
+
weight: thisWeight,
|
|
83
|
+
minlen: minlen,
|
|
84
|
+
nestingEdge: true
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!g.parent(v)) {
|
|
89
|
+
g.setEdge(root, top, { weight: 0, minlen: height + depths[v] });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function treeDepths(g) {
|
|
94
|
+
var depths = {};
|
|
95
|
+
function dfs(v, depth) {
|
|
96
|
+
var children = g.children(v);
|
|
97
|
+
if (children && children.length) {
|
|
98
|
+
children.forEach(child => dfs(child, depth + 1));
|
|
99
|
+
}
|
|
100
|
+
depths[v] = depth;
|
|
101
|
+
}
|
|
102
|
+
g.children().forEach(v => dfs(v, 1));
|
|
103
|
+
return depths;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sumWeights(g) {
|
|
107
|
+
return g.edges().reduce((acc, e) => acc + g.edge(e).weight, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function cleanup(g) {
|
|
111
|
+
var graphLabel = g.graph();
|
|
112
|
+
g.removeNode(graphLabel.nestingRoot);
|
|
113
|
+
delete graphLabel.nestingRoot;
|
|
114
|
+
g.edges().forEach(e => {
|
|
115
|
+
var edge = g.edge(e);
|
|
116
|
+
if (edge.nestingEdge) {
|
|
117
|
+
g.removeEdge(e);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|