@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,186 @@
1
+ /**
2
+ * @fileoverview
3
+ *
4
+ * A collection of utilities to ease debugging the layout steps.
5
+ */
6
+
7
+ import { GRAPH_ROOT } from "../../tsabre/graph.ts";
8
+ // import { stringGraph } from "../../tsabre.ts";
9
+ import { stringGraph } from "../../tsabre.ts";
10
+ import type { Edge, FullGraph, Node, NodeId, SmartGraph } from "../../types.ts";
11
+ import type { EdgeProperties, NodeProperties } from "../types.ts";
12
+ import { getEdgeProps, getNodeProps, initOrder } from "../util.ts";
13
+
14
+ /**
15
+ * Print a DOT text for a graph.
16
+ */
17
+ export function printGraph(g: FullGraph): void {
18
+ console.info(stringGraph(g, { graph: () => new Map([["printDebug", "true"]]) }));
19
+ }
20
+
21
+ /**
22
+ * Utility to get an immediate snapshot of the state of the graph layout
23
+ * algorithm's execution.
24
+ */
25
+ export function debugGraph(g: FullGraph, step: string): void {
26
+ const nodes = Array.from(g.nodes)
27
+ .map((id) => [id, getNodeProps(g.node(id)!)] as [string, NodeProperties])
28
+ .map(
29
+ ([
30
+ id,
31
+ {
32
+ rank,
33
+ order,
34
+ geometry: { x, y },
35
+ },
36
+ ]) => ({ id, rank, order, x, y }),
37
+ );
38
+ const layout = initOrder(g).map((r) =>
39
+ r.map((id) => {
40
+ const {
41
+ geometry: { x, y },
42
+ } = getNodeProps(g.node(id)!);
43
+ return `${id} <${x}, ${y}>`;
44
+ }),
45
+ );
46
+
47
+ // Pull cutValue from edgeProps, returning a table with source, target, name,
48
+ // weight, and minLength for each edge.
49
+ const edges = Array.from(g.edges())
50
+ .map((e) => [e, getEdgeProps(e)] as [Edge, EdgeProperties])
51
+ .map(([{ source, target, name }, { weight, minLength }]) => ({
52
+ source,
53
+ target,
54
+ name,
55
+ weight,
56
+ minLength,
57
+ }));
58
+
59
+ console.group(`SmartGraph state after ${step}`);
60
+
61
+ console.group("nodes");
62
+ console.table(nodes);
63
+ console.groupEnd();
64
+
65
+ console.group("edges");
66
+ console.table(edges);
67
+ console.groupEnd();
68
+
69
+ console.group("layout");
70
+ console.table(layout);
71
+ console.groupEnd();
72
+
73
+ console.groupEnd();
74
+ }
75
+
76
+ const NODE: unique symbol = Symbol("Node");
77
+ interface DebugTree extends Record<NodeId, DebugTree> {
78
+ [NODE]?: Node;
79
+ }
80
+
81
+ /**
82
+ * Prints a spanning tree, including [low, lim] of each node indicating children
83
+ * and the cutValue for each edge.
84
+ */
85
+ export function printSpanningTree(g: FullGraph, step = ""): void {
86
+ const treeDFS = (id: NodeId, obj: DebugTree) => {
87
+ obj[id] = { [NODE]: g.node(id) };
88
+ for (const cid of g.children(id)) {
89
+ treeDFS(cid, obj[id]);
90
+ }
91
+ return obj;
92
+ };
93
+
94
+ const nodes = treeDFS(GRAPH_ROOT, {});
95
+
96
+ // Pull cutValue from edgeProps, returning a table with source, target, name,
97
+ // and cutValue for each edge.
98
+ const edges = Array.from(g.allEdges)
99
+ .map((e) => [e, getEdgeProps(e)] as [Edge, EdgeProperties])
100
+ .map(([{ source, target, name }, { cutValue }]) => ({
101
+ source,
102
+ target,
103
+ name,
104
+ cutValue,
105
+ }));
106
+
107
+ console.group(`Spanning tree ${step}`);
108
+
109
+ console.group("nodes");
110
+ console.info(nodes);
111
+ console.groupEnd();
112
+
113
+ console.group("edges");
114
+ console.table(edges);
115
+ console.groupEnd();
116
+
117
+ console.groupEnd();
118
+ }
119
+
120
+ /**
121
+ * Given a layering matrix, print debug information for it.
122
+ */
123
+ export function printLayering(layers: NodeId[][], g: FullGraph): void {
124
+ console.group("Layering");
125
+ console.table(
126
+ layers.map((l) =>
127
+ l.map((id) => {
128
+ const inCount = [...g.inEdges(id)].length;
129
+ const outCount = [...g.outEdges(id)].length;
130
+ return `${inCount}> ${id} >${outCount}`;
131
+ }),
132
+ ),
133
+ );
134
+ console.groupEnd();
135
+ }
136
+
137
+ /**
138
+ * Given a graph and set of FAS buckets, print the FAS state including d(n).
139
+ */
140
+ export function debugFas(
141
+ {
142
+ graph,
143
+ buckets,
144
+ }: {
145
+ graph: SmartGraph<{}, {}>;
146
+ buckets: Array<Array<{ id: string; in: number; out: number }>>;
147
+ },
148
+ results: Edge[] = [],
149
+ ): void {
150
+ console.group("FAS State");
151
+ const b = buckets.reduce(
152
+ (a, bs, i) => {
153
+ if (bs.length > 0) {
154
+ a = a.concat(
155
+ bs.map(({ id, in: n, out }) => ({
156
+ id,
157
+ in: n,
158
+ out,
159
+ d: Math.abs(out - n),
160
+ i,
161
+ })),
162
+ );
163
+ }
164
+ return a;
165
+ },
166
+ [] as Array<{ id: string; in: number; out: number }>,
167
+ );
168
+ console.table(b);
169
+ console.groupEnd();
170
+ console.group("FAS results");
171
+ console.table(
172
+ results.map(({ source, target, name }) => ({
173
+ s: source,
174
+ t: target,
175
+ n: name,
176
+ })),
177
+ );
178
+ console.groupEnd();
179
+ }
180
+
181
+ /**
182
+ * Print an arbitrary value.
183
+ */
184
+ export function print(k: unknown): void {
185
+ console.info(k);
186
+ }
@@ -0,0 +1,266 @@
1
+ import { assert } from "../../assert.ts";
2
+ import { edgeId } from "../../tsabre/graph.ts";
3
+ import type { Edge, FullGraph, NodeId } from "../../types.ts";
4
+ import * as childrenRelative from "../hulls/hulls.ts";
5
+ import { assignSegmentPoints } from "../hulls/segments.ts";
6
+ import {
7
+ getGraphProps,
8
+ getNodeProps,
9
+ readVirtualNodeLayoutCache,
10
+ writeVirtualNodeLayoutCache,
11
+ } from "../util.ts";
12
+
13
+ import { decycle, recycle } from "./acyclic.ts";
14
+ import * as childrenAbsolute from "./children.ts";
15
+ import * as debug from "./debug.ts";
16
+ import { denormalize, normalize } from "./normalize.ts";
17
+ import { order } from "./order.ts";
18
+ import { position } from "./position.ts";
19
+ import { rank } from "./rank.ts";
20
+
21
+ /**
22
+ * Given a graph G, assign (x, y) to each node to result in an efficient and
23
+ * aesthetically pleasing layout.
24
+ *
25
+ * The algorithm itself proceeds in a series of discrete steps, with a pair of
26
+ * simplifying operations applied to make some algorithmic steps possible. Many
27
+ * of the sub-problems are themselves NP-complete to achieve the above goals;
28
+ * therefore, bounded polynomial solutions are used in several places to achieve
29
+ * "good enough" results. Overall, the combined running time of the entire
30
+ * process should be O(VE+VlogV), that is, constrained by ranking in time
31
+ * comensurate to number of nodes & number of edges, and in ordering by time
32
+ * comensurate to number of nodes times the log number of nodes.
33
+ *
34
+ * Layout begins and ends by removing and then restoring cycles in the graph.
35
+ * Ranking, especially, is non-deterministic with cycles present in a graph. By
36
+ * reversing cycle-causing edges, the layout still applies their weights,
37
+ * crossings, etc, but the algorithms are able to assert preconditions that the
38
+ * graphs they receive are acyclic.
39
+ *
40
+ * With an acyclic graph, ranking is the process of assigning a `rank` value
41
+ * such that for every edge n1 -> n2, n1.rank < n2.rank. Additionally, ranking
42
+ * uses a minLength value for each edge (default and minimum 1) such that
43
+ * n1.rank + minLength <= n2.rank. This additional minLength property allows
44
+ * callers some flexibility in controlling the vertical layout & whitespace of
45
+ * the graph.
46
+ *
47
+ * After ranking has been completed, the graph is normalized to collapse all
48
+ * multi-edges into single edges with their aggregate weight. This will be
49
+ * undone after both ordering and positioning, before undoing the acyclic break.
50
+ *
51
+ * With a simplified graph, ordering arranges nodes within a rank in a way that
52
+ * minimizes edge crossings. Additionally, by ordering the virtual nodes, their
53
+ * positions will later be available to route edges "between" gaps in ranks.
54
+ *
55
+ * After ranking and ordering, the final step in positioning assigns a final (x,
56
+ * y) coordinate with emphasis on vertical alignment provided by a weight on
57
+ * each edge, as well as a final whitespace factor provided by nodeSep and
58
+ * rankSep (width and height respectively).
59
+ *
60
+ * Nesting layout (positioning children) is the responsibility of the calling
61
+ * graph; while it can be done at any time, the size of the top-most nodes must
62
+ * be set for positioning to behave appropriately.
63
+ *
64
+ * This implementation draws heavily from
65
+ * [Dagre](https://github.com/dagrejs/dagre), itself a re-implementation of
66
+ * GraphViz. Some benefits provided in this custom solution:
67
+ *
68
+ * - Using modern ES6 data structures and the TypeScript type system, the code
69
+ * is both more performant and maintainable. ES6 data structures are more
70
+ * performant in many cases than their array and object equivalents, because
71
+ * browser optimizations are built specifically for the structures
72
+ * themselves and not for the just-in-time compiled objects case.
73
+ * Additionally, they have stronger runtime and type-system constraints on
74
+ * their usage.
75
+ */
76
+ export function layout(g: FullGraph, triggeredNodeId?: NodeId): void {
77
+ const children =
78
+ // tslint:ignore-next-line:ban-module-namespace-object-escape
79
+ getGraphProps(g).childrenRelative ? childrenRelative : childrenAbsolute;
80
+ const childrenHandler = children.handler();
81
+ const { printDebug } = getGraphProps(g);
82
+ const pipeline = timedPipeline([
83
+ validate,
84
+ // E = |edges|, V = |vertices|, R = |ranks|
85
+ (fg: FullGraph) => {
86
+ children.layout(fg, triggeredNodeId);
87
+ },
88
+ childrenHandler.remove,
89
+ decycle, // Heuristic FAS, O(E)
90
+ rank, // Network simplex, O(VE) but heuristically better than the worst
91
+ // case
92
+ normalize, // O(ER)
93
+ order, // O(VlogV+E) from [Forster]
94
+ writeVirtualNodeLayoutCache,
95
+ position, // O(VE) from [Brendes]
96
+ denormalize, // O(ER)
97
+ recycle, // Very small, the number of edges found in decycle
98
+ childrenHandler.replace,
99
+ assignSegmentPoints,
100
+ ]);
101
+ if (printDebug) console.group("Layout Debug");
102
+ pipeline(g);
103
+ if (printDebug) console.groupEnd();
104
+ }
105
+
106
+ /**
107
+ * Expand/collapse events require only a partial layout recalculation, in
108
+ * particular, positioning and hull layout, which in turn:
109
+ *
110
+ * * position() requires reintroduction of virtual nodes (used to build edges
111
+ * spanning over more than 1 rank) => normalize(), denormalize().
112
+ * * [1] Reintroduced virtual nodes to be properly ordered together with all
113
+ * real nodes => ordering.
114
+ * * Only top-level nodes can participate in ordering, which means all their
115
+ * descendant nodes and their connections should be stashed => handler.remove(),
116
+ * handler.replace().
117
+ * * Segment points need to be updated => assignSegmentPoints().
118
+ * * [2] Decycle can't be skipped because recycle needs to run, and vice versa.
119
+ * Decycling is important because ranking a cyclic graph is non-deterministic.
120
+ * Also, removing decycle() leads to a lot of side-effects for graphs with a lot
121
+ * of horizontal distance between nodes (i.e. a lot of virtual nodes holding
122
+ * that space).
123
+ *
124
+ * [1] - For ordering on partial layout, order() doesn't actually run. On first
125
+ * run of layout(), order() gets called, and the resulting orders, together with
126
+ * ranks, that have been calculated previously, are cached in
127
+ * graph::@@layout::virtualNodeLayout. Those values are subsequently used as a
128
+ * substitute for rank() and order() in partialLayout().
129
+ *
130
+ * [2] - The first time decycling is ran, the FAS, subset of edges that need to
131
+ * be reversed in order to remove all the cycles, is calculated by greedyFas()
132
+ * and cached in graph::@@layout::fas. Next time decycle() gets called on the
133
+ * same graph in partialLayout, greedyFas() doesn't need to run. Instead, the
134
+ * cached FAS is used.
135
+ */
136
+ export function partialLayoutOnNodeEvent(g: FullGraph, triggeredNodeId?: NodeId): void {
137
+ const children =
138
+ // tslint:ignore-next-line:ban-module-namespace-object-escape
139
+ getGraphProps(g).childrenRelative ? childrenRelative : childrenAbsolute;
140
+ const childrenHandler = children.handler();
141
+ const { printDebug } = getGraphProps(g);
142
+ const pipeline = timedPipeline([
143
+ (fg: FullGraph) => {
144
+ children.layout(fg, triggeredNodeId);
145
+ },
146
+ childrenHandler.remove,
147
+ // Removing decycle also leads to weird side-effects, like decrease of the
148
+ // number of virtual nodes
149
+ decycle,
150
+ normalize,
151
+ readVirtualNodeLayoutCache,
152
+ position,
153
+ denormalize,
154
+ recycle,
155
+ childrenHandler.replace,
156
+ assignSegmentPoints,
157
+ ]);
158
+
159
+ if (printDebug) console.group("Partial Layout Debug");
160
+ pipeline(g);
161
+ if (printDebug) console.groupEnd();
162
+ }
163
+
164
+ /**
165
+ * Apply a list of functions to a single graph, capturing timing information for
166
+ * each (when requested by the GraphConfig).
167
+ */
168
+ function timedPipeline(fns: Array<(g: FullGraph) => FullGraph | void>) {
169
+ return (g: FullGraph) => {
170
+ const { timing, printDebug } = getGraphProps(g);
171
+ const timingFn = timing && !printDebug ? time : notime;
172
+ if (printDebug) debug.printGraph(g);
173
+ for (const fn of fns) {
174
+ const res = timingFn(fn)(g);
175
+ if (res !== undefined) {
176
+ g = res;
177
+ }
178
+ if (printDebug) {
179
+ debug.debugGraph(g, fn.name);
180
+ }
181
+ }
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Wrap a function in a pair of calls to console.time/timeEnd, using the
187
+ * function's name for the timer.
188
+ */
189
+ export function time<T, U>(fn: (...args: T[]) => U): (...args: T[]) => U {
190
+ return (...args: T[]) => {
191
+ const name = fn.name;
192
+ let result: U;
193
+ console.time(name);
194
+ try {
195
+ result = fn(...args);
196
+ } finally {
197
+ console.timeEnd(name);
198
+ }
199
+ return result;
200
+ };
201
+ }
202
+
203
+ /** Return the function. For symmetry with time, above. */
204
+ export function notime<T, U>(fn: (...args: T[]) => U): (...args: T[]) => U {
205
+ return fn;
206
+ }
207
+
208
+ /* Validate that the layout graph has no structural issues that might cause the
209
+ * algorithm to finish successfully. For instance, all top-level ranks must
210
+ * point "down" the graph ranks. This is guaranteed by the ranking algorithm,
211
+ * but must be verified if callers have asked for fixed ranks.
212
+ */
213
+ function validate(graph: FullGraph): void {
214
+ const fixed = validateFixedRanks(graph);
215
+ if (fixed) {
216
+ validateRanksPointDown(graph);
217
+ }
218
+ }
219
+
220
+ /* Validate that if any node is fixedRank, then every node is fixedRank */
221
+ function validateFixedRanks(graph: FullGraph): boolean {
222
+ let fixed: boolean | undefined;
223
+ for (const id of graph.children()) {
224
+ const { fixedRank } = getNodeProps(graph.node(id)!);
225
+ if (fixed === undefined) {
226
+ fixed = fixedRank;
227
+ }
228
+ assert(
229
+ fixed === fixedRank,
230
+ `HierarchicalLayout::validateFixedRanks: Top-level nodes must either all be fixedRank or non be fixedRank; ${id} is a goose.`,
231
+ );
232
+ }
233
+ return fixed || false;
234
+ }
235
+
236
+ /* Validate that all top-level segments point "down" the graph, that is, source
237
+ * has a lower rank than target. */
238
+ function validateRanksPointDown(graph: FullGraph): void {
239
+ const violatingEdges = new Set<Edge>();
240
+ for (const segment of graph.topSegments) {
241
+ for (const edge of segment.edges) {
242
+ const sourceParent = graph.ancestors(edge.source).pop()!;
243
+ const targetParent = graph.ancestors(edge.target).pop()!;
244
+ const sourceRank = getNodeProps(graph.node(sourceParent)!).rank;
245
+ const targetRank = getNodeProps(graph.node(targetParent)!).rank;
246
+
247
+ if (sourceRank >= targetRank) {
248
+ violatingEdges.add(edge);
249
+ }
250
+ }
251
+ }
252
+
253
+ if (violatingEdges.size > 0) {
254
+ const firstFew: Array<Edge> = [];
255
+ let i = 0;
256
+ for (const edge of violatingEdges) {
257
+ firstFew.push(edge);
258
+ if (++i > 10) break;
259
+ }
260
+ throw new Error(
261
+ `HierarchicalLayout::validateRanksPointDown: ${
262
+ violatingEdges.size
263
+ } edge(s) point the wrong direction, first few are [${firstFew.map(edgeId).join(", ")}]`,
264
+ );
265
+ }
266
+ }
@@ -0,0 +1,65 @@
1
+ import type { NodeId } from "../../types.ts";
2
+ import { ChildManager } from "../manager/child_manager.ts";
3
+ import { AbstractLayoutManager } from "../manager/manager.ts";
4
+ import type { LayoutGraph } from "../types.ts";
5
+
6
+ import { layout, partialLayoutOnNodeEvent } from "./layout.ts";
7
+
8
+ /**
9
+ * HierarchicalLayoutManager assumes drag functionality from
10
+ * AbstractLayoutManager, and does layout using the Hierarchical layout engine.
11
+ */
12
+
13
+ export class HierarchicalLayoutManager extends AbstractLayoutManager {
14
+ override buildChildManager(): ChildManager {
15
+ return new HierarchicalChildManager(this);
16
+ }
17
+
18
+ /**
19
+ * Rerun the layout.
20
+ * @param triggeredNodeId - See manager/manager.ts::run(). ID of the
21
+ * expanded/collapsed node. Each such event triggers a partial layout
22
+ * recalculation. If no event fired and/or we need the complete layout
23
+ * recalculation, no triggeredNodeId is passed.
24
+ */
25
+ doLayout(triggeredNodeId?: NodeId): void {
26
+ // TODO: Make layout work on ReadonlyLayoutGraph, so that all this
27
+ // can be Readonly
28
+ layout(this.graph, triggeredNodeId);
29
+ }
30
+
31
+ /**
32
+ * Rerun the layout through a simplified pipeline on node expand/collapse.
33
+ */
34
+ runPartial(graph: LayoutGraph, triggeredNodeId: NodeId, panToCenter = true): void {
35
+ this.graph = graph;
36
+
37
+ this.nodePositions.clear();
38
+ this.segmentPositions.clear();
39
+ this.entityTouched.clear();
40
+
41
+ this.doPartialLayout(triggeredNodeId);
42
+ this.updateAt(undefined, panToCenter);
43
+ }
44
+
45
+ /**
46
+ * Partial recalculation of layout on expand/collapse. See
47
+ * hierarchic/layout.ts::partialLayout for full explanation.
48
+ */
49
+ private doPartialLayout(triggeredNodeId?: NodeId) {
50
+ partialLayoutOnNodeEvent(this.graph, triggeredNodeId);
51
+ if (triggeredNodeId) this.updateNodes(triggeredNodeId);
52
+ }
53
+ }
54
+
55
+ class HierarchicalChildManager extends ChildManager {
56
+ private readonly hierarchicManager: HierarchicalLayoutManager;
57
+ constructor(hierarchicManager: HierarchicalLayoutManager) {
58
+ super(hierarchicManager);
59
+ this.hierarchicManager = hierarchicManager;
60
+ }
61
+
62
+ protected override doNodeResize(id: NodeId) {
63
+ this.hierarchicManager.runPartial(this.manager.graph, id);
64
+ }
65
+ }
@@ -0,0 +1,177 @@
1
+ import { assertExists } from "../../assert.ts";
2
+ import { RNGon } from "../../geometry.ts";
3
+ import { EDGE_KEY_DELIM, edgeId } from "../../tsabre/graph.ts";
4
+ import type { Edge, FullGraph, NodeId, Point } from "../../types.ts";
5
+ import type { NodeProperties } from "../types.ts";
6
+ import {
7
+ addVirtualNode,
8
+ getEdgeProps,
9
+ getGraphProps,
10
+ getNodeProps,
11
+ getSegmentProps,
12
+ makeEdge,
13
+ position,
14
+ Virtual,
15
+ } from "../util.ts";
16
+
17
+ /**
18
+ * Given a graph with long edges (spanning more than 1 rank), break it into a
19
+ * chain of virtual nodes. This greatly simplifies order processing &
20
+ * positioning. During layout, this will be performed after ranking, and undone
21
+ * after positioning. During ordering, having a virtual node for each path at
22
+ * every rank is critical for sweeping, so that long edges get pulled out of
23
+ * conflict when possible. During positioning, any virtual nodes that have moved
24
+ * will get positioned using 0 size, and their position can be used as a point
25
+ * on the edge spline.
26
+ */
27
+ export function normalize(g: FullGraph): void {
28
+ const edges = [...g.edges()];
29
+ for (const edge of edges) {
30
+ normalizeEdge(g, edge);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * For each edge, break it into a series of virtual nodes and edges each
36
+ * crossing a single rank. Store the original edge in the edgeLayout for the
37
+ * first virtual node, and store each starting node in the vertical chain in
38
+ * the virtualChains set of the graph.
39
+ */
40
+ function normalizeEdge(g: FullGraph, e: Edge) {
41
+ const gLayout = getGraphProps(g);
42
+ const virtualChains = (gLayout.virtualChains = gLayout.virtualChains || new Set<NodeId>());
43
+ const { source, target, name } = e;
44
+ let sId = source;
45
+ const tId = target;
46
+ let sourceRank = getNodeProps(g.node(sId)!).rank + 1;
47
+ const targetRank = getNodeProps(g.node(tId)!).rank;
48
+ const edgeLayout = getEdgeProps(g.edge(e)!);
49
+ const { weight } = edgeLayout;
50
+ const edge = g.edge(e);
51
+
52
+ if (targetRank === sourceRank) return;
53
+
54
+ g.removeEdge(e);
55
+
56
+ // Using i as an internal index, iterate from source to target rank,
57
+ // generating a new node at each step.
58
+ for (let i = 0; sourceRank < targetRank; ++i, ++sourceRank) {
59
+ // It is crucially important for virtual nodes, created on normalization, to
60
+ // have unique, but reproducible ids. I.e. the id of the virtual node
61
+ // created in the same edge on the same rank should always be the same and
62
+ // not change between different runs of layout() and partialLayout(). This
63
+ // is because new virtual nodes should be associated with their old
64
+ // counterparts, ids of which were recorded in virtualNodeLayout. Here, a
65
+ // virtual node's name depends on the edge it belongs to (which in turn has
66
+ // its own unique name) and on its rank.
67
+ const vIdRank = EDGE_KEY_DELIM + sourceRank.toString();
68
+ const vId = `(϶${EDGE_KEY_DELIM}${edgeId(edge!)}${vIdRank})`;
69
+ const layout: Partial<NodeProperties> = {
70
+ geometry: RNGon.point(),
71
+ rank: sourceRank,
72
+ virtualEdge: edge,
73
+ virtualEdgeLayout: edgeLayout,
74
+ };
75
+
76
+ addVirtualNode(g, Virtual.EDGE, layout, vId);
77
+ const sv = makeEdge(sId, vId, { weight }, name);
78
+ g.setEdge(sv, sv);
79
+ if (i === 0) {
80
+ virtualChains.add(vId);
81
+ }
82
+ sId = vId;
83
+ }
84
+
85
+ const vt = makeEdge(sId, tId, { weight }, name);
86
+ g.setEdge(vt, vt);
87
+ }
88
+
89
+ /**
90
+ * Given a graph with additional virtual nodes added by `normalize`, remove them
91
+ * and restore the original long edges. Use the positions of the virtual nodes
92
+ * as the points the edge should pass through.
93
+ */
94
+ export function denormalize(g: FullGraph): void {
95
+ removeVirtualChains(g);
96
+ }
97
+
98
+ function removeVirtualChains(g: FullGraph) {
99
+ const chains = getGraphProps(g).virtualChains || new Set();
100
+ for (let vId of chains) {
101
+ let node = g.node(vId)!;
102
+
103
+ // Reset the original edge
104
+ const { virtualEdge, virtualEdgeLayout } = getNodeProps(node);
105
+
106
+ // Clear old points
107
+ virtualEdgeLayout!.points = [];
108
+ const vEdge = makeEdge(
109
+ virtualEdge!.source,
110
+ virtualEdge!.target,
111
+ virtualEdgeLayout!,
112
+ virtualEdge!.name,
113
+ );
114
+ g.setEdge(vEdge, virtualEdge!);
115
+
116
+ // Remove all the nodes, and gather their positions for the edge.
117
+ let next: NodeId;
118
+ while (getNodeProps(node).virtual) {
119
+ next = Array.from(g.successors(vId))[0];
120
+ const { x, y } = getNodeProps(g.node(vId)!).geometry;
121
+ g.removeNode(vId);
122
+ virtualEdgeLayout!.points.push({ x, y });
123
+ vId = next;
124
+ node = g.node(vId)!;
125
+ }
126
+ }
127
+
128
+ chains.clear();
129
+
130
+ // Now that all chains have been devirtualized, reverse points as necessary
131
+ // and then normalize the segments underlying the edges.
132
+ for (const e of g.edges()) {
133
+ const edge = getEdgeProps(g.edge(e)!);
134
+ if (edge.reversed) {
135
+ edge.points.reverse();
136
+ }
137
+ // At this point, `g` is still the reduced graph with edges mapping 1:1 to
138
+ // top level segments.
139
+ if (!g.hasNode(g.parent(e.source)) && !g.hasNode(g.parent(e.target))) {
140
+ normalizeSegment(g, e);
141
+ }
142
+ }
143
+ }
144
+
145
+ function normalizeSegment(graph: FullGraph, edge: Edge) {
146
+ // Find the matching top level segment, and attach the points there as well.
147
+ // There is exactly one matching top segment for each edge before
148
+ // denormalization.
149
+ const segment = assertExists(
150
+ graph.topSegment(edge.source, edge.target),
151
+ `hierarchic/normalize.ts::normalizeSegment: No top level segment for ${edge.source} -> ${edge.target}`,
152
+ );
153
+ const layout = getEdgeProps(graph.edge(edge)!);
154
+
155
+ // This is still normalizing a reduced graph, so the passed edge aligns with
156
+ // the top level segment.
157
+ const edgeAlignedWithSegment = segment.source === edge.source;
158
+
159
+ // Push (possibly back to front) into an array. spread and reverse are costly.
160
+ const points: Point[] = (getSegmentProps(segment).points = []);
161
+ points.push(position(graph.node(segment.source)!));
162
+
163
+ // If edge is aligned with the segment and the layout is not reversed, push
164
+ // points in order. If the edge is not aligned with the layout but the edge
165
+ // is reversed, push the points in order. And vice verse. This is actually a
166
+ // place to use a logical XOR.
167
+ if (edgeAlignedWithSegment !== layout.reversed) {
168
+ for (const point of layout.points) {
169
+ points.push(point);
170
+ }
171
+ } else {
172
+ for (let i = layout.points.length - 1; i >= 0; i--) {
173
+ points.push(layout.points[i]);
174
+ }
175
+ }
176
+ points.push(position(graph.node(segment.target)!));
177
+ }