@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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { default as feasibleTree } from "./feasible-tree.js";
|
|
4
|
+
import { slack, longestPath as initRank } from "./util.js";
|
|
5
|
+
import { alg } from "@dagrejs/graphlib";
|
|
6
|
+
var preorder = alg.preorder;
|
|
7
|
+
var postorder = alg.postorder;
|
|
8
|
+
import { simplify } from "../util.js";
|
|
9
|
+
|
|
10
|
+
// Expose some internals for testing purposes
|
|
11
|
+
networkSimplex.initLowLimValues = initLowLimValues;
|
|
12
|
+
networkSimplex.initCutValues = initCutValues;
|
|
13
|
+
networkSimplex.calcCutValue = calcCutValue;
|
|
14
|
+
networkSimplex.leaveEdge = leaveEdge;
|
|
15
|
+
networkSimplex.enterEdge = enterEdge;
|
|
16
|
+
networkSimplex.exchangeEdges = exchangeEdges;
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The network simplex algorithm assigns ranks to each node in the input graph
|
|
20
|
+
* and iteratively improves the ranking to reduce the length of edges.
|
|
21
|
+
*
|
|
22
|
+
* Preconditions:
|
|
23
|
+
*
|
|
24
|
+
* 1. The input graph must be a DAG.
|
|
25
|
+
* 2. All nodes in the graph must have an object value.
|
|
26
|
+
* 3. All edges in the graph must have "minlen" and "weight" attributes.
|
|
27
|
+
*
|
|
28
|
+
* Postconditions:
|
|
29
|
+
*
|
|
30
|
+
* 1. All nodes in the graph will have an assigned "rank" attribute that has
|
|
31
|
+
* been optimized by the network simplex algorithm. Ranks start at 0.
|
|
32
|
+
*
|
|
33
|
+
*
|
|
34
|
+
* A rough sketch of the algorithm is as follows:
|
|
35
|
+
*
|
|
36
|
+
* 1. Assign initial ranks to each node. We use the longest path algorithm,
|
|
37
|
+
* which assigns ranks to the lowest position possible. In general this
|
|
38
|
+
* leads to very wide bottom ranks and unnecessarily long edges.
|
|
39
|
+
* 2. Construct a feasible tight tree. A tight tree is one such that all
|
|
40
|
+
* edges in the tree have no slack (difference between length of edge
|
|
41
|
+
* and minlen for the edge). This by itself greatly improves the assigned
|
|
42
|
+
* rankings by shorting edges.
|
|
43
|
+
* 3. Iteratively find edges that have negative cut values. Generally a
|
|
44
|
+
* negative cut value indicates that the edge could be removed and a new
|
|
45
|
+
* tree edge could be added to produce a more compact graph.
|
|
46
|
+
*
|
|
47
|
+
* Much of the algorithms here are derived from Gansner, et al., "A Technique
|
|
48
|
+
* for Drawing Directed Graphs." The structure of the file roughly follows the
|
|
49
|
+
* structure of the overall algorithm.
|
|
50
|
+
*/
|
|
51
|
+
export default function networkSimplex(g) {
|
|
52
|
+
g = simplify(g);
|
|
53
|
+
initRank(g);
|
|
54
|
+
var t = feasibleTree(g);
|
|
55
|
+
initLowLimValues(t);
|
|
56
|
+
initCutValues(t, g);
|
|
57
|
+
|
|
58
|
+
var e, f;
|
|
59
|
+
while ((e = leaveEdge(t))) {
|
|
60
|
+
f = enterEdge(t, g, e);
|
|
61
|
+
exchangeEdges(t, g, e, f);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* Initializes cut values for all edges in the tree.
|
|
67
|
+
*/
|
|
68
|
+
function initCutValues(t, g) {
|
|
69
|
+
var vs = postorder(t, t.nodes());
|
|
70
|
+
vs = vs.slice(0, vs.length - 1);
|
|
71
|
+
vs.forEach(v => assignCutValue(t, g, v));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assignCutValue(t, g, child) {
|
|
75
|
+
var childLab = t.node(child);
|
|
76
|
+
var parent = childLab.parent;
|
|
77
|
+
t.edge(child, parent).cutvalue = calcCutValue(t, g, child);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Given the tight tree, its graph, and a child in the graph calculate and
|
|
82
|
+
* return the cut value for the edge between the child and its parent.
|
|
83
|
+
*/
|
|
84
|
+
function calcCutValue(t, g, child) {
|
|
85
|
+
var childLab = t.node(child);
|
|
86
|
+
var parent = childLab.parent;
|
|
87
|
+
// True if the child is on the tail end of the edge in the directed graph
|
|
88
|
+
var childIsTail = true;
|
|
89
|
+
// The graph's view of the tree edge we're inspecting
|
|
90
|
+
var graphEdge = g.edge(child, parent);
|
|
91
|
+
// The accumulated cut value for the edge between this node and its parent
|
|
92
|
+
var cutValue = 0;
|
|
93
|
+
|
|
94
|
+
if (!graphEdge) {
|
|
95
|
+
childIsTail = false;
|
|
96
|
+
graphEdge = g.edge(parent, child);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cutValue = graphEdge.weight;
|
|
100
|
+
|
|
101
|
+
g.nodeEdges(child).forEach(e => {
|
|
102
|
+
var isOutEdge = e.v === child,
|
|
103
|
+
other = isOutEdge ? e.w : e.v;
|
|
104
|
+
|
|
105
|
+
if (other !== parent) {
|
|
106
|
+
var pointsToHead = isOutEdge === childIsTail,
|
|
107
|
+
otherWeight = g.edge(e).weight;
|
|
108
|
+
|
|
109
|
+
cutValue += pointsToHead ? otherWeight : -otherWeight;
|
|
110
|
+
if (isTreeEdge(t, child, other)) {
|
|
111
|
+
var otherCutValue = t.edge(child, other).cutvalue;
|
|
112
|
+
cutValue += pointsToHead ? -otherCutValue : otherCutValue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return cutValue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function initLowLimValues(tree, root) {
|
|
121
|
+
if (arguments.length < 2) {
|
|
122
|
+
root = tree.nodes()[0];
|
|
123
|
+
}
|
|
124
|
+
dfsAssignLowLim(tree, {}, 1, root);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dfsAssignLowLim(tree, visited, nextLim, v, parent) {
|
|
128
|
+
var low = nextLim;
|
|
129
|
+
var label = tree.node(v);
|
|
130
|
+
|
|
131
|
+
visited[v] = true;
|
|
132
|
+
tree.neighbors(v).forEach(w => {
|
|
133
|
+
if (!visited.hasOwnProperty(w)) {
|
|
134
|
+
nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
label.low = low;
|
|
139
|
+
label.lim = nextLim++;
|
|
140
|
+
if (parent) {
|
|
141
|
+
label.parent = parent;
|
|
142
|
+
} else {
|
|
143
|
+
// TODO should be able to remove this when we incrementally update low lim
|
|
144
|
+
delete label.parent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return nextLim;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function leaveEdge(tree) {
|
|
151
|
+
return tree.edges().find(e => tree.edge(e).cutvalue < 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function enterEdge(t, g, edge) {
|
|
155
|
+
var v = edge.v;
|
|
156
|
+
var w = edge.w;
|
|
157
|
+
|
|
158
|
+
// For the rest of this function we assume that v is the tail and w is the
|
|
159
|
+
// head, so if we don't have this edge in the graph we should flip it to
|
|
160
|
+
// match the correct orientation.
|
|
161
|
+
if (!g.hasEdge(v, w)) {
|
|
162
|
+
v = edge.w;
|
|
163
|
+
w = edge.v;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var vLabel = t.node(v);
|
|
167
|
+
var wLabel = t.node(w);
|
|
168
|
+
var tailLabel = vLabel;
|
|
169
|
+
var flip = false;
|
|
170
|
+
|
|
171
|
+
// If the root is in the tail of the edge then we need to flip the logic that
|
|
172
|
+
// checks for the head and tail nodes in the candidates function below.
|
|
173
|
+
if (vLabel.lim > wLabel.lim) {
|
|
174
|
+
tailLabel = wLabel;
|
|
175
|
+
flip = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var candidates = g.edges().filter(edge => {
|
|
179
|
+
return flip === isDescendant(t, t.node(edge.v), tailLabel) &&
|
|
180
|
+
flip !== isDescendant(t, t.node(edge.w), tailLabel);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return candidates.reduce((acc, edge) => {
|
|
184
|
+
if (slack(g, edge) < slack(g, acc)) {
|
|
185
|
+
return edge;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return acc;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function exchangeEdges(t, g, e, f) {
|
|
193
|
+
var v = e.v;
|
|
194
|
+
var w = e.w;
|
|
195
|
+
t.removeEdge(v, w);
|
|
196
|
+
t.setEdge(f.v, f.w, {});
|
|
197
|
+
initLowLimValues(t);
|
|
198
|
+
initCutValues(t, g);
|
|
199
|
+
updateRanks(t, g);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function updateRanks(t, g) {
|
|
203
|
+
var root = t.nodes().find(v => !g.node(v).parent);
|
|
204
|
+
var vs = preorder(t, root);
|
|
205
|
+
vs = vs.slice(1);
|
|
206
|
+
vs.forEach(v => {
|
|
207
|
+
var parent = t.node(v).parent,
|
|
208
|
+
edge = g.edge(v, parent),
|
|
209
|
+
flipped = false;
|
|
210
|
+
|
|
211
|
+
if (!edge) {
|
|
212
|
+
edge = g.edge(parent, v);
|
|
213
|
+
flipped = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* Returns true if the edge is in the tree.
|
|
222
|
+
*/
|
|
223
|
+
function isTreeEdge(tree, u, v) {
|
|
224
|
+
return tree.hasEdge(u, v);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* Returns true if the specified node is descendant of the root node per the
|
|
229
|
+
* assigned low and lim attributes in the tree.
|
|
230
|
+
*/
|
|
231
|
+
function isDescendant(tree, vLabel, rootLabel) {
|
|
232
|
+
return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim;
|
|
233
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Initializes ranks for the input graph using the longest path algorithm. This
|
|
5
|
+
* algorithm scales well and is fast in practice, it yields rather poor
|
|
6
|
+
* solutions. Nodes are pushed to the lowest layer possible, leaving the bottom
|
|
7
|
+
* ranks wide and leaving edges longer than necessary. However, due to its
|
|
8
|
+
* speed, this algorithm is good for getting an initial ranking that can be fed
|
|
9
|
+
* into other algorithms.
|
|
10
|
+
*
|
|
11
|
+
* This algorithm does not normalize layers because it will be used by other
|
|
12
|
+
* algorithms in most cases. If using this algorithm directly, be sure to
|
|
13
|
+
* run normalize at the end.
|
|
14
|
+
*
|
|
15
|
+
* Pre-conditions:
|
|
16
|
+
*
|
|
17
|
+
* 1. Input graph is a DAG.
|
|
18
|
+
* 2. Input graph node labels can be assigned properties.
|
|
19
|
+
*
|
|
20
|
+
* Post-conditions:
|
|
21
|
+
*
|
|
22
|
+
* 1. Each node will be assign an (unnormalized) "rank" property.
|
|
23
|
+
*/
|
|
24
|
+
export function longestPath(g) {
|
|
25
|
+
var visited = {};
|
|
26
|
+
|
|
27
|
+
function dfs(v) {
|
|
28
|
+
var label = g.node(v);
|
|
29
|
+
if (visited.hasOwnProperty(v)) {
|
|
30
|
+
return label.rank;
|
|
31
|
+
}
|
|
32
|
+
visited[v] = true;
|
|
33
|
+
|
|
34
|
+
var rank = Math.min(...g.outEdges(v).map(e => {
|
|
35
|
+
if (e == null) {
|
|
36
|
+
return Number.POSITIVE_INFINITY;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return dfs(e.w) - g.edge(e).minlen;
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
if (rank === Number.POSITIVE_INFINITY) {
|
|
43
|
+
rank = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (label.rank = rank);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
g.sources().forEach(dfs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Returns the amount of slack for the given edge. The slack is defined as the
|
|
54
|
+
* difference between the length of the edge and its minimum length.
|
|
55
|
+
*/
|
|
56
|
+
export function slack(g, e) {
|
|
57
|
+
return g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen;
|
|
58
|
+
}
|
package/mjs-lib/util.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/* eslint "no-console": off */
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
import { Graph as Graph } from "@dagrejs/graphlib";
|
|
6
|
+
|
|
7
|
+
export { addBorderNode,
|
|
8
|
+
addDummyNode,
|
|
9
|
+
asNonCompoundGraph,
|
|
10
|
+
buildLayerMatrix,
|
|
11
|
+
intersectRect,
|
|
12
|
+
mapValues,
|
|
13
|
+
maxRank,
|
|
14
|
+
normalizeRanks,
|
|
15
|
+
notime,
|
|
16
|
+
partition,
|
|
17
|
+
pick,
|
|
18
|
+
predecessorWeights,
|
|
19
|
+
range,
|
|
20
|
+
removeEmptyRanks,
|
|
21
|
+
simplify,
|
|
22
|
+
successorWeights,
|
|
23
|
+
time,
|
|
24
|
+
uniqueId,
|
|
25
|
+
zipObject,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Adds a dummy node to the graph and return v.
|
|
30
|
+
*/
|
|
31
|
+
function addDummyNode(g, type, attrs, name) {
|
|
32
|
+
let v;
|
|
33
|
+
do {
|
|
34
|
+
v = uniqueId(name);
|
|
35
|
+
} while (g.hasNode(v));
|
|
36
|
+
|
|
37
|
+
attrs.dummy = type;
|
|
38
|
+
g.setNode(v, attrs);
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* Returns a new graph with only simple edges. Handles aggregation of data
|
|
44
|
+
* associated with multi-edges.
|
|
45
|
+
*/
|
|
46
|
+
function simplify(g) {
|
|
47
|
+
let simplified = new Graph().setGraph(g.graph());
|
|
48
|
+
g.nodes().forEach(v => simplified.setNode(v, g.node(v)));
|
|
49
|
+
g.edges().forEach(e => {
|
|
50
|
+
let simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 };
|
|
51
|
+
let label = g.edge(e);
|
|
52
|
+
simplified.setEdge(e.v, e.w, {
|
|
53
|
+
weight: simpleLabel.weight + label.weight,
|
|
54
|
+
minlen: Math.max(simpleLabel.minlen, label.minlen)
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return simplified;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function asNonCompoundGraph(g) {
|
|
61
|
+
let simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph());
|
|
62
|
+
g.nodes().forEach(v => {
|
|
63
|
+
if (!g.children(v).length) {
|
|
64
|
+
simplified.setNode(v, g.node(v));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
g.edges().forEach(e => {
|
|
68
|
+
simplified.setEdge(e, g.edge(e));
|
|
69
|
+
});
|
|
70
|
+
return simplified;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function successorWeights(g) {
|
|
74
|
+
let weightMap = g.nodes().map(v => {
|
|
75
|
+
let sucs = {};
|
|
76
|
+
g.outEdges(v).forEach(e => {
|
|
77
|
+
sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight;
|
|
78
|
+
});
|
|
79
|
+
return sucs;
|
|
80
|
+
});
|
|
81
|
+
return zipObject(g.nodes(), weightMap);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function predecessorWeights(g) {
|
|
85
|
+
let weightMap = g.nodes().map(v => {
|
|
86
|
+
let preds = {};
|
|
87
|
+
g.inEdges(v).forEach(e => {
|
|
88
|
+
preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight;
|
|
89
|
+
});
|
|
90
|
+
return preds;
|
|
91
|
+
});
|
|
92
|
+
return zipObject(g.nodes(), weightMap);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Finds where a line starting at point ({x, y}) would intersect a rectangle
|
|
97
|
+
* ({x, y, width, height}) if it were pointing at the rectangle's center.
|
|
98
|
+
*/
|
|
99
|
+
function intersectRect(rect, point) {
|
|
100
|
+
let x = rect.x;
|
|
101
|
+
let y = rect.y;
|
|
102
|
+
|
|
103
|
+
// Rectangle intersection algorithm from:
|
|
104
|
+
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
|
105
|
+
let dx = point.x - x;
|
|
106
|
+
let dy = point.y - y;
|
|
107
|
+
let w = rect.width / 2;
|
|
108
|
+
let h = rect.height / 2;
|
|
109
|
+
|
|
110
|
+
if (!dx && !dy) {
|
|
111
|
+
throw new Error("Not possible to find intersection inside of the rectangle");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let sx, sy;
|
|
115
|
+
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
116
|
+
// Intersection is top or bottom of rect.
|
|
117
|
+
if (dy < 0) {
|
|
118
|
+
h = -h;
|
|
119
|
+
}
|
|
120
|
+
sx = h * dx / dy;
|
|
121
|
+
sy = h;
|
|
122
|
+
} else {
|
|
123
|
+
// Intersection is left or right of rect.
|
|
124
|
+
if (dx < 0) {
|
|
125
|
+
w = -w;
|
|
126
|
+
}
|
|
127
|
+
sx = w;
|
|
128
|
+
sy = w * dy / dx;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { x: x + sx, y: y + sy };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Given a DAG with each node assigned "rank" and "order" properties, this
|
|
136
|
+
* function will produce a matrix with the ids of each node.
|
|
137
|
+
*/
|
|
138
|
+
function buildLayerMatrix(g) {
|
|
139
|
+
let layering = range(maxRank(g) + 1).map(() => []);
|
|
140
|
+
g.nodes().forEach(v => {
|
|
141
|
+
let node = g.node(v);
|
|
142
|
+
let rank = node.rank;
|
|
143
|
+
if (rank !== undefined) {
|
|
144
|
+
layering[rank][node.order] = v;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return layering;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* Adjusts the ranks for all nodes in the graph such that all nodes v have
|
|
152
|
+
* rank(v) >= 0 and at least one node w has rank(w) = 0.
|
|
153
|
+
*/
|
|
154
|
+
function normalizeRanks(g) {
|
|
155
|
+
let min = Math.min(...g.nodes().map(v => {
|
|
156
|
+
let rank = g.node(v).rank;
|
|
157
|
+
if (rank === undefined) {
|
|
158
|
+
return Number.MAX_VALUE;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return rank;
|
|
162
|
+
}));
|
|
163
|
+
g.nodes().forEach(v => {
|
|
164
|
+
let node = g.node(v);
|
|
165
|
+
if (node.hasOwnProperty("rank")) {
|
|
166
|
+
node.rank -= min;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function removeEmptyRanks(g) {
|
|
172
|
+
// Ranks may not start at 0, so we need to offset them
|
|
173
|
+
let offset = Math.min(...g.nodes().map(v => g.node(v).rank));
|
|
174
|
+
|
|
175
|
+
let layers = [];
|
|
176
|
+
g.nodes().forEach(v => {
|
|
177
|
+
let rank = g.node(v).rank - offset;
|
|
178
|
+
if (!layers[rank]) {
|
|
179
|
+
layers[rank] = [];
|
|
180
|
+
}
|
|
181
|
+
layers[rank].push(v);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let delta = 0;
|
|
185
|
+
let nodeRankFactor = g.graph().nodeRankFactor;
|
|
186
|
+
Array.from(layers).forEach((vs, i) => {
|
|
187
|
+
if (vs === undefined && i % nodeRankFactor !== 0) {
|
|
188
|
+
--delta;
|
|
189
|
+
} else if (vs !== undefined && delta) {
|
|
190
|
+
vs.forEach(v => g.node(v).rank += delta);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function addBorderNode(g, prefix, rank, order) {
|
|
196
|
+
let node = {
|
|
197
|
+
width: 0,
|
|
198
|
+
height: 0
|
|
199
|
+
};
|
|
200
|
+
if (arguments.length >= 4) {
|
|
201
|
+
node.rank = rank;
|
|
202
|
+
node.order = order;
|
|
203
|
+
}
|
|
204
|
+
return addDummyNode(g, "border", node, prefix);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function maxRank(g) {
|
|
208
|
+
return Math.max(...g.nodes().map(v => {
|
|
209
|
+
let rank = g.node(v).rank;
|
|
210
|
+
if (rank === undefined) {
|
|
211
|
+
return Number.MIN_VALUE;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return rank;
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* Partition a collection into two groups: `lhs` and `rhs`. If the supplied
|
|
220
|
+
* function returns true for an entry it goes into `lhs`. Otherwise it goes
|
|
221
|
+
* into `rhs.
|
|
222
|
+
*/
|
|
223
|
+
function partition(collection, fn) {
|
|
224
|
+
let result = { lhs: [], rhs: [] };
|
|
225
|
+
collection.forEach(value => {
|
|
226
|
+
if (fn(value)) {
|
|
227
|
+
result.lhs.push(value);
|
|
228
|
+
} else {
|
|
229
|
+
result.rhs.push(value);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* Returns a new function that wraps `fn` with a timer. The wrapper logs the
|
|
237
|
+
* time it takes to execute the function.
|
|
238
|
+
*/
|
|
239
|
+
function time(name, fn) {
|
|
240
|
+
let start = Date.now();
|
|
241
|
+
try {
|
|
242
|
+
return fn();
|
|
243
|
+
} finally {
|
|
244
|
+
console.log(name + " time: " + (Date.now() - start) + "ms");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function notime(name, fn) {
|
|
249
|
+
return fn();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let idCounter = 0;
|
|
253
|
+
function uniqueId(prefix) {
|
|
254
|
+
var id = ++idCounter;
|
|
255
|
+
return toString(prefix) + id;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function range(start, limit, step = 1) {
|
|
259
|
+
if (limit == null) {
|
|
260
|
+
limit = start;
|
|
261
|
+
start = 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let endCon = (i) => i < limit;
|
|
265
|
+
if (step < 0) {
|
|
266
|
+
endCon = (i) => limit < i;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const range = [];
|
|
270
|
+
for (let i = start; endCon(i); i += step) {
|
|
271
|
+
range.push(i);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return range;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function pick(source, keys) {
|
|
278
|
+
const dest = {};
|
|
279
|
+
for (const key of keys) {
|
|
280
|
+
if (source[key] !== undefined) {
|
|
281
|
+
dest[key] = source[key];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return dest;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function mapValues(obj, funcOrProp) {
|
|
289
|
+
let func = funcOrProp;
|
|
290
|
+
if (typeof funcOrProp === 'string') {
|
|
291
|
+
func = (val) => val[funcOrProp];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return Object.entries(obj).reduce((acc, [k, v]) => {
|
|
295
|
+
acc[k] = func(v, k);
|
|
296
|
+
return acc;
|
|
297
|
+
}, {});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function zipObject(props, values) {
|
|
301
|
+
return props.reduce((acc, key, i) => {
|
|
302
|
+
acc[key] = values[i];
|
|
303
|
+
return acc;
|
|
304
|
+
}, {});
|
|
305
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "1.1.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dagrejs/dagre",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Graph layout for JavaScript",
|
|
5
5
|
"author": "Chris Pettitt <cpettitt@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
"lint": "make lint",
|
|
13
13
|
"test": "make test"
|
|
14
14
|
},
|
|
15
|
+
"module": "./mjs-lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
"import": "./mjs-lib/index.js",
|
|
18
|
+
"require": "./index.js"
|
|
19
|
+
},
|
|
15
20
|
"files": [
|
|
16
21
|
"index.js",
|
|
17
22
|
"index.d.ts",
|
|
18
23
|
"dist/",
|
|
19
|
-
"lib/"
|
|
24
|
+
"lib/",
|
|
25
|
+
"mjs-lib/"
|
|
20
26
|
],
|
|
21
27
|
"types": "index.d.ts",
|
|
22
28
|
"keywords": [
|
|
@@ -24,9 +30,14 @@
|
|
|
24
30
|
"layout"
|
|
25
31
|
],
|
|
26
32
|
"dependencies": {
|
|
27
|
-
"@dagrejs/graphlib": "2.
|
|
33
|
+
"@dagrejs/graphlib": "2.2.0"
|
|
28
34
|
},
|
|
29
35
|
"devDependencies": {
|
|
36
|
+
"@babel/cli": "^7.23.9",
|
|
37
|
+
"@babel/core": "^7.23.9",
|
|
38
|
+
"@babel/plugin-transform-export-namespace-from": "^7.23.4",
|
|
39
|
+
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
|
|
40
|
+
"babel-plugin-add-module-exports": "^1.0.4",
|
|
30
41
|
"benchmark": "2.1.4",
|
|
31
42
|
"browserify": "17.0.0",
|
|
32
43
|
"chai": "4.3.6",
|