@contraagent/elements 0.9.0 → 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.
- package/custom-elements.json +13 -2
- package/dist/graph/arrange.d.ts +8 -1
- package/dist/graph/arrange.d.ts.map +1 -1
- package/dist/graph/arrange.js +192 -65
- package/dist/graph/arrange.js.map +1 -1
- package/dist/graph/layered-graph.d.ts +3 -0
- package/dist/graph/layered-graph.d.ts.map +1 -1
- package/dist/graph/layered-graph.js +43 -12
- package/dist/graph/layered-graph.js.map +1 -1
- package/dist/graph/layout.d.ts.map +1 -1
- package/dist/graph/layout.js +42 -13
- package/dist/graph/layout.js.map +1 -1
- package/dist/graph/views/node-table.d.ts.map +1 -1
- package/dist/graph/views/node-table.js +23 -3
- package/dist/graph/views/node-table.js.map +1 -1
- package/dist/lanes/stack.d.ts.map +1 -1
- package/dist/lanes/stack.js +7 -2
- package/dist/lanes/stack.js.map +1 -1
- package/dist/table/types.d.ts +0 -5
- package/dist/table/types.d.ts.map +1 -1
- package/dist/table/types.js +3 -1
- package/dist/table/types.js.map +1 -1
- package/package.json +1 -1
- package/src/graph/arrange-before.fixture.ts +338 -0
- package/src/graph/arrange.ts +179 -62
- package/src/graph/layered-graph.ts +39 -6
- package/src/graph/layout-before.fixture.ts +331 -0
- package/src/graph/layout.ts +36 -13
- package/src/graph/views/node-table.ts +26 -3
- package/src/lanes/stack.ts +7 -2
- package/src/table/types.ts +4 -1
package/src/graph/arrange.ts
CHANGED
|
@@ -25,7 +25,14 @@ const pairKey = (a: string, b: string) => `${a}\u0000${b}`;
|
|
|
25
25
|
export function rankLayers(ids: readonly string[], edges: readonly (readonly [string, string])[], fixed: ReadonlyMap<string, number>): Map<string, number> {
|
|
26
26
|
const known = new Set(ids);
|
|
27
27
|
const all = new Map<string, string[]>(ids.map((id) => [id, []]));
|
|
28
|
-
|
|
28
|
+
const joined = new Set<string>();
|
|
29
|
+
for (const [a, b] of edges) {
|
|
30
|
+
if (a === b || !known.has(a) || !known.has(b)) continue;
|
|
31
|
+
const k = pairKey(a, b);
|
|
32
|
+
if (joined.has(k)) continue;
|
|
33
|
+
joined.add(k);
|
|
34
|
+
all.get(a)?.push(b);
|
|
35
|
+
}
|
|
29
36
|
const back = new Set<string>();
|
|
30
37
|
const seen = new Map<string, 1 | 2>();
|
|
31
38
|
for (const root of ids) {
|
|
@@ -62,8 +69,11 @@ export function rankLayers(ids: readonly string[], edges: readonly (readonly [st
|
|
|
62
69
|
const indeg = new Map(ids.map((id) => [id, pred.get(id)?.length ?? 0]));
|
|
63
70
|
const queue = ids.filter((id) => !indeg.get(id));
|
|
64
71
|
const layer = new Map<string, number>();
|
|
65
|
-
for (let
|
|
66
|
-
|
|
72
|
+
for (let head = 0; head < queue.length; head++) {
|
|
73
|
+
const q = queue[head] as string;
|
|
74
|
+
let deepest = 0;
|
|
75
|
+
for (const p of pred.get(q) ?? []) deepest = Math.max(deepest, (layer.get(p) ?? 0) + 1);
|
|
76
|
+
layer.set(q, fixed.get(q) ?? deepest);
|
|
67
77
|
for (const s of succ.get(q) ?? []) {
|
|
68
78
|
indeg.set(s, (indeg.get(s) ?? 0) - 1);
|
|
69
79
|
if (!indeg.get(s)) queue.push(s);
|
|
@@ -74,6 +84,7 @@ export function rankLayers(ids: readonly string[], edges: readonly (readonly [st
|
|
|
74
84
|
|
|
75
85
|
/** Pairs (a, b) between adjacent layers that cross, for the order `pos`. */
|
|
76
86
|
function crossings(rows: readonly string[][], links: readonly (readonly [string, string])[], pos: ReadonlyMap<string, number>, layerOf: ReadonlyMap<string, number>): number {
|
|
87
|
+
if (!rows.length) return 0;
|
|
77
88
|
let n = 0;
|
|
78
89
|
const by = new Map<number, [number, number][]>();
|
|
79
90
|
for (const [a, b] of links) {
|
|
@@ -81,14 +92,46 @@ function crossings(rows: readonly string[][], links: readonly (readonly [string,
|
|
|
81
92
|
if (l === undefined) continue;
|
|
82
93
|
push(by, l, [pos.get(a) ?? 0, pos.get(b) ?? 0]);
|
|
83
94
|
}
|
|
84
|
-
for (const ls of by.values())
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
for (const ls of by.values()) n += crossingPairs(ls);
|
|
96
|
+
return n;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How many pairs of `ls` cross: one's ends are both before the other's, on opposite
|
|
101
|
+
* sides ((a1 - a2) * (b1 - b2) < 0). Sorted by `a` (then `b`), they are the pairs whose
|
|
102
|
+
* `b` goes down, counted with a Fenwick tree over the `b` values: O(n log n), not n².
|
|
103
|
+
*/
|
|
104
|
+
export function crossingPairs(ls: readonly (readonly [number, number])[]): number {
|
|
105
|
+
if (ls.length < 2) return 0;
|
|
106
|
+
// Positions are small whole numbers; anything else is counted pair by pair.
|
|
107
|
+
let top = 0;
|
|
108
|
+
for (const [a, b] of ls) {
|
|
109
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || b < 0 || b > 1e7) return crossingPairsSlow(ls);
|
|
110
|
+
if (b > top) top = b;
|
|
111
|
+
}
|
|
112
|
+
const sorted = [...ls].sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
113
|
+
const tree = new Array<number>(top + 2).fill(0);
|
|
114
|
+
let n = 0;
|
|
115
|
+
sorted.forEach(([, b], seen) => {
|
|
116
|
+
// Earlier pairs (a smaller or the same) with a larger b: a crossing when a is smaller;
|
|
117
|
+
// a pair with the same a has a b no larger, sorted so.
|
|
118
|
+
let atMost = 0;
|
|
119
|
+
for (let i = b + 1; i > 0; i -= i & -i) atMost += tree[i] as number;
|
|
120
|
+
n += seen - atMost;
|
|
121
|
+
for (let i = b + 1; i < tree.length; i += i & -i) tree[i] = (tree[i] as number) + 1;
|
|
122
|
+
});
|
|
123
|
+
return n;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function crossingPairsSlow(ls: readonly (readonly [number, number])[]): number {
|
|
127
|
+
let n = 0;
|
|
128
|
+
for (let i = 0; i < ls.length; i++)
|
|
129
|
+
for (let j = i + 1; j < ls.length; j++) {
|
|
130
|
+
const [a1, b1] = ls[i] as readonly [number, number];
|
|
131
|
+
const [a2, b2] = ls[j] as readonly [number, number];
|
|
132
|
+
if ((a1 - a2) * (b1 - b2) < 0) n++;
|
|
133
|
+
}
|
|
134
|
+
return n;
|
|
92
135
|
}
|
|
93
136
|
|
|
94
137
|
/** True for a point where an edge crosses a layer. */
|
|
@@ -160,7 +203,31 @@ export interface GraphShape {
|
|
|
160
203
|
stubs: readonly Pick<GraphStubInput, 'id' | 'to' | 'style'>[];
|
|
161
204
|
}
|
|
162
205
|
|
|
163
|
-
|
|
206
|
+
type Arranged = { prev: GraphOrder | undefined; down: boolean; out: Arrangement }[];
|
|
207
|
+
|
|
208
|
+
/** The structure each shape object was found to have (`shapeKey`). */
|
|
209
|
+
const keys = new WeakMap<GraphShape, string>();
|
|
210
|
+
/** The last few arrangements, by structure, the latest last. */
|
|
211
|
+
const arranged = new Map<string, Arranged>();
|
|
212
|
+
const KEPT = 16;
|
|
213
|
+
|
|
214
|
+
/** A value as the key writes it: null, undefined and a string kept apart. */
|
|
215
|
+
const tag = (v: string | null | undefined): string => (v === undefined ? 'u' : v === null ? 'n' : `s${v}`);
|
|
216
|
+
|
|
217
|
+
/** Everything `arrangeNew` reads of `g`, as one string: two shapes with the same key give the same arrangement. */
|
|
218
|
+
function shapeKey(g: GraphShape): string {
|
|
219
|
+
let k = keys.get(g);
|
|
220
|
+
if (k === undefined) {
|
|
221
|
+
k = JSON.stringify([
|
|
222
|
+
g.nodes.map((n) => [tag(n.id), tag(n.cluster)]),
|
|
223
|
+
g.clusters.map((c) => [tag(c.id), tag(c.parent), !!c.cycle]),
|
|
224
|
+
g.edges.map((e) => [tag(e.from), tag(e.to), tag(e.style), !!e.ranks]),
|
|
225
|
+
g.stubs.map((s) => [tag(s.id), tag(s.to), tag(s.style)]),
|
|
226
|
+
]);
|
|
227
|
+
keys.set(g, k);
|
|
228
|
+
}
|
|
229
|
+
return k;
|
|
230
|
+
}
|
|
164
231
|
|
|
165
232
|
/**
|
|
166
233
|
* The order of `g`'s drawing: each container's layers (the top level, then each
|
|
@@ -168,14 +235,17 @@ const arranged = new WeakMap<GraphShape, { prev: GraphOrder | undefined; down: b
|
|
|
168
235
|
* and the slots each edge passes. Pass a previous `order` of the same graph as
|
|
169
236
|
* `prev` to keep its items where they are (new ones are appended to their layer);
|
|
170
237
|
* `down` lays out the edge paths for a drawing top to bottom (a cluster's label is then on
|
|
171
|
-
* the side its edges come in). The same `g
|
|
238
|
+
* the side its edges come in). The same `g` (or a graph of the same ids, clusters and links),
|
|
239
|
+
* `prev` and `down` give the same object.
|
|
172
240
|
*/
|
|
173
241
|
export function arrange(g: GraphShape, prev?: GraphOrder, down = false): Arrangement {
|
|
174
|
-
const
|
|
242
|
+
const key = shapeKey(g);
|
|
243
|
+
const seen = arranged.get(key) ?? [];
|
|
244
|
+
arranged.delete(key);
|
|
175
245
|
const hit = seen.find((x) => x.prev === prev && x.down === down);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
arranged.
|
|
246
|
+
const out = hit?.out ?? arrangeNew(g, prev, down);
|
|
247
|
+
arranged.set(key, hit ? seen : [...seen.slice(-3), { prev, down, out }]);
|
|
248
|
+
if (arranged.size > KEPT) arranged.delete(arranged.keys().next().value as string);
|
|
179
249
|
return out;
|
|
180
250
|
}
|
|
181
251
|
|
|
@@ -187,16 +257,23 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
187
257
|
const stubIds = [...new Set(g.stubs.map((s) => s.id))];
|
|
188
258
|
const isStub = new Set(stubIds);
|
|
189
259
|
|
|
190
|
-
// The clusters an id sits in, innermost first (for a cluster id, itself first).
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
b =
|
|
260
|
+
// The clusters an id sits in, innermost first (for a cluster id, itself first); read only.
|
|
261
|
+
const paths = new Map<string, { list: string[]; set: Set<string> }>();
|
|
262
|
+
const pathOf = (id: string): { list: string[]; set: Set<string> } => {
|
|
263
|
+
let p = paths.get(id);
|
|
264
|
+
if (!p) {
|
|
265
|
+
const list: string[] = [];
|
|
266
|
+
let b: string | null = clusterIds.has(id) ? id : (boxOf.get(id) ?? null);
|
|
267
|
+
while (b) {
|
|
268
|
+
list.push(b);
|
|
269
|
+
b = parentOf.get(b) ?? null;
|
|
270
|
+
}
|
|
271
|
+
p = { list, set: new Set(list) };
|
|
272
|
+
paths.set(id, p);
|
|
197
273
|
}
|
|
198
|
-
return
|
|
274
|
+
return p;
|
|
199
275
|
};
|
|
276
|
+
const boxPath = (id: string): readonly string[] => pathOf(id).list;
|
|
200
277
|
// The direct child of `container` (a cluster, or '' for the top level) that holds a node `id`.
|
|
201
278
|
const itemIn = (id: string, container: string): string | undefined => {
|
|
202
279
|
const bp = boxPath(id);
|
|
@@ -207,7 +284,8 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
207
284
|
// The deepest cluster holding both ends of an edge (the top level when none).
|
|
208
285
|
const commonBox = (a: string, b: string): string => {
|
|
209
286
|
const bp = boxPath(a);
|
|
210
|
-
|
|
287
|
+
const other = pathOf(b).set;
|
|
288
|
+
return bp.find((x) => other.has(x)) ?? '';
|
|
211
289
|
};
|
|
212
290
|
// The container a slot is in: a stub's is the top level.
|
|
213
291
|
const containerOf = (id: string): string => (isStub.has(id) ? '' : (boxOf.get(id) ?? ''));
|
|
@@ -216,21 +294,34 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
216
294
|
// clusters nested in it). Each cluster is one item of its parent.
|
|
217
295
|
const containers = ['', ...g.clusters.map((c) => c.id)];
|
|
218
296
|
const kidsOf = new Map<string, string[]>(containers.map((c) => [c, []]));
|
|
297
|
+
const kidSet = new Map<string, Set<string>>(containers.map((c) => [c, new Set()]));
|
|
298
|
+
const addKid = (c: string, it: string): void => {
|
|
299
|
+
const set = kidSet.get(c);
|
|
300
|
+
if (!set || set.has(it)) return;
|
|
301
|
+
set.add(it);
|
|
302
|
+
kidsOf.get(c)?.push(it);
|
|
303
|
+
};
|
|
304
|
+
// A node is an item of the top level and of each cluster around it, and of no other container.
|
|
219
305
|
for (const { id } of g.nodes) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
306
|
+
const bp = boxPath(id);
|
|
307
|
+
addKid('', bp[bp.length - 1] ?? id);
|
|
308
|
+
bp.forEach((c, i) => addKid(c, i === 0 ? id : (bp[i - 1] as string)));
|
|
224
309
|
}
|
|
310
|
+
// Each edge's container: the deepest cluster around both ends.
|
|
311
|
+
const edgeBox = g.edges.map((e) => commonBox(e.from, e.to));
|
|
312
|
+
const edgesIn = new Map<string, number[]>();
|
|
313
|
+
edgeBox.forEach((c, i) => push(edgesIn, c, i));
|
|
225
314
|
const lay = new Map<string, Map<string, number>>();
|
|
226
315
|
for (const c of containers) {
|
|
227
316
|
const kids = kidsOf.get(c) ?? [];
|
|
317
|
+
const own = kidSet.get(c) ?? new Set<string>();
|
|
228
318
|
const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
|
|
229
319
|
const fixed = new Map<string, number>();
|
|
230
|
-
prevRows?.forEach((row, i) => row.forEach((id) =>
|
|
320
|
+
prevRows?.forEach((row, i) => row.forEach((id) => own.has(id) && fixed.set(id, i)));
|
|
231
321
|
// An edge that closes a cycle does not set the layers.
|
|
232
|
-
const es =
|
|
233
|
-
.
|
|
322
|
+
const es = (edgesIn.get(c) ?? [])
|
|
323
|
+
.map((i) => g.edges[i] as (typeof g.edges)[number])
|
|
324
|
+
.filter((e) => e.ranks)
|
|
234
325
|
.map((e) => [itemIn(e.from, c) as string, itemIn(e.to, c) as string] as const);
|
|
235
326
|
lay.set(c, rankLayers(kids, es, fixed));
|
|
236
327
|
}
|
|
@@ -238,35 +329,40 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
238
329
|
const top = lay.get('') as Map<string, number>;
|
|
239
330
|
const prevTop = new Map<string, number>();
|
|
240
331
|
prev?.layers.forEach((row, i) => row.forEach((id) => prevTop.set(id, i)));
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
);
|
|
332
|
+
const firstTo = new Map<string, number>();
|
|
333
|
+
for (const s of g.stubs) firstTo.set(s.id, Math.min(firstTo.get(s.id) ?? Infinity, top.get(itemIn(s.to, '') as string) ?? 0));
|
|
334
|
+
const stubAt = new Map(stubIds.map((id) => [id, prevTop.get(id) ?? (firstTo.get(id) as number) - 1]));
|
|
244
335
|
if ([...stubAt.values()].some((l) => l < 0)) {
|
|
245
336
|
for (const [id, l] of top) top.set(id, l + 1);
|
|
246
337
|
for (const [id, l] of stubAt) stubAt.set(id, l + 1);
|
|
247
338
|
}
|
|
248
339
|
for (const [id, l] of stubAt) top.set(id, l);
|
|
249
340
|
const layerOf = (c: string, id: string): number => lay.get(c)?.get(id) ?? 0;
|
|
250
|
-
const
|
|
341
|
+
const lastOf = new Map<string, number>();
|
|
342
|
+
const lastLayer = (c: string): number => {
|
|
343
|
+
let l = lastOf.get(c);
|
|
344
|
+
if (l === undefined) {
|
|
345
|
+
l = 0;
|
|
346
|
+
for (const v of lay.get(c)?.values() ?? []) l = Math.max(l, v);
|
|
347
|
+
lastOf.set(c, l);
|
|
348
|
+
}
|
|
349
|
+
return l;
|
|
350
|
+
};
|
|
251
351
|
|
|
252
352
|
// Every edge whose end is not in a later layer than its start goes back: not drawn.
|
|
253
|
-
const
|
|
254
|
-
const c =
|
|
255
|
-
return layerOf(c, itemIn(from, c) as string) >= layerOf(c, itemIn(to, c) as string);
|
|
256
|
-
};
|
|
257
|
-
const back = g.edges.map((e) => isBack(e.from, e.to));
|
|
353
|
+
const back = g.edges.map((e, i) => {
|
|
354
|
+
const c = edgeBox[i] as string;
|
|
355
|
+
return layerOf(c, itemIn(e.from, c) as string) >= layerOf(c, itemIn(e.to, c) as string);
|
|
356
|
+
});
|
|
258
357
|
|
|
259
358
|
// Each container's rows: its items by layer, a previous order kept, new items after it.
|
|
260
359
|
const rows = new Map<string, string[][]>();
|
|
261
360
|
for (const c of containers) {
|
|
262
361
|
const prevRows = c === '' ? prev?.layers : prev?.clusters[c];
|
|
263
|
-
const kept =
|
|
362
|
+
const kept = new Map<string, number>();
|
|
363
|
+
(prevRows ?? []).flat().forEach((id, i) => kept.has(id) || kept.set(id, i));
|
|
264
364
|
const ids = [...(c === '' ? [...stubIds] : []), ...(kidsOf.get(c) ?? [])];
|
|
265
|
-
ids.sort((a, b) =>
|
|
266
|
-
const ka = kept.indexOf(a);
|
|
267
|
-
const kb = kept.indexOf(b);
|
|
268
|
-
return (ka < 0 ? Infinity : ka) - (kb < 0 ? Infinity : kb);
|
|
269
|
-
});
|
|
365
|
+
ids.sort((a, b) => (kept.get(a) ?? Infinity) - (kept.get(b) ?? Infinity));
|
|
270
366
|
const r: string[][] = Array.from({ length: lastLayer(c) + 1 }, () => []);
|
|
271
367
|
for (const id of ids) r[layerOf(c, id)]?.push(id);
|
|
272
368
|
rows.set(c, r);
|
|
@@ -333,24 +429,23 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
333
429
|
});
|
|
334
430
|
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) });
|
|
335
431
|
|
|
336
|
-
// The order within each layer.
|
|
432
|
+
// The order within each layer. Each container's joins: two steps of an edge in it, in two layers.
|
|
433
|
+
const levels = new Map<string, Map<string, number>>();
|
|
434
|
+
for (const c of containers) levels.set(c, new Map((rows.get(c) ?? []).flatMap((r, i) => r.map((id) => [id, i] as const))));
|
|
435
|
+
const linksIn = new Map<string, [string, string][]>(containers.map((c) => [c, []]));
|
|
436
|
+
for (const r of edgePaths)
|
|
437
|
+
for (let i = 1; i < r.steps.length; i++) {
|
|
438
|
+
const a = r.steps[i - 1] as EdgePathStep;
|
|
439
|
+
const b = r.steps[i] as EdgePathStep;
|
|
440
|
+
if (a.in !== b.in) continue;
|
|
441
|
+
const lv = levels.get(a.in);
|
|
442
|
+
if (lv && lv.get(a.id) !== lv.get(b.id)) linksIn.get(a.in)?.push([a.id, b.id]);
|
|
443
|
+
}
|
|
337
444
|
const slots: Record<string, string[][]> = {};
|
|
338
445
|
for (const c of containers) {
|
|
339
|
-
const links: [string, string][] = [];
|
|
340
|
-
const lv = new Map((rows.get(c) ?? []).flatMap((r, i) => r.map((id) => [id, i] as const)));
|
|
341
|
-
for (const r of edgePaths)
|
|
342
|
-
for (let i = 1; i < r.steps.length; i++) {
|
|
343
|
-
const a = r.steps[i - 1] as EdgePathStep;
|
|
344
|
-
const b = r.steps[i] as EdgePathStep;
|
|
345
|
-
if (a.in === c && b.in === c && lv.get(a.id) !== lv.get(b.id)) links.push([a.id, b.id]);
|
|
346
|
-
}
|
|
347
446
|
const hasPrev = c === '' ? !!prev : !!prev?.clusters[c];
|
|
348
447
|
// A point beside a cluster is right after it.
|
|
349
|
-
slots[c] = orderRows(rows.get(c) ?? [],
|
|
350
|
-
const by = row.filter((id) => id.includes('|by:'));
|
|
351
|
-
const rest = row.filter((id) => !id.includes('|by:'));
|
|
352
|
-
return rest.flatMap((id) => [id, ...by.filter((p) => p.includes(`|by:${id}|`))]);
|
|
353
|
-
});
|
|
448
|
+
slots[c] = orderRows(rows.get(c) ?? [], linksIn.get(c) ?? [], hasPrev).map(byPointsAfter);
|
|
354
449
|
}
|
|
355
450
|
const real = (r: readonly string[][]) => r.map((row) => row.filter((id) => !isPoint(id)));
|
|
356
451
|
const clusters = g.clusters.map((c) => ({ id: c.id, parent: c.parent, cycle: !!c.cycle, layers: real(slots[c.id] ?? []).filter((r) => r.length) }));
|
|
@@ -370,6 +465,28 @@ function arrangeNew(g: GraphShape, prev: GraphOrder | undefined, down: boolean):
|
|
|
370
465
|
};
|
|
371
466
|
}
|
|
372
467
|
|
|
468
|
+
/** A layer with each point beside a cluster (`|by:<cluster>|`) right after that cluster. */
|
|
469
|
+
function byPointsAfter(row: readonly string[]): string[] {
|
|
470
|
+
const by = row.filter((id) => id.includes('|by:'));
|
|
471
|
+
const rest = row.filter((id) => !id.includes('|by:'));
|
|
472
|
+
if (!by.length) return rest;
|
|
473
|
+
// An id without '|' is named in a point as the text between '|by:' and the next '|';
|
|
474
|
+
// the few others are looked for in each point.
|
|
475
|
+
const named = new Map<string, string[]>();
|
|
476
|
+
for (const p of by) {
|
|
477
|
+
const seen = new Set<string>();
|
|
478
|
+
for (let i = p.indexOf('|by:'); i >= 0; i = p.indexOf('|by:', i + 1)) {
|
|
479
|
+
const end = p.indexOf('|', i + 4);
|
|
480
|
+
if (end < 0) break;
|
|
481
|
+
const id = p.slice(i + 4, end);
|
|
482
|
+
if (seen.has(id)) continue;
|
|
483
|
+
seen.add(id);
|
|
484
|
+
push(named, id, p);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return rest.flatMap((id) => [id, ...(id.includes('|') ? by.filter((p) => p.includes(`|by:${id}|`)) : (named.get(id) ?? []))]);
|
|
488
|
+
}
|
|
489
|
+
|
|
373
490
|
/**
|
|
374
491
|
* How many layers the drawing spans along its flow, and how many items it stacks across
|
|
375
492
|
* it at most, counting a cluster as what it holds: an estimate of the drawing's shape,
|
|
@@ -31,6 +31,27 @@ type Changed = PropertyValues<CaeLayeredGraph> & PropertyValueMap<{ down: boolea
|
|
|
31
31
|
|
|
32
32
|
const EMPTY: GraphInput = { series: [], nodes: [], clusters: [], edges: [], stubs: [] };
|
|
33
33
|
|
|
34
|
+
/** An arrangement's slots and the slots each edge passes, as one string: the same string, the same layout from the same sizes. */
|
|
35
|
+
const structures = new WeakMap<Arrangement, string>();
|
|
36
|
+
function structureOf(a: Arrangement): string {
|
|
37
|
+
let s = structures.get(a);
|
|
38
|
+
if (s === undefined) {
|
|
39
|
+
s = JSON.stringify([a.slots, a.edgePaths.map((r) => r.steps.map((x) => x.id))]);
|
|
40
|
+
structures.set(a, s);
|
|
41
|
+
}
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type LayKey = { key: string; structure: string; down: boolean; sizes: [string, number, number][] };
|
|
46
|
+
|
|
47
|
+
function sameLayKey(a: LayKey, b: LayKey | null): boolean {
|
|
48
|
+
if (!b || a.key !== b.key || a.down !== b.down || a.structure !== b.structure || a.sizes.length !== b.sizes.length) return false;
|
|
49
|
+
return a.sizes.every((x, i) => {
|
|
50
|
+
const y = b.sizes[i] as [string, number, number];
|
|
51
|
+
return x[0] === y[0] && x[1] === y[1] && x[2] === y[2];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
/**
|
|
35
56
|
* A layered graph: each node in a layer by the longest path to it, edges bending only
|
|
36
57
|
* in the gaps between layers and never passing a node or a cluster's label; a cluster
|
|
@@ -169,10 +190,13 @@ export class CaeLayeredGraph extends LitElement {
|
|
|
169
190
|
private readonly active = new Map<string, number>();
|
|
170
191
|
/** What the sizes were last measured for, what the layout was computed from, and the graph it is of. */
|
|
171
192
|
private measured: { graph: GraphInput; key: string } | null = null;
|
|
172
|
-
|
|
193
|
+
/** What the layout was computed from: the graph's key, its structure (`structureOf`), the direction and the sizes. */
|
|
194
|
+
private layKey: LayKey | null = null;
|
|
173
195
|
private layFor: string | null = null;
|
|
174
196
|
private litCache: { key: string | null; graph: GraphInput; related: unknown; on: LitMarks | null } | null = null;
|
|
175
197
|
private scrolled: Element | null = null;
|
|
198
|
+
/** The marks by id, for the graph and the matrix they were found in (`findMark`). */
|
|
199
|
+
private markIx: { drawn: DrawnGraph; matrix: MarkMatrixInput | null; marks: Map<string, { drill?: boolean }> } | null = null;
|
|
176
200
|
|
|
177
201
|
private readonly pz = new PanZoomController(this, {
|
|
178
202
|
frame: () => this.renderRoot.querySelector<HTMLElement>('.frame'),
|
|
@@ -299,8 +323,8 @@ export class CaeLayeredGraph extends LitElement {
|
|
|
299
323
|
const sizes = new Map<string, Size>();
|
|
300
324
|
for (const el of dagEl.querySelectorAll<HTMLElement>('[data-id]')) sizes.set(el.dataset['id'] ?? '', { w: el.offsetWidth, h: el.offsetHeight });
|
|
301
325
|
const a = this.arr;
|
|
302
|
-
const key =
|
|
303
|
-
if (key
|
|
326
|
+
const key = { key: this.key, structure: structureOf(a), down: this.down, sizes: [...sizes].map(([id, s]): [string, number, number] => [id, s.w, s.h]) };
|
|
327
|
+
if (sameLayKey(key, this.layKey)) return this.fit();
|
|
304
328
|
this.layKey = key;
|
|
305
329
|
this.layFor = this.key;
|
|
306
330
|
this.lay = layoutGraph(a, sizes, this.down);
|
|
@@ -335,9 +359,18 @@ export class CaeLayeredGraph extends LitElement {
|
|
|
335
359
|
}
|
|
336
360
|
|
|
337
361
|
private findMark(id: string): { drill?: boolean } | null {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
362
|
+
let ix = this.markIx;
|
|
363
|
+
if (!ix || ix.drawn !== this.drawn || ix.matrix !== this.matrix) {
|
|
364
|
+
// Each mark by its id: the first of the nodes', then the matrix's.
|
|
365
|
+
const marks = new Map<string, { drill?: boolean }>();
|
|
366
|
+
for (const n of this.drawn.nodes) {
|
|
367
|
+
for (const m of n.marks) if (m && !marks.has(m.id)) marks.set(m.id, m);
|
|
368
|
+
for (const m of n.loose) if (m && !marks.has(m.id)) marks.set(m.id, m);
|
|
369
|
+
}
|
|
370
|
+
for (const r of this.matrix?.rows ?? []) for (const m of r.cells) if (m && !marks.has(m.id)) marks.set(m.id, m);
|
|
371
|
+
ix = this.markIx = { drawn: this.drawn, matrix: this.matrix, marks };
|
|
372
|
+
}
|
|
373
|
+
return ix.marks.get(id) ?? null;
|
|
341
374
|
}
|
|
342
375
|
|
|
343
376
|
/** The series (or mark id) highlighted: the one hovered or focused, else the selected mark's, else `highlight`; and the marks it lights. */
|