@davidsouther/agathon 0.0.0-stage → 2026.11.1
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/package.json +19 -4
- package/src/assert.ts +67 -0
- package/src/geometry/box.ts +76 -0
- package/src/geometry/circle.ts +296 -0
- package/src/geometry/geometry.ts +318 -0
- package/src/geometry/path.ts +315 -0
- package/src/geometry/quadtree.ts +321 -0
- package/src/geometry/rectangle.ts +129 -0
- package/src/geometry/rngon.ts +132 -0
- package/src/geometry/types.ts +102 -0
- package/src/geometry.ts +61 -0
- package/src/highlight/highlight.ts +85 -0
- package/src/index.ts +5 -0
- package/src/layout/grid/manager.ts +55 -0
- package/src/layout/grid.ts +1 -0
- package/src/layout/hierarchic/README.md +21 -0
- package/src/layout/hierarchic/acyclic.ts +249 -0
- package/src/layout/hierarchic/children.ts +169 -0
- package/src/layout/hierarchic/debug.ts +186 -0
- package/src/layout/hierarchic/layout.ts +266 -0
- package/src/layout/hierarchic/manager.ts +65 -0
- package/src/layout/hierarchic/normalize.ts +177 -0
- package/src/layout/hierarchic/order.ts +321 -0
- package/src/layout/hierarchic/position.ts +514 -0
- package/src/layout/hierarchic/rank.ts +629 -0
- package/src/layout/hierarchic.ts +2 -0
- package/src/layout/hulls/arrange.ts +94 -0
- package/src/layout/hulls/hulls.ts +243 -0
- package/src/layout/hulls/segments.ts +174 -0
- package/src/layout/hulls.ts +3 -0
- package/src/layout/manager/child_manager.ts +42 -0
- package/src/layout/manager/drag_manager.ts +112 -0
- package/src/layout/manager/manager.ts +425 -0
- package/src/layout/manager/pan_manager.ts +146 -0
- package/src/layout/manager/visibility_manager.ts +84 -0
- package/src/layout/manager/zoom_manager.ts +263 -0
- package/src/layout/manager.ts +6 -0
- package/src/layout/properties/defaults.ts +99 -0
- package/src/layout/properties/symbol.ts +99 -0
- package/src/layout/properties/weak_map.ts +73 -0
- package/src/layout/properties.ts +111 -0
- package/src/layout/types.ts +470 -0
- package/src/layout/util.ts +444 -0
- package/src/layout.ts +6 -0
- package/src/samples/diff_graph.ts +15 -0
- package/src/samples/edge_cases.ts +168 -0
- package/src/samples/gansner_with_states.ts +67 -0
- package/src/samples/load_generator.ts +213 -0
- package/src/samples/search_graph.ts +35 -0
- package/src/tsabre/algo.ts +126 -0
- package/src/tsabre/builder.ts +97 -0
- package/src/tsabre/compress.ts +15 -0
- package/src/tsabre/dijkstra/dijkstra.ts +76 -0
- package/src/tsabre/dijkstra/heap.ts +83 -0
- package/src/tsabre/graph.ts +808 -0
- package/src/tsabre/parse.ts +634 -0
- package/src/tsabre/print.ts +188 -0
- package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
- package/src/tsabre/sourcebuilder/printable.ts +33 -0
- package/src/tsabre.ts +6 -0
- package/src/types.ts +424 -0
- package/README.md +0 -3
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { assert, assertExists, assertFinite } from "../../assert.ts";
|
|
2
|
+
import { dfs, Order } from "../../tsabre/algo.ts";
|
|
3
|
+
import { edgeId, TsabreGraph } from "../../tsabre/graph.ts";
|
|
4
|
+
import type { Edge, FullGraph, Node, NodeId } from "../../types.ts";
|
|
5
|
+
import type { LayoutEdge, LayoutNode, NodeProperties } from "../types.ts";
|
|
6
|
+
import * as util from "../util.ts";
|
|
7
|
+
|
|
8
|
+
import * as debug from "./debug.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ranking a graph is the process of assigning a vertical position to each node
|
|
12
|
+
* in the graph, such that all successors of a node have a higher rank than
|
|
13
|
+
* itself. There are many feasible rankings which satisfy this criteria, so
|
|
14
|
+
* by applying principal A3 "Short edges" in Layout, there is an additional
|
|
15
|
+
* constraint to minimize weighted edges lengths. This is more than an aesthetic
|
|
16
|
+
* concern, however, as many later passes (especially in ordering) are runtime
|
|
17
|
+
* sensitive to the number of ranks in the graph.
|
|
18
|
+
*
|
|
19
|
+
* The constraint that all successors have a higher rank additionally implies an
|
|
20
|
+
* acyclic graph. Therefore, the first step in ranking is to remove cycles. This
|
|
21
|
+
* is achieved by finding the "Feedback Arc Set", a known problem in graph
|
|
22
|
+
* literature. See the implementation & notes in acyclic.ts. Further
|
|
23
|
+
* optimizaztions come by nesting multiple edges between pairs of nodes into a
|
|
24
|
+
* single edge with a summed weight.
|
|
25
|
+
*
|
|
26
|
+
* By reformulating this constraint as a numerical optimization problem,
|
|
27
|
+
* well-known network simplex algorithms solve the problem quickly and
|
|
28
|
+
* effectively. Finally, the graph is renormalized so that the lowest rank is 0.
|
|
29
|
+
*
|
|
30
|
+
* This implementation follows sections 2.2-2.4 of [Gansner], see docs/tsabre for
|
|
31
|
+
* links.
|
|
32
|
+
*/
|
|
33
|
+
export function rank(g: FullGraph): void {
|
|
34
|
+
nesting.run(g);
|
|
35
|
+
networkSimplex(g);
|
|
36
|
+
removeEmptyRanks(g);
|
|
37
|
+
nesting.undo(g);
|
|
38
|
+
normalizeRanks(g);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run a network simplex optimization on a graph, assigning an optimal rank to
|
|
43
|
+
* every node. This proceeds by creating an initial feasible tree† and then
|
|
44
|
+
* exchanging edges from the spanning tree with shorter edges from the graph.
|
|
45
|
+
* The exchange happens between the tree edge with the most negative "cut
|
|
46
|
+
* value", and exchanges it with the edge in the graph having the least "slack"
|
|
47
|
+
* which can connect the two halves of the tree. "Slack" is the difference
|
|
48
|
+
* between current length and minimum length. The loop terminates when either no
|
|
49
|
+
* edge in the tree has a negative cut value, or when no edge in the graph could
|
|
50
|
+
* reconnect the halves.
|
|
51
|
+
*
|
|
52
|
+
* † Feasible Tree is a subset of the edges in the graph such that all nodes are
|
|
53
|
+
* present and reachable as a tree.
|
|
54
|
+
*/
|
|
55
|
+
function networkSimplex(g: FullGraph): void {
|
|
56
|
+
const { printDebug } = util.getGraphProps(g);
|
|
57
|
+
g = simplify(g);
|
|
58
|
+
longestPath(g); // Initialize ranking using longest path.
|
|
59
|
+
const t = feasibleTree(g);
|
|
60
|
+
initLowLimValues(t);
|
|
61
|
+
initCutValues(t, g);
|
|
62
|
+
|
|
63
|
+
if (printDebug) console.group("Network Simplex");
|
|
64
|
+
if (printDebug) debug.printSpanningTree(t);
|
|
65
|
+
const swaps = new Set<string>();
|
|
66
|
+
|
|
67
|
+
let e: Edge | undefined;
|
|
68
|
+
while ((e = leaveEdge(t))) {
|
|
69
|
+
const f = enterEdge(t, g, e);
|
|
70
|
+
|
|
71
|
+
const eId = edgeId(e);
|
|
72
|
+
const fId = f !== undefined ? edgeId(f) : "undefined";
|
|
73
|
+
const id = `${eId} ⇋ ${fId}`;
|
|
74
|
+
|
|
75
|
+
if (swaps.has(id)) {
|
|
76
|
+
debug.print(`network simplex found looping swap with ${id}`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
swaps.add(id);
|
|
80
|
+
if (f === undefined) break;
|
|
81
|
+
|
|
82
|
+
exchangeEdges(t, g, e, f);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (printDebug) console.table(swaps);
|
|
86
|
+
if (printDebug) console.groupEnd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Given a multigraph (a graph allowing multiple edges between pairs of nodes,
|
|
91
|
+
* collapse all multiedges updating their minlength and weights.
|
|
92
|
+
*/
|
|
93
|
+
function simplify(graph: FullGraph): FullGraph {
|
|
94
|
+
const g2 = new TsabreGraph<LayoutNode, LayoutEdge>();
|
|
95
|
+
const { nestingRoot } = util.getGraphProps(graph);
|
|
96
|
+
util.getGraphProps(g2).nestingRoot = nestingRoot;
|
|
97
|
+
|
|
98
|
+
for (const id of graph.nodes) {
|
|
99
|
+
g2.setNode(id, graph.node(id)!);
|
|
100
|
+
}
|
|
101
|
+
for (const edge of graph.edges()) {
|
|
102
|
+
const { source, target } = edge;
|
|
103
|
+
const simple = { source, target };
|
|
104
|
+
if (!g2.hasEdge(simple)) {
|
|
105
|
+
g2.setEdge(simple, { ...simple, layout: { weight: 0 } } as LayoutEdge);
|
|
106
|
+
}
|
|
107
|
+
const edgeLayout = util.getEdgeProps(graph.edge(edge)!);
|
|
108
|
+
const layout = util.getEdgeProps(g2.edge(simple)!);
|
|
109
|
+
layout.weight += edgeLayout.weight;
|
|
110
|
+
layout.minLength = Math.max(layout.minLength, edgeLayout.minLength);
|
|
111
|
+
}
|
|
112
|
+
return g2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Given a graph, assign a rank to each node such that all successors of a node
|
|
117
|
+
* have a higher rank than itself. If the node has already been visited, it gets
|
|
118
|
+
* the first such assignment.
|
|
119
|
+
*/
|
|
120
|
+
function longestPath(g: FullGraph): void {
|
|
121
|
+
const visited = new Set<NodeId>();
|
|
122
|
+
function dfs(id: NodeId, minRank = 0): number {
|
|
123
|
+
const props = util.getNodeProps(g.node(id)!);
|
|
124
|
+
if (visited.has(id)) {
|
|
125
|
+
assert(props.rank !== undefined, `${id} visited with no set rank, likely a cycle.`);
|
|
126
|
+
assert(!isNaN(props.rank), `${id} has rank NaN`);
|
|
127
|
+
return props.rank;
|
|
128
|
+
}
|
|
129
|
+
visited.add(id);
|
|
130
|
+
|
|
131
|
+
const ranks = Array.from(g.outEdges(id)).map((edge) => {
|
|
132
|
+
const { minLength } = util.getEdgeProps(g.edge(edge)!);
|
|
133
|
+
const childLength = dfs(edge.target, props.fixedRank ? props.rank : 0);
|
|
134
|
+
return childLength - minLength;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const rank = Math.min(...ranks, props.rank || 0) + minRank;
|
|
138
|
+
setRank(props, rank);
|
|
139
|
+
return props.rank;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const id of g.sources) dfs(id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Adjusts the ranks for all nodes in the graph such that all nodes have
|
|
147
|
+
* rank >= 0 and at least one node has rank == 0.
|
|
148
|
+
*/
|
|
149
|
+
function normalizeRanks(g: FullGraph): void {
|
|
150
|
+
const ranks = Array.from(g.allNodes).map((n) => util.getNodeProps(n).rank);
|
|
151
|
+
const offset = Math.min(...ranks);
|
|
152
|
+
|
|
153
|
+
for (const id of g.nodes) {
|
|
154
|
+
const layout = util.getNodeProps(g.node(id)!);
|
|
155
|
+
layout.rank -= offset;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Compact a graph so that there are no ranks with no nodes assigned.
|
|
161
|
+
*/
|
|
162
|
+
export function removeEmptyRanks(g: FullGraph): void {
|
|
163
|
+
const offset = Math.min(...Array.from(g.nodes).map((id) => util.getNodeProps(g.node(id)!).rank));
|
|
164
|
+
|
|
165
|
+
const layers: NodeProperties[][] = [];
|
|
166
|
+
for (const id of g.nodes) {
|
|
167
|
+
const layout = util.getNodeProps(g.node(id)!);
|
|
168
|
+
const rank = layout.rank - offset;
|
|
169
|
+
layers[rank] = layers[rank] || [];
|
|
170
|
+
layers[rank].push(layout);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let delta = 0;
|
|
174
|
+
const { nodeRankFactor } = util.getGraphProps(g);
|
|
175
|
+
for (let i = 0; i < layers.length; i++) {
|
|
176
|
+
const layer = layers[i];
|
|
177
|
+
if (layer === undefined && i % nodeRankFactor !== 0) {
|
|
178
|
+
--delta;
|
|
179
|
+
} else {
|
|
180
|
+
for (const layout of layer || []) {
|
|
181
|
+
layout.rank += delta;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Given a graph, determine an initial feasible spanning tree. Starting from a
|
|
189
|
+
* longest path, increase rankings of higher-rank nodes until slack on all edges
|
|
190
|
+
* is non-negative.
|
|
191
|
+
*/
|
|
192
|
+
function feasibleTree(g: FullGraph): TsabreGraph<Node, Edge> {
|
|
193
|
+
const tree = new TsabreGraph<Node, Edge>({ trackSegments: false });
|
|
194
|
+
|
|
195
|
+
const start = Array.from(g.nodes)[0];
|
|
196
|
+
const size = g.nodeCount;
|
|
197
|
+
|
|
198
|
+
tree.setNode(start, { id: start });
|
|
199
|
+
|
|
200
|
+
while (tightTree(tree, g) < size) {
|
|
201
|
+
const edgeSlack = findMinSlackEdge(tree, g);
|
|
202
|
+
if (!edgeSlack) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
const [edge, s] = edgeSlack;
|
|
206
|
+
const delta = (tree.hasNode(edge.source) ? 1 : -1) * s;
|
|
207
|
+
shiftRanks(tree, g, delta);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return tree;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Given a graph `g` with ranked edges, update `t` to be the subset of `g`
|
|
215
|
+
* having only edges with slack 0. `t` will be modified in place. Returns the
|
|
216
|
+
* number of nodes in `t`.
|
|
217
|
+
*/
|
|
218
|
+
function tightTree(t: FullGraph, g: FullGraph): number {
|
|
219
|
+
for (const id of t.nodes) {
|
|
220
|
+
findLowSlack(id);
|
|
221
|
+
}
|
|
222
|
+
return t.nodeCount;
|
|
223
|
+
|
|
224
|
+
function findLowSlack(id: NodeId) {
|
|
225
|
+
for (const e of g.edges(id)) {
|
|
226
|
+
const source = e.source;
|
|
227
|
+
const target = id === source ? e.target : source;
|
|
228
|
+
const edgeSlack = slack(g, e);
|
|
229
|
+
if (!t.hasNode(target) && edgeSlack === 0) {
|
|
230
|
+
t.setNode(target, { id: target });
|
|
231
|
+
const st = { source: id, target };
|
|
232
|
+
const ts = { source: target, target: id };
|
|
233
|
+
t.setEdge(st, st);
|
|
234
|
+
t.setEdge(ts, ts);
|
|
235
|
+
findLowSlack(target);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns true if an edge connects to but is not entirely in a tree, and false
|
|
243
|
+
* if both node are in or not in the tree. That is, at least one node must be
|
|
244
|
+
* in the tree and the other must not be.
|
|
245
|
+
*/
|
|
246
|
+
function incidentEdge(t: FullGraph, e: Edge): boolean {
|
|
247
|
+
return t.hasNode(e.source) !== t.hasNode(e.target);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Given a partial spanning tree `t` and a graph `g`, an edge in `g` that has
|
|
252
|
+
* the least slack while also being incident to (only one edge in) `t`. Returns
|
|
253
|
+
* `undefined` if no such edge exists.
|
|
254
|
+
*/
|
|
255
|
+
function findMinSlackEdge(t: FullGraph, g: FullGraph): [Edge, number] | undefined {
|
|
256
|
+
const incidentEdges = Array.from(g.edges())
|
|
257
|
+
.filter((e) => incidentEdge(t, e))
|
|
258
|
+
.filter(
|
|
259
|
+
(e) =>
|
|
260
|
+
!util.getNodeProps(g.node(e.source)!).fixedRank &&
|
|
261
|
+
!util.getNodeProps(g.node(e.target)!).fixedRank,
|
|
262
|
+
);
|
|
263
|
+
// Incident edges sorted by least slack.
|
|
264
|
+
const slackEdges = incidentEdges
|
|
265
|
+
.map((e) => [e, slack(g, e)] as [Edge, number])
|
|
266
|
+
.sort(([e1, s1], [e2, s2]) => s1 - s2);
|
|
267
|
+
|
|
268
|
+
return slackEdges[0];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Given a spanning tree `t`, move all its nodes in `g` up `delta` ranks.
|
|
273
|
+
*/
|
|
274
|
+
function shiftRanks(t: FullGraph, g: FullGraph, delta: number) {
|
|
275
|
+
for (const id of t.nodes) {
|
|
276
|
+
const props = util.getNodeProps(g.node(id)!);
|
|
277
|
+
setRank(props, props.rank + delta);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The slack of an edge is the difference in its length (in ranks spanned) minus
|
|
283
|
+
* is minLength. A tight edge has slack = 0.
|
|
284
|
+
*/
|
|
285
|
+
function slack(g: FullGraph, e: Edge) {
|
|
286
|
+
const sourceRank = util.getNodeProps(g.node(e.source)!).rank;
|
|
287
|
+
const targetRank = util.getNodeProps(g.node(e.target)!).rank;
|
|
288
|
+
const minLength = util.getEdgeProps(g.edge(e)!).minLength;
|
|
289
|
+
return targetRank - sourceRank - minLength;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Find any edge with a negative cut value.
|
|
294
|
+
*/
|
|
295
|
+
function leaveEdge(g: FullGraph): Edge | undefined {
|
|
296
|
+
for (const edge of g.edges()) {
|
|
297
|
+
const value = g.edge(edge)!;
|
|
298
|
+
const layout = util.getEdgeProps(value);
|
|
299
|
+
if (layout.cutValue < 0) return edge;
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Given an edge with a negative cut value, find the edge within the bounds that
|
|
306
|
+
* has the smallest slack.
|
|
307
|
+
*/
|
|
308
|
+
function enterEdge(t: FullGraph, g: FullGraph, edge: Edge): Edge | undefined {
|
|
309
|
+
let { source, target } = edge;
|
|
310
|
+
if (!g.hasEdge({ source, target })) {
|
|
311
|
+
[source, target] = [target, source];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sourceNode = assertExists(t.node(source));
|
|
315
|
+
const targetNode = assertExists(t.node(target));
|
|
316
|
+
let tail = sourceNode;
|
|
317
|
+
let flip = false;
|
|
318
|
+
|
|
319
|
+
if (util.getNodeProps(sourceNode).lim > util.getNodeProps(targetNode).lim) {
|
|
320
|
+
tail = targetNode;
|
|
321
|
+
flip = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const candidates = Array.from(g.edges()).filter((edge) => {
|
|
325
|
+
const source = t.node(edge.source);
|
|
326
|
+
const target = t.node(edge.target);
|
|
327
|
+
// Nodes with a fixed rank are not eligible to swap.
|
|
328
|
+
if (
|
|
329
|
+
!source ||
|
|
330
|
+
util.getNodeProps(source).fixedRank ||
|
|
331
|
+
!target ||
|
|
332
|
+
util.getNodeProps(target).fixedRank
|
|
333
|
+
) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const sourceInBounds = isDescendant(source, tail, flip);
|
|
337
|
+
const targetInBounds = isDescendant(target, tail, !flip);
|
|
338
|
+
return sourceInBounds && targetInBounds;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const candidate = candidates
|
|
342
|
+
.map<[Edge, number]>((edge) => [edge, slack(g, edge)])
|
|
343
|
+
.sort((a, b) => a[1] - b[1])
|
|
344
|
+
.map(([edge]) => edge)[0];
|
|
345
|
+
|
|
346
|
+
return candidate;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* `source` is a descendant of `root` if its min and max ranks are completely
|
|
351
|
+
* bounded by `root`.
|
|
352
|
+
*/
|
|
353
|
+
function isDescendant(source: Node, root: Node, flip: boolean): boolean {
|
|
354
|
+
const sourceLayout = util.getNodeProps(source);
|
|
355
|
+
const rootLayout = util.getNodeProps(root);
|
|
356
|
+
const lowerBound = rootLayout.low <= sourceLayout.lim;
|
|
357
|
+
const upperBound = sourceLayout.lim <= rootLayout.lim;
|
|
358
|
+
const inBounds = lowerBound && upperBound;
|
|
359
|
+
return flip === inBounds;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Given a spanning tree, initialize lim and low for each node.
|
|
364
|
+
*/
|
|
365
|
+
function initLowLimValues(t: FullGraph, root = Array.from(t.children())[0]): void {
|
|
366
|
+
dfsAssignLowLim(t, new Set(), 1, root);
|
|
367
|
+
|
|
368
|
+
function dfsAssignLowLim(
|
|
369
|
+
tree: FullGraph,
|
|
370
|
+
visited: Set<NodeId>,
|
|
371
|
+
nextLim: number,
|
|
372
|
+
id: NodeId,
|
|
373
|
+
parent?: NodeId,
|
|
374
|
+
): number {
|
|
375
|
+
const low = nextLim;
|
|
376
|
+
tree.setParent(id, parent);
|
|
377
|
+
|
|
378
|
+
visited.add(id);
|
|
379
|
+
for (const n of tree.neighbors(id)) {
|
|
380
|
+
if (!visited.has(n)) {
|
|
381
|
+
nextLim = dfsAssignLowLim(tree, visited, nextLim, n, id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const layout = util.getNodeProps(tree.node(id)!);
|
|
386
|
+
layout.low = low;
|
|
387
|
+
layout.lim = nextLim++;
|
|
388
|
+
|
|
389
|
+
return nextLim;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Given two edges in a spanning tree, swap them and reset graph layout values.
|
|
395
|
+
*/
|
|
396
|
+
function exchangeEdges(t: FullGraph, g: FullGraph, a: Edge, b: Edge): void {
|
|
397
|
+
const { source: sa, target: ta } = a;
|
|
398
|
+
t.removeEdge({ source: sa, target: ta });
|
|
399
|
+
t.removeEdge({ source: ta, target: sa });
|
|
400
|
+
const { source: sb, target: tb } = b;
|
|
401
|
+
const st = { source: sb, target: tb };
|
|
402
|
+
const ts = { source: tb, target: sb };
|
|
403
|
+
t.setEdge(st, st);
|
|
404
|
+
t.setEdge(ts, ts);
|
|
405
|
+
|
|
406
|
+
initLowLimValues(t);
|
|
407
|
+
initCutValues(t, g);
|
|
408
|
+
updateRanks(t, g);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function updateRanks(t: FullGraph, g: FullGraph) {
|
|
412
|
+
const root = Array.from(t.children());
|
|
413
|
+
const ids = dfs(t, root);
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
const props = util.getNodeProps(g.node(id)!);
|
|
416
|
+
if (props.fixedRank) continue;
|
|
417
|
+
const parent = t.parent(id);
|
|
418
|
+
if (!g.hasNode(parent)) continue;
|
|
419
|
+
let edge = g.edge({ source: id, target: parent });
|
|
420
|
+
let flipped = false;
|
|
421
|
+
if (!edge) {
|
|
422
|
+
edge = g.edge({ source: parent, target: id });
|
|
423
|
+
flipped = true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const minLength = util.getEdgeProps(edge!).minLength;
|
|
427
|
+
const rank = util.getNodeProps(g.node(parent)!).rank + (flipped ? minLength : -minLength);
|
|
428
|
+
setRank(props, rank);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Given a spanning tree and a graph, determine the cut value for a node.
|
|
434
|
+
*/
|
|
435
|
+
function calcCutValue(t: FullGraph, g: FullGraph, child: NodeId): number {
|
|
436
|
+
assert(t.hasNode(child), "Child not in tree");
|
|
437
|
+
const parent = t.parent(child);
|
|
438
|
+
assert(t.hasNode(parent), "Root has no cut value");
|
|
439
|
+
let childIsTail = true;
|
|
440
|
+
|
|
441
|
+
let graphEdge: Edge = { source: child, target: parent };
|
|
442
|
+
if (!g.hasEdge(graphEdge)) {
|
|
443
|
+
childIsTail = false;
|
|
444
|
+
graphEdge = { source: parent, target: child };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let cutValue = util.getEdgeProps(graphEdge!).weight;
|
|
448
|
+
|
|
449
|
+
for (const edge of g.edges(child)) {
|
|
450
|
+
const isOutEdge = edge.source === child;
|
|
451
|
+
const other: NodeId = isOutEdge ? edge.target : edge.source;
|
|
452
|
+
|
|
453
|
+
if (other !== parent) {
|
|
454
|
+
const pointsToHead = isOutEdge === childIsTail;
|
|
455
|
+
const otherWeight = util.getEdgeProps(edge).weight;
|
|
456
|
+
|
|
457
|
+
cutValue += pointsToHead ? otherWeight : -otherWeight;
|
|
458
|
+
|
|
459
|
+
const otherEdge: Edge = { source: child, target: other };
|
|
460
|
+
if (t.hasEdge(otherEdge)) {
|
|
461
|
+
const otherCutValue = util.getEdgeProps(t.edge(otherEdge)!).cutValue;
|
|
462
|
+
cutValue += pointsToHead ? -otherCutValue : otherCutValue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return cutValue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Assign initial cut values to a spanning tree edges. Cut value on an edge is
|
|
472
|
+
* the sum of all weights to the source node, minus the sum of all weights to
|
|
473
|
+
* the target node.
|
|
474
|
+
*/
|
|
475
|
+
function initCutValues(t: FullGraph, g: FullGraph): void {
|
|
476
|
+
// Proceed from leaves in, for efficient iterative cut value. See [gansner].
|
|
477
|
+
const nodes = dfs(t, t.children(), Order.POST);
|
|
478
|
+
for (const id of nodes) {
|
|
479
|
+
assignCutValue(t, g, id);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function assignCutValue(t: FullGraph, g: FullGraph, id: NodeId) {
|
|
484
|
+
const parent = t.parent(id);
|
|
485
|
+
if (!t.hasNode(parent)) return;
|
|
486
|
+
const cutValue = calcCutValue(t, g, id);
|
|
487
|
+
let edge = t.edge({ source: id, target: parent });
|
|
488
|
+
if (edge) util.getEdgeProps(edge).cutValue = cutValue;
|
|
489
|
+
edge = t.edge({ target: id, source: parent });
|
|
490
|
+
if (edge) util.getEdgeProps(edge).cutValue = cutValue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Nesting takes an arbitrary graph and ensures all subgraphs are connected, by
|
|
495
|
+
* introducing virtual "border" nodes between distinct subgraphs (those
|
|
496
|
+
* subgraphs having no connections).
|
|
497
|
+
*/
|
|
498
|
+
export const nesting = {
|
|
499
|
+
run(g: FullGraph): void {
|
|
500
|
+
const root = util.addVirtualNode(g, util.Virtual.ROOT, {}, undefined, "_root_");
|
|
501
|
+
const depths = treeDepths(g);
|
|
502
|
+
const height = Math.max(...Object.values(depths)) - 1;
|
|
503
|
+
const nodeSep = 2 * height + 1;
|
|
504
|
+
const weight = sumWeights(g);
|
|
505
|
+
|
|
506
|
+
util.getGraphProps(g).nestingRoot = root;
|
|
507
|
+
util.getGraphProps(g).nodeRankFactor = nodeSep;
|
|
508
|
+
|
|
509
|
+
for (const edge of g.edges()) {
|
|
510
|
+
util.getEdgeProps(g.edge(edge)!).minLength *= nodeSep;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const child of g.children()) {
|
|
514
|
+
nestingDfs(child);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function treeDepths(g: FullGraph): Record<NodeId, number> {
|
|
518
|
+
const depths: Record<NodeId, number> = {};
|
|
519
|
+
function dfs(id: NodeId, depth: number) {
|
|
520
|
+
depths[id] = depth;
|
|
521
|
+
const children = g.children(id);
|
|
522
|
+
for (const child of children) {
|
|
523
|
+
dfs(child, depth + 1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const child of g.children()) {
|
|
527
|
+
dfs(child, 1);
|
|
528
|
+
}
|
|
529
|
+
return depths;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function sumWeights(g: FullGraph): number {
|
|
533
|
+
return Array.from(g.edges())
|
|
534
|
+
.map((e) => util.getEdgeProps(g.edge(e)!))
|
|
535
|
+
.reduce((acc, el) => acc + el.weight, 0);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function nestingDfs(id: NodeId) {
|
|
539
|
+
const children = g.children(id);
|
|
540
|
+
if (children.size === 0) {
|
|
541
|
+
if (id !== root) {
|
|
542
|
+
const rootId = util.makeEdge(root, id, {
|
|
543
|
+
weight: 0,
|
|
544
|
+
minLength: nodeSep,
|
|
545
|
+
});
|
|
546
|
+
g.setEdge(rootId, rootId);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const top = util.addBorderNode(g, util.Border.TOP);
|
|
552
|
+
const bottom = util.addBorderNode(g, util.Border.BOTTOM);
|
|
553
|
+
const layout = util.getNodeProps(g.node(id)!);
|
|
554
|
+
|
|
555
|
+
g.setParent(top, id);
|
|
556
|
+
g.setParent(bottom, id);
|
|
557
|
+
layout.top = top;
|
|
558
|
+
layout.bottom = bottom;
|
|
559
|
+
|
|
560
|
+
for (const child of children) {
|
|
561
|
+
nestingDfs(child);
|
|
562
|
+
|
|
563
|
+
const childLayout = util.getNodeProps(g.node(child)!);
|
|
564
|
+
const childTop = childLayout.top || child;
|
|
565
|
+
const childBottom = childLayout.bottom || child;
|
|
566
|
+
const thisWeight = (childLayout.top ? 1 : 2) * weight;
|
|
567
|
+
const minLength = childTop !== childBottom ? 1 : height - depths[id] + 1;
|
|
568
|
+
|
|
569
|
+
const tct = util.makeEdge(top, childTop, {
|
|
570
|
+
weight: thisWeight,
|
|
571
|
+
minLength,
|
|
572
|
+
nestingEdge: true,
|
|
573
|
+
});
|
|
574
|
+
const cbb = util.makeEdge(childBottom, bottom, {
|
|
575
|
+
weight: thisWeight,
|
|
576
|
+
minLength,
|
|
577
|
+
nestingEdge: true,
|
|
578
|
+
});
|
|
579
|
+
if (top !== childTop) g.setEdge(tct, tct);
|
|
580
|
+
if (childBottom !== bottom) g.setEdge(cbb, cbb);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!g.hasNode(g.parent(id))) {
|
|
584
|
+
const rootTop = util.makeEdge(root, top, {
|
|
585
|
+
weight: 0,
|
|
586
|
+
minLength: height + depths[id],
|
|
587
|
+
});
|
|
588
|
+
g.setEdge(rootTop, rootTop);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
undo(g: FullGraph): void {
|
|
594
|
+
const layout = util.getGraphProps(g);
|
|
595
|
+
if (layout.nestingRoot) g.removeNode(layout.nestingRoot);
|
|
596
|
+
layout.nestingRoot = undefined;
|
|
597
|
+
for (const e of g.edges()) {
|
|
598
|
+
const { nestingEdge } = util.getEdgeProps(g.edge(e)!);
|
|
599
|
+
if (nestingEdge) {
|
|
600
|
+
g.removeEdge(e);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/* Function to safely set the rank of a node. If the node is fixed rank, does
|
|
607
|
+
* nothing. Otherwise, sets the rank to this value. */
|
|
608
|
+
function setRank(props: NodeProperties, rank: number): void {
|
|
609
|
+
assertFinite(rank, `Non-finite rank ${rank}`);
|
|
610
|
+
if (!props.fixedRank) {
|
|
611
|
+
props.rank = rank;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const TEST_ONLY = {
|
|
616
|
+
calcCutValue,
|
|
617
|
+
enterEdge,
|
|
618
|
+
exchangeEdges,
|
|
619
|
+
feasibleTree,
|
|
620
|
+
initCutValues,
|
|
621
|
+
initLowLimValues,
|
|
622
|
+
leaveEdge,
|
|
623
|
+
longestPath,
|
|
624
|
+
nesting,
|
|
625
|
+
networkSimplex,
|
|
626
|
+
normalizeRanks,
|
|
627
|
+
removeEmptyRanks,
|
|
628
|
+
simplify,
|
|
629
|
+
};
|