@davidsouther/agathon 0.0.0-stage → 2026.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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
+ };