@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.
- 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,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
|
+
}
|