@davidsouther/agathon 0.0.0-stage → 0.0.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.
Files changed (62) hide show
  1. package/package.json +19 -4
  2. package/src/assert.ts +67 -0
  3. package/src/geometry/box.ts +76 -0
  4. package/src/geometry/circle.ts +296 -0
  5. package/src/geometry/geometry.ts +318 -0
  6. package/src/geometry/path.ts +315 -0
  7. package/src/geometry/quadtree.ts +321 -0
  8. package/src/geometry/rectangle.ts +129 -0
  9. package/src/geometry/rngon.ts +132 -0
  10. package/src/geometry/types.ts +102 -0
  11. package/src/geometry.ts +61 -0
  12. package/src/highlight/highlight.ts +85 -0
  13. package/src/index.ts +5 -0
  14. package/src/layout/grid/manager.ts +55 -0
  15. package/src/layout/grid.ts +1 -0
  16. package/src/layout/hierarchic/README.md +21 -0
  17. package/src/layout/hierarchic/acyclic.ts +249 -0
  18. package/src/layout/hierarchic/children.ts +169 -0
  19. package/src/layout/hierarchic/debug.ts +186 -0
  20. package/src/layout/hierarchic/layout.ts +266 -0
  21. package/src/layout/hierarchic/manager.ts +65 -0
  22. package/src/layout/hierarchic/normalize.ts +177 -0
  23. package/src/layout/hierarchic/order.ts +321 -0
  24. package/src/layout/hierarchic/position.ts +514 -0
  25. package/src/layout/hierarchic/rank.ts +629 -0
  26. package/src/layout/hierarchic.ts +2 -0
  27. package/src/layout/hulls/arrange.ts +94 -0
  28. package/src/layout/hulls/hulls.ts +243 -0
  29. package/src/layout/hulls/segments.ts +174 -0
  30. package/src/layout/hulls.ts +3 -0
  31. package/src/layout/manager/child_manager.ts +42 -0
  32. package/src/layout/manager/drag_manager.ts +112 -0
  33. package/src/layout/manager/manager.ts +425 -0
  34. package/src/layout/manager/pan_manager.ts +146 -0
  35. package/src/layout/manager/visibility_manager.ts +84 -0
  36. package/src/layout/manager/zoom_manager.ts +263 -0
  37. package/src/layout/manager.ts +6 -0
  38. package/src/layout/properties/defaults.ts +99 -0
  39. package/src/layout/properties/symbol.ts +99 -0
  40. package/src/layout/properties/weak_map.ts +73 -0
  41. package/src/layout/properties.ts +111 -0
  42. package/src/layout/types.ts +470 -0
  43. package/src/layout/util.ts +444 -0
  44. package/src/layout.ts +6 -0
  45. package/src/samples/diff_graph.ts +15 -0
  46. package/src/samples/edge_cases.ts +168 -0
  47. package/src/samples/gansner_with_states.ts +67 -0
  48. package/src/samples/load_generator.ts +213 -0
  49. package/src/samples/search_graph.ts +35 -0
  50. package/src/tsabre/algo.ts +126 -0
  51. package/src/tsabre/builder.ts +97 -0
  52. package/src/tsabre/compress.ts +15 -0
  53. package/src/tsabre/dijkstra/dijkstra.ts +76 -0
  54. package/src/tsabre/dijkstra/heap.ts +83 -0
  55. package/src/tsabre/graph.ts +808 -0
  56. package/src/tsabre/parse.ts +634 -0
  57. package/src/tsabre/print.ts +188 -0
  58. package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
  59. package/src/tsabre/sourcebuilder/printable.ts +33 -0
  60. package/src/tsabre.ts +6 -0
  61. package/src/types.ts +424 -0
  62. 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
+ };
@@ -0,0 +1,2 @@
1
+ export { layout } from "./hierarchic/layout.ts";
2
+ export { HierarchicalLayoutManager } from "./hierarchic/manager.ts";