@contraagent/elements 0.9.1 → 0.9.2

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.
@@ -0,0 +1,338 @@
1
+ // The arrangement as it was first written, pair by pair and with linear searches: the
2
+ // faster one in arrange.ts must give the same result.
3
+ import type { Arrangement, EdgeStyle, GraphOrder, EdgePathStep } from './types.js';
4
+ import type { GraphShape } from './arrange.js';
5
+
6
+ const push = <K, V>(m: Map<K, V[]>, k: K, v: V): void => {
7
+ const xs = m.get(k);
8
+ if (xs) xs.push(v);
9
+ else m.set(k, [v]);
10
+ };
11
+
12
+ const pairKey = (a: string, b: string) => `${a}\u0000${b}`;
13
+
14
+ /**
15
+ * Each id's layer by longest path. Edges that close a cycle (found depth first, in
16
+ * `ids` order) are left out; `fixed` ids keep their layer.
17
+ */
18
+ function rankLayers(ids: readonly string[], edges: readonly (readonly [string, string])[], fixed: ReadonlyMap<string, number>): Map<string, number> {
19
+ const known = new Set(ids);
20
+ const all = new Map<string, string[]>(ids.map((id) => [id, []]));
21
+ for (const [a, b] of edges) if (a !== b && known.has(a) && known.has(b) && !all.get(a)?.includes(b)) all.get(a)?.push(b);
22
+ const back = new Set<string>();
23
+ const seen = new Map<string, 1 | 2>();
24
+ for (const root of ids) {
25
+ if (seen.has(root)) continue;
26
+ seen.set(root, 1);
27
+ const stack: [string, number][] = [[root, 0]];
28
+ while (stack.length) {
29
+ const top = stack[stack.length - 1] as [string, number];
30
+ const ss = all.get(top[0]) ?? [];
31
+ if (top[1] < ss.length) {
32
+ const s = ss[top[1]++] as string;
33
+ const st = seen.get(s);
34
+ if (st === 1) back.add(pairKey(top[0], s));
35
+ else if (!st) {
36
+ seen.set(s, 1);
37
+ stack.push([s, 0]);
38
+ }
39
+ } else {
40
+ seen.set(top[0], 2);
41
+ stack.pop();
42
+ }
43
+ }
44
+ }
45
+ const succ = new Map<string, string[]>(ids.map((id) => [id, []]));
46
+ const pred = new Map<string, string[]>(ids.map((id) => [id, []]));
47
+ for (const [a, ss] of all) {
48
+ for (const b of ss) {
49
+ if (back.has(pairKey(a, b))) continue;
50
+ succ.get(a)?.push(b);
51
+ pred.get(b)?.push(a);
52
+ }
53
+ }
54
+ // Longest path, in topological order (Kahn's).
55
+ const indeg = new Map(ids.map((id) => [id, pred.get(id)?.length ?? 0]));
56
+ const queue = ids.filter((id) => !indeg.get(id));
57
+ const layer = new Map<string, number>();
58
+ for (let q = queue.shift(); q !== undefined; q = queue.shift()) {
59
+ layer.set(q, fixed.get(q) ?? Math.max(0, ...(pred.get(q) ?? []).map((p) => (layer.get(p) ?? 0) + 1)));
60
+ for (const s of succ.get(q) ?? []) {
61
+ indeg.set(s, (indeg.get(s) ?? 0) - 1);
62
+ if (!indeg.get(s)) queue.push(s);
63
+ }
64
+ }
65
+ return layer;
66
+ }
67
+
68
+ /** Pairs (a, b) between adjacent layers that cross, for the order `pos`. */
69
+ function crossings(rows: readonly string[][], links: readonly (readonly [string, string])[], pos: ReadonlyMap<string, number>, layerOf: ReadonlyMap<string, number>): number {
70
+ let n = 0;
71
+ const by = new Map<number, [number, number][]>();
72
+ for (const [a, b] of links) {
73
+ const l = layerOf.get(a);
74
+ if (l === undefined) continue;
75
+ push(by, l, [pos.get(a) ?? 0, pos.get(b) ?? 0]);
76
+ }
77
+ for (const ls of by.values())
78
+ for (let i = 0; i < ls.length; i++)
79
+ for (let j = i + 1; j < ls.length; j++) {
80
+ const [a1, b1] = ls[i] as [number, number];
81
+ const [a2, b2] = ls[j] as [number, number];
82
+ if ((a1 - a2) * (b1 - b2) < 0) n++;
83
+ }
84
+ return rows.length ? n : 0;
85
+ }
86
+
87
+ /** True for a point where an edge crosses a layer. */
88
+ const isPoint = (id: string): boolean => id.startsWith('~');
89
+
90
+ /**
91
+ * The order within each layer that reduces crossings: barycentre sweeps, down and
92
+ * up, four times, keeping the best. `links` join a slot to one in the next layer.
93
+ * With `keep`, those ids keep their order (a previous layout's, new ones after
94
+ * them) and only the other slots (the points) move between them.
95
+ */
96
+ function orderRows(rows: string[][], links: readonly (readonly [string, string])[], keep: boolean): string[][] {
97
+ const layerOf = new Map<string, number>();
98
+ rows.forEach((row, i) => row.forEach((id) => layerOf.set(id, i)));
99
+ const pred = new Map<string, string[]>();
100
+ const succ = new Map<string, string[]>();
101
+ for (const [a, b] of links) {
102
+ push(succ, a, b);
103
+ push(pred, b, a);
104
+ }
105
+ const pos = new Map<string, number>();
106
+ const fix = (row: readonly string[]) => row.forEach((id, i) => pos.set(id, i));
107
+ const cur = rows.map((r) => [...r]);
108
+ cur.forEach(fix);
109
+ const score = () => crossings(cur, links, pos, layerOf);
110
+ let best = cur.map((r) => [...r]);
111
+ let bestN = score();
112
+ const sortRow = (row: string[], near: Map<string, string[]>): string[] => {
113
+ const bc = new Map<string, number>();
114
+ for (const id of row) {
115
+ const ps = (near.get(id) ?? []).map((p) => pos.get(p)).filter((x): x is number => x !== undefined);
116
+ bc.set(id, ps.length ? ps.reduce((a, b) => a + b, 0) / ps.length : (pos.get(id) ?? 0));
117
+ }
118
+ const at = (id: string) => bc.get(id) ?? 0;
119
+ if (!keep) return [...row].sort((a, b) => at(a) - at(b) || (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
120
+ // The kept ids stay in their order; each point goes before the first kept id it is left of.
121
+ const fixed = row.filter((id) => !isPoint(id));
122
+ const points = row.filter(isPoint).sort((a, b) => at(a) - at(b) || (pos.get(a) ?? 0) - (pos.get(b) ?? 0));
123
+ const out: string[] = [];
124
+ let p = 0;
125
+ for (const id of fixed) {
126
+ while (p < points.length && at(points[p] as string) < at(id)) out.push(points[p++] as string);
127
+ out.push(id);
128
+ }
129
+ return [...out, ...points.slice(p)];
130
+ };
131
+ for (let sweep = 0; sweep < 4; sweep++) {
132
+ const down = sweep % 2 === 0;
133
+ const order = down ? cur.map((_, i) => i).slice(1) : cur.map((_, i) => i).reverse().slice(1);
134
+ for (const i of order) {
135
+ const row = sortRow(cur[i] as string[], down ? pred : succ);
136
+ cur[i] = row;
137
+ fix(row);
138
+ }
139
+ const n = score();
140
+ if (n < bestN) {
141
+ bestN = n;
142
+ best = cur.map((r) => [...r]);
143
+ }
144
+ }
145
+ return score() <= bestN ? cur : best;
146
+ }
147
+
148
+ export function arrangeBefore(g: GraphShape, prev: GraphOrder | undefined, down: boolean): Arrangement {
149
+ const clusterIds = new Set(g.clusters.map((c) => c.id));
150
+ const parentOf = new Map(g.clusters.map((c) => [c.id, c.parent]));
151
+ const boxOf = new Map<string, string>(); // node id -> its innermost cluster
152
+ for (const n of g.nodes) if (n.cluster) boxOf.set(n.id, n.cluster);
153
+ const stubIds = [...new Set(g.stubs.map((s) => s.id))];
154
+ const isStub = new Set(stubIds);
155
+
156
+ // The clusters an id sits in, innermost first (for a cluster id, itself first).
157
+ const boxPath = (id: string): string[] => {
158
+ const out: string[] = [];
159
+ let b: string | null = clusterIds.has(id) ? id : (boxOf.get(id) ?? null);
160
+ while (b) {
161
+ out.push(b);
162
+ b = parentOf.get(b) ?? null;
163
+ }
164
+ return out;
165
+ };
166
+ // The direct child of `container` (a cluster, or '' for the top level) that holds a node `id`.
167
+ const itemIn = (id: string, container: string): string | undefined => {
168
+ const bp = boxPath(id);
169
+ if (container === '') return bp[bp.length - 1] ?? id;
170
+ const i = bp.indexOf(container);
171
+ return i < 0 ? undefined : i === 0 ? id : bp[i - 1];
172
+ };
173
+ // The deepest cluster holding both ends of an edge (the top level when none).
174
+ const commonBox = (a: string, b: string): string => {
175
+ const bp = boxPath(a);
176
+ return bp.find((x) => boxPath(b).includes(x)) ?? '';
177
+ };
178
+ // The container a slot is in: a stub's is the top level.
179
+ const containerOf = (id: string): string => (isStub.has(id) ? '' : (boxOf.get(id) ?? ''));
180
+
181
+ // Layers, by container: the top level, then each cluster (its own nodes and the
182
+ // clusters nested in it). Each cluster is one item of its parent.
183
+ const containers = ['', ...g.clusters.map((c) => c.id)];
184
+ const kidsOf = new Map<string, string[]>(containers.map((c) => [c, []]));
185
+ for (const { id } of g.nodes) {
186
+ for (const c of containers) {
187
+ const it = itemIn(id, c);
188
+ if (it !== undefined && !kidsOf.get(c)?.includes(it)) kidsOf.get(c)?.push(it);
189
+ }
190
+ }
191
+ const lay = new Map<string, Map<string, number>>();
192
+ for (const c of containers) {
193
+ const kids = kidsOf.get(c) ?? [];
194
+ const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
195
+ const fixed = new Map<string, number>();
196
+ prevRows?.forEach((row, i) => row.forEach((id) => kids.includes(id) && fixed.set(id, i)));
197
+ // An edge that closes a cycle does not set the layers.
198
+ const es = g.edges
199
+ .filter((e) => e.ranks && commonBox(e.from, e.to) === c)
200
+ .map((e) => [itemIn(e.from, c) as string, itemIn(e.to, c) as string] as const);
201
+ lay.set(c, rankLayers(kids, es, fixed));
202
+ }
203
+ // Stubs, in the top level: in the layer before the first they lead to (a new first layer when that is the first).
204
+ const top = lay.get('') as Map<string, number>;
205
+ const prevTop = new Map<string, number>();
206
+ prev?.layers.forEach((row, i) => row.forEach((id) => prevTop.set(id, i)));
207
+ const stubAt = new Map(
208
+ stubIds.map((id) => [id, prevTop.get(id) ?? Math.min(...g.stubs.filter((s) => s.id === id).map((s) => top.get(itemIn(s.to, '') as string) ?? 0)) - 1]),
209
+ );
210
+ if ([...stubAt.values()].some((l) => l < 0)) {
211
+ for (const [id, l] of top) top.set(id, l + 1);
212
+ for (const [id, l] of stubAt) stubAt.set(id, l + 1);
213
+ }
214
+ for (const [id, l] of stubAt) top.set(id, l);
215
+ const layerOf = (c: string, id: string): number => lay.get(c)?.get(id) ?? 0;
216
+ const lastLayer = (c: string): number => Math.max(0, ...(lay.get(c)?.values() ?? []));
217
+
218
+ // Every edge whose end is not in a later layer than its start goes back: not drawn.
219
+ const isBack = (from: string, to: string): boolean => {
220
+ const c = commonBox(from, to);
221
+ return layerOf(c, itemIn(from, c) as string) >= layerOf(c, itemIn(to, c) as string);
222
+ };
223
+ const back = g.edges.map((e) => isBack(e.from, e.to));
224
+
225
+ // Each container's rows: its items by layer, a previous order kept, new items after it.
226
+ const rows = new Map<string, string[][]>();
227
+ for (const c of containers) {
228
+ const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
229
+ const kept = (prevRows ?? []).flat();
230
+ const ids = [...(c === '' ? [...stubIds] : []), ...(kidsOf.get(c) ?? [])];
231
+ ids.sort((a, b) => {
232
+ const ka = kept.indexOf(a);
233
+ const kb = kept.indexOf(b);
234
+ return (ka < 0 ? Infinity : ka) - (kb < 0 ? Infinity : kb);
235
+ });
236
+ const r: string[][] = Array.from({ length: lastLayer(c) + 1 }, () => []);
237
+ for (const id of ids) r[layerOf(c, id)]?.push(id);
238
+ rows.set(c, r);
239
+ }
240
+
241
+ // Edge paths: the drawn edges through points on the layers they cross. The points of
242
+ // edges from one place (of one style) are shared, so their lines run as one.
243
+ const edgePaths: Arrangement['edgePaths'] = [];
244
+ const points = new Set<string>();
245
+ const point = (c: string, origin: string, style: string, l: number): EdgePathStep => {
246
+ const id = `~${c}|${origin}|${style}|${l}`;
247
+ if (!points.has(id)) {
248
+ points.add(id);
249
+ rows.get(c)?.[l]?.push(id);
250
+ }
251
+ return { id, in: c };
252
+ };
253
+ // A point beside a cluster, in its layer of the container around it: an edge that leaves or
254
+ // enters the cluster through its side, between two of its layers, passes there.
255
+ const byPoint = (c: string, box: string, origin: string, style: string): EdgePathStep => {
256
+ const l = layerOf(c, box);
257
+ const id = `~${c}|by:${box}|${origin}|${style}|${l}`;
258
+ if (!points.has(id)) {
259
+ points.add(id);
260
+ rows.get(c)?.[l]?.push(id);
261
+ }
262
+ return { id, in: c };
263
+ };
264
+ // An edge enters a cluster at its near side only to reach the cluster's first layer (and,
265
+ // top to bottom, not even then: the cluster's label is on that side); else through its
266
+ // side, next to the layer it goes to. It leaves at the far side only from the cluster's
267
+ // last layer, else through its side. So it runs inside a cluster only where it starts or ends.
268
+ const edgePath = (from: string, to: string, style: EdgeStyle): EdgePathStep[] => {
269
+ const c = isStub.has(from) ? '' : commonBox(from, to);
270
+ let curIn = containerOf(from);
271
+ let cur = from;
272
+ const steps: EdgePathStep[] = [{ id: from, in: curIn }];
273
+ // Out of the clusters around the source, up to the common container.
274
+ while (curIn !== c) {
275
+ const up = parentOf.get(curIn) ?? '';
276
+ steps.push({ id: curIn, in: up });
277
+ if (layerOf(curIn, cur) < lastLayer(curIn)) steps.push(byPoint(up, curIn, cur, style));
278
+ cur = curIn;
279
+ curIn = up;
280
+ }
281
+ // Across the common container, then into the clusters around the target.
282
+ const target = itemIn(to, c) as string;
283
+ for (let l = layerOf(c, cur) + 1; l < layerOf(c, target); l++) steps.push(point(c, cur, style, l));
284
+ const bp = boxPath(to);
285
+ const inward = bp.slice(0, c === '' ? bp.length : bp.indexOf(c)).reverse();
286
+ const origin = `>${c}:${cur}`;
287
+ let k = c;
288
+ for (const box of inward) {
289
+ const n = itemIn(to, box) as string;
290
+ if (layerOf(box, n) > 0 || down) steps.push(byPoint(k, box, origin, style));
291
+ steps.push({ id: box, in: k });
292
+ k = box;
293
+ }
294
+ steps.push({ id: to, in: k });
295
+ return steps;
296
+ };
297
+ g.edges.forEach((e, i) => {
298
+ if (!back[i]) edgePaths.push({ from: e.from, to: e.to, style: e.style, steps: edgePath(e.from, e.to, e.style) });
299
+ });
300
+ for (const s of g.stubs) edgePaths.push({ from: s.id, to: s.to, style: s.style, steps: edgePath(s.id, s.to, s.style) });
301
+
302
+ // The order within each layer.
303
+ const slots: Record<string, string[][]> = {};
304
+ for (const c of containers) {
305
+ const links: [string, string][] = [];
306
+ const lv = new Map((rows.get(c) ?? []).flatMap((r, i) => r.map((id) => [id, i] as const)));
307
+ for (const r of edgePaths)
308
+ for (let i = 1; i < r.steps.length; i++) {
309
+ const a = r.steps[i - 1] as EdgePathStep;
310
+ const b = r.steps[i] as EdgePathStep;
311
+ if (a.in === c && b.in === c && lv.get(a.id) !== lv.get(b.id)) links.push([a.id, b.id]);
312
+ }
313
+ const hasPrev = c === '' ? !!prev : !!prev?.clusters[c];
314
+ // A point beside a cluster is right after it.
315
+ slots[c] = orderRows(rows.get(c) ?? [], links, hasPrev).map((row) => {
316
+ const by = row.filter((id) => id.includes('|by:'));
317
+ const rest = row.filter((id) => !id.includes('|by:'));
318
+ return rest.flatMap((id) => [id, ...by.filter((p) => p.includes(`|by:${id}|`))]);
319
+ });
320
+ }
321
+ const real = (r: readonly string[][]) => r.map((row) => row.filter((id) => !isPoint(id)));
322
+ const clusters = g.clusters.map((c) => ({ id: c.id, parent: c.parent, cycle: !!c.cycle, layers: real(slots[c.id] ?? []).filter((r) => r.length) }));
323
+ const outerRows = real(slots[''] ?? []).map((row) => row.filter((id) => !isStub.has(id)));
324
+ const byId = new Map(clusters.map((c) => [c.id, c]));
325
+ // Every node id, in drawing order: expand each cluster to the nodes nested in it.
326
+ const expand = (ids: readonly string[]): string[] =>
327
+ ids.flatMap((id) => (clusterIds.has(id) ? expand((byId.get(id)?.layers ?? []).flat()) : [id]));
328
+ return {
329
+ drawn: expand(outerRows.flat()),
330
+ layers: outerRows.filter((r) => r.length),
331
+ clusters,
332
+ back,
333
+ slots,
334
+ edgePaths,
335
+ order: { layers: real(slots[''] ?? []), clusters: Object.fromEntries(clusters.map((c) => [c.id, real(slots[c.id] ?? [])])) },
336
+ };
337
+ }
338
+