@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,321 @@
|
|
|
1
|
+
import type { Edge, FullGraph, NodeId } from "../../types.ts";
|
|
2
|
+
import * as util from "../util.ts";
|
|
3
|
+
import { initOrder } from "../util.ts";
|
|
4
|
+
|
|
5
|
+
import * as debug from "./debug.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Given a graph with `rank` computed, determine an ideal ordering for nodes
|
|
9
|
+
* within each rank, heuristically minimizing edge crosses. While NP-complete,
|
|
10
|
+
* there are a number of heuristic approaches which all boil down to
|
|
11
|
+
* polynomial bounded "best attempt" solutions.
|
|
12
|
+
*
|
|
13
|
+
* The general solution is reducible to the problem of minimizing crossings
|
|
14
|
+
* for two input layers with arbitrary edges between the layers, and then
|
|
15
|
+
* "sweeping" that pass up and down the rank layers, stopping at either N (=4)
|
|
16
|
+
* improvements or when no improvement is found. Within each sweep, one layer
|
|
17
|
+
* (bottom for sweeping down or top for sweeping up) is permuted, following a
|
|
18
|
+
* similar polynomial cutoff.
|
|
19
|
+
*
|
|
20
|
+
* See [Forster], [Junger], and [Barth] papers linked in docs/tsabre for details
|
|
21
|
+
* & proofs of correctness & efficiency.
|
|
22
|
+
*/
|
|
23
|
+
export function order(g: FullGraph): void {
|
|
24
|
+
const { printDebug } = util.getGraphProps(g);
|
|
25
|
+
|
|
26
|
+
// initOrder returns a 2-d array of NodeIds, where row is the rank of the
|
|
27
|
+
// node, and the node will be put in the column which is its ordering.
|
|
28
|
+
const layering = initOrder(g);
|
|
29
|
+
|
|
30
|
+
// Assign the initial layering into the graph, so that there's something to
|
|
31
|
+
// start with.
|
|
32
|
+
assignOrder(g, layering);
|
|
33
|
+
|
|
34
|
+
if (printDebug) {
|
|
35
|
+
debug.printLayering(layering, g);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let bestCC = Number.POSITIVE_INFINITY;
|
|
39
|
+
let best = layering.map((r) => r.slice(0)); // Copy the layering
|
|
40
|
+
|
|
41
|
+
// i tracks the iteration step, while lastBest tracks the number of
|
|
42
|
+
// iterations since a better ordering was found.
|
|
43
|
+
for (
|
|
44
|
+
let i = 0, lastBest = 0;
|
|
45
|
+
// If bestCC is 0 crossings, end early.
|
|
46
|
+
bestCC > 0 &&
|
|
47
|
+
// If after four sweeps there hasn't been an improvement, call it good.
|
|
48
|
+
lastBest < 4 &&
|
|
49
|
+
// End at 24 iterations, max.
|
|
50
|
+
i < 24;
|
|
51
|
+
++i, ++lastBest
|
|
52
|
+
) {
|
|
53
|
+
reorder(g, i, layering);
|
|
54
|
+
|
|
55
|
+
const cc = crossCount(g, layering);
|
|
56
|
+
if (cc < bestCC) {
|
|
57
|
+
// Counted fewer crossings, so reset the lastBest count and record
|
|
58
|
+
// the new best crossing.
|
|
59
|
+
lastBest = 0;
|
|
60
|
+
best = layering.map((r) => r.slice(0)); // Copy the layering
|
|
61
|
+
bestCC = cc;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Copy the best ordering into the graph.
|
|
66
|
+
assignOrder(g, best);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function reorder(g: FullGraph, i: number, layering: NodeId[][]) {
|
|
70
|
+
// sweepLayerGraphs will assign an ordering to each node. When sweeping,
|
|
71
|
+
// go down bias left, up bias left, down bias right, up bias right, and
|
|
72
|
+
// repeat.
|
|
73
|
+
const topToBottom = i % 2 === 0;
|
|
74
|
+
const biasRight = i % 4 >= 2;
|
|
75
|
+
if (topToBottom) {
|
|
76
|
+
sweepLayerGraphsDown(g, layering, biasRight);
|
|
77
|
+
} else {
|
|
78
|
+
sweepLayerGraphsUp(g, layering, biasRight);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Given a computed layers matrix, assign those indices back into the node's
|
|
84
|
+
* `order` property.
|
|
85
|
+
*/
|
|
86
|
+
function assignOrder(g: FullGraph, layers: NodeId[][]): void {
|
|
87
|
+
for (let layer of layers) {
|
|
88
|
+
layer = layer || []; // some situations which can result in empty ranks
|
|
89
|
+
for (let i = 0; i < layer.length; i++) {
|
|
90
|
+
if (layer[i] && g.hasNode(layer[i])) {
|
|
91
|
+
util.getNodeProps(g.node(layer[i])!).order = i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Given a list of "layer" graphs, that is, having only nodes on adjacent ranks
|
|
99
|
+
* with edges between them, "sweep" through the list reordering nodes to reduce
|
|
100
|
+
* crossings.
|
|
101
|
+
*/
|
|
102
|
+
function sweepLayerGraphsDown(g: FullGraph, layering: NodeId[][], biasRight: boolean): void {
|
|
103
|
+
for (let i = 0; i < layering.length - 1; i++) {
|
|
104
|
+
sortLayer(
|
|
105
|
+
g,
|
|
106
|
+
layering,
|
|
107
|
+
i + 1,
|
|
108
|
+
(id: NodeId) => g.inEdges(id),
|
|
109
|
+
({ source }) => source,
|
|
110
|
+
biasRight,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sweepLayerGraphsUp(g: FullGraph, layering: NodeId[][], biasRight: boolean): void {
|
|
116
|
+
for (let i = layering.length - 1; i > 0; i--) {
|
|
117
|
+
sortLayer(
|
|
118
|
+
g,
|
|
119
|
+
layering,
|
|
120
|
+
i - 1,
|
|
121
|
+
(id: NodeId) => g.outEdges(id),
|
|
122
|
+
({ target }) => target,
|
|
123
|
+
biasRight,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function updateOrdering(g: FullGraph, sources: NodeId[]): void {
|
|
129
|
+
for (let i = 0; i < sources.length; i++) {
|
|
130
|
+
const id = sources[i];
|
|
131
|
+
util.getNodeProps(g.node(id)!).order = i;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Given a graph and a layering, count the number of crossing edges.
|
|
137
|
+
*
|
|
138
|
+
* Uses an O(E log V) algorithm from Barth et al that builds an accumulator tree
|
|
139
|
+
* to count crossings as they are inserted & discoverd.
|
|
140
|
+
*/
|
|
141
|
+
function crossCount(g: FullGraph, layering: NodeId[][]): number {
|
|
142
|
+
let cc = 0;
|
|
143
|
+
for (let i = 1; i < layering.length; i++) {
|
|
144
|
+
cc += twoLayerCrossCounting(layering[i - 1], layering[i]);
|
|
145
|
+
}
|
|
146
|
+
return cc;
|
|
147
|
+
|
|
148
|
+
function twoLayerCrossCounting(top: NodeId[], bottom: NodeId[]): number {
|
|
149
|
+
const bottomEntries = sortLayers(g, top, bottom);
|
|
150
|
+
|
|
151
|
+
// Prepare accumulator tree, a binary tree large enough to build a
|
|
152
|
+
// complete tree containing all nodes in `bottom`.
|
|
153
|
+
let firstIndex = 1;
|
|
154
|
+
while (firstIndex < bottom.length) firstIndex <<= 1;
|
|
155
|
+
const treeSize = 2 * firstIndex - 1;
|
|
156
|
+
firstIndex -= 1;
|
|
157
|
+
const tree = new Array(treeSize).fill(0);
|
|
158
|
+
|
|
159
|
+
// Count crossings with weight, eg if a weight 1 and weight 2 edge cross,
|
|
160
|
+
// the total cross counting is weight 3. This implies crossings are
|
|
161
|
+
// preferred on lower weight edges.
|
|
162
|
+
let cc = 0;
|
|
163
|
+
for (const { pos, weight } of bottomEntries) {
|
|
164
|
+
let index = pos + firstIndex;
|
|
165
|
+
tree[index] += weight;
|
|
166
|
+
let weightSum = 0;
|
|
167
|
+
while (index > 0) {
|
|
168
|
+
if (index % 2) {
|
|
169
|
+
weightSum += tree[index + 1];
|
|
170
|
+
}
|
|
171
|
+
index = (index - 1) >> 1;
|
|
172
|
+
tree[index] += weight;
|
|
173
|
+
}
|
|
174
|
+
cc += weight * weightSum;
|
|
175
|
+
}
|
|
176
|
+
return cc;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Return a list including the position and weight of all edges, sorted by
|
|
180
|
+
// the edge's location in the top layer, then by its position in the bottom
|
|
181
|
+
// layer.
|
|
182
|
+
function sortLayers(
|
|
183
|
+
g: FullGraph,
|
|
184
|
+
top: NodeId[],
|
|
185
|
+
bottom: NodeId[],
|
|
186
|
+
): Array<{ pos: number; weight: number }> {
|
|
187
|
+
// Map the lower layer's IDs to their position in the list.
|
|
188
|
+
const bottomPositions = bottom.reduce(
|
|
189
|
+
(a, id, i) => {
|
|
190
|
+
a[id] = i;
|
|
191
|
+
return a;
|
|
192
|
+
},
|
|
193
|
+
{} as Record<NodeId, number>,
|
|
194
|
+
);
|
|
195
|
+
return top
|
|
196
|
+
.map((id) =>
|
|
197
|
+
Array.from(g.outEdges(id))
|
|
198
|
+
.map(getBottomEdge)
|
|
199
|
+
.sort((a, b) => a.pos - b.pos),
|
|
200
|
+
)
|
|
201
|
+
.reduce((a, i) => a.concat(i), []);
|
|
202
|
+
|
|
203
|
+
function getBottomEdge(e: Edge): { pos: number; weight: number } {
|
|
204
|
+
return {
|
|
205
|
+
pos: bottomPositions[e.target],
|
|
206
|
+
weight: util.getEdgeProps(g.edge(e)!).weight,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A barycenter encodes a 1-d coordinate & weighting against it.
|
|
214
|
+
*/
|
|
215
|
+
export interface Barycenter {
|
|
216
|
+
id: NodeId;
|
|
217
|
+
barycenter?: number;
|
|
218
|
+
weight?: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Given a set of nodes in a graph, find the horizontal center for each node.
|
|
223
|
+
* The horizontal center is the point that keeps it equally placed between all
|
|
224
|
+
* incoming node edges, obeying weightedness of edges. From [Forster],
|
|
225
|
+
* is defined as the arithmetic mean of the relative positions of its
|
|
226
|
+
* neighbors b(v) = 1 / |N(v)| ∑v∈N(v) pos(v)
|
|
227
|
+
*/
|
|
228
|
+
function barycenter(
|
|
229
|
+
g: FullGraph,
|
|
230
|
+
nodes: NodeId[],
|
|
231
|
+
edgeFn: (n: NodeId) => Iterable<Edge>,
|
|
232
|
+
neighborFn: (edge: Edge) => NodeId,
|
|
233
|
+
): Barycenter[] {
|
|
234
|
+
function findCenter(id: NodeId): Barycenter {
|
|
235
|
+
const neighbors = new Set(edgeFn(id));
|
|
236
|
+
if (neighbors.size === 0) return { id };
|
|
237
|
+
let sum = 0;
|
|
238
|
+
let weight = 0;
|
|
239
|
+
for (const e of neighbors) {
|
|
240
|
+
const nid = neighborFn(e);
|
|
241
|
+
const order = util.getNodeProps(g.node(nid)!).order;
|
|
242
|
+
const edgeWeight = util.getEdgeProps(g.edge(e)!).weight;
|
|
243
|
+
sum += edgeWeight * order;
|
|
244
|
+
weight += edgeWeight;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
id,
|
|
248
|
+
barycenter: sum / weight,
|
|
249
|
+
weight,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return nodes.filter((id) => g.hasNode(id)).map(findCenter);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Given a graph and layering, sort one layer based on barycenters (with
|
|
258
|
+
* neighbors selected via edgeFn |> neighborFn). Updates layering and the graph
|
|
259
|
+
* in place.
|
|
260
|
+
*/
|
|
261
|
+
function sortLayer(
|
|
262
|
+
g: FullGraph,
|
|
263
|
+
layering: NodeId[][],
|
|
264
|
+
sweep: number,
|
|
265
|
+
edgeFn: (n: NodeId) => Iterable<Edge>,
|
|
266
|
+
neighborFn: (edge: Edge) => NodeId,
|
|
267
|
+
biasRight: boolean,
|
|
268
|
+
): void {
|
|
269
|
+
const centers = barycenter(g, layering[sweep], edgeFn, neighborFn);
|
|
270
|
+
centers.sort(barycenterComparator(layering[sweep], biasRight));
|
|
271
|
+
layering[sweep] = centers.map(({ id }) => id);
|
|
272
|
+
// update ordering in g for next sweep pass
|
|
273
|
+
updateOrdering(g, layering[sweep]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns a comparator function which sorts two Barycenters. If the centers are
|
|
278
|
+
* equal, bias determines which order to place the nodes in. This is a complete
|
|
279
|
+
* ordering, no two nodes will have a stable sort value.
|
|
280
|
+
*/
|
|
281
|
+
function barycenterComparator(
|
|
282
|
+
original: NodeId[],
|
|
283
|
+
bias: boolean,
|
|
284
|
+
): (a: Barycenter, b: Barycenter) => number {
|
|
285
|
+
return (a: Barycenter, b: Barycenter) => {
|
|
286
|
+
if (a === b) return 0; // Allow stable sort
|
|
287
|
+
|
|
288
|
+
const baryA = a.barycenter || 0;
|
|
289
|
+
const baryB = b.barycenter || 0;
|
|
290
|
+
|
|
291
|
+
// Normalize to just the sign bit.
|
|
292
|
+
const sort = Math.sign(baryA - baryB);
|
|
293
|
+
if (sort !== 0) return sort;
|
|
294
|
+
|
|
295
|
+
// This is necessary for consistent ordering, because the order of arguments
|
|
296
|
+
// to the compare function is implementation dependant. In practice, that
|
|
297
|
+
// means chrome and firefox end up with the same overall results, but with
|
|
298
|
+
// each row reversed from the other.
|
|
299
|
+
//
|
|
300
|
+
// This probably makes the entire algorithm O(N log(N^2)). Or maybe
|
|
301
|
+
// O(N^2 log(N)). I'm not really sure how to handle a log(N) operation which
|
|
302
|
+
// itself uses N time.
|
|
303
|
+
const baryAIdx = original.indexOf(a.id);
|
|
304
|
+
const baryBIdx = original.indexOf(b.id);
|
|
305
|
+
if (bias) {
|
|
306
|
+
return baryBIdx - baryAIdx;
|
|
307
|
+
} else {
|
|
308
|
+
return baryAIdx - baryBIdx;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Functions for tests. */
|
|
314
|
+
export const TEST_ONLY = {
|
|
315
|
+
assignOrder,
|
|
316
|
+
crossCount,
|
|
317
|
+
barycenter,
|
|
318
|
+
sortLayer,
|
|
319
|
+
barycenterComparator,
|
|
320
|
+
initOrder,
|
|
321
|
+
};
|