@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.
- 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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// The layout as it was first written, with a scan of every edge per container and per
|
|
2
|
+
// slot: the faster one in layout.ts must give the same drawing.
|
|
3
|
+
import { isPoint } from './arrange.js';
|
|
4
|
+
import type { Arrangement, EdgeStyle, EdgePathStep } from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface Size {
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A rectangle in the drawing. */
|
|
12
|
+
export interface Rect {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
w: number;
|
|
16
|
+
h: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A straight stretch of an edge along the flow, from `a` to `b`, `at` across it; an edge bends between two. */
|
|
20
|
+
export interface Stretch {
|
|
21
|
+
a: number;
|
|
22
|
+
b: number;
|
|
23
|
+
at: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GraphLayout {
|
|
27
|
+
/** The drawing's size, its margin included. */
|
|
28
|
+
w: number;
|
|
29
|
+
h: number;
|
|
30
|
+
/** Every node, stub and cluster, by id. */
|
|
31
|
+
at: Record<string, Rect>;
|
|
32
|
+
/** Every edge path, with its runs. */
|
|
33
|
+
edges: { from: string; to: string; style: EdgeStyle; runs: Stretch[] }[];
|
|
34
|
+
/** Each cycling cluster's return to its next round. */
|
|
35
|
+
returns: LoopReturn[];
|
|
36
|
+
down: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A cluster's return to its next round, along the flow (`u`) and across it (`v`): from
|
|
41
|
+
* the far side of its last layer's outermost item (`from`, at `v0`) out to a band
|
|
42
|
+
* past its content (`band`), back along the band, and in to its first layer's
|
|
43
|
+
* outermost item (`to`, at `v1`). Left to right the band is under the nodes; top to
|
|
44
|
+
* bottom, on their right.
|
|
45
|
+
*/
|
|
46
|
+
export interface LoopReturn {
|
|
47
|
+
box: string;
|
|
48
|
+
from: number;
|
|
49
|
+
v0: number;
|
|
50
|
+
to: number;
|
|
51
|
+
v1: number;
|
|
52
|
+
band: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The spaces of a drawing; left to right unless `down`. */
|
|
56
|
+
export interface LayoutSpace {
|
|
57
|
+
/** Between two layers, at the top level and in a cluster. */
|
|
58
|
+
gap: number;
|
|
59
|
+
boxGap: number;
|
|
60
|
+
/** Between two items of a layer, and next to a point an edge passes. */
|
|
61
|
+
sep: number;
|
|
62
|
+
pointSep: number;
|
|
63
|
+
/** A cluster's padding, and the space under its label. */
|
|
64
|
+
pad: number;
|
|
65
|
+
/** The space after a box's content, across the flow: the band its return to the next round runs in. */
|
|
66
|
+
padEnd: number;
|
|
67
|
+
margin: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The band past a cluster's content that its return to the next round runs in, and the return's corner radius. */
|
|
71
|
+
export const RETURN_BAND = 24;
|
|
72
|
+
export const RETURN_R = 6;
|
|
73
|
+
|
|
74
|
+
export const SPACE: Record<'across' | 'down', LayoutSpace> = {
|
|
75
|
+
across: { gap: 56, boxGap: 40, sep: 16, pointSep: 12, pad: 12, padEnd: RETURN_BAND, margin: 16 },
|
|
76
|
+
down: { gap: 40, boxGap: 32, sep: 16, pointSep: 12, pad: 10, padEnd: RETURN_BAND, margin: 16 },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Tops (across the flow) nearest to `want` that keep the order and each gap: weighted isotonic regression (pool adjacent violators). */
|
|
80
|
+
function placeRow(want: readonly number[], gaps: readonly number[], weight: readonly number[]): number[] {
|
|
81
|
+
const off: number[] = [0];
|
|
82
|
+
for (let i = 1; i < want.length; i++) off.push((off[i - 1] as number) + (gaps[i - 1] as number));
|
|
83
|
+
const blocks: { sum: number; w: number; n: number }[] = [];
|
|
84
|
+
want.forEach((v, i) => {
|
|
85
|
+
const w = weight[i] ?? 1;
|
|
86
|
+
blocks.push({ sum: w * (v - (off[i] as number)), w, n: 1 });
|
|
87
|
+
while (blocks.length > 1) {
|
|
88
|
+
const b = blocks[blocks.length - 1] as { sum: number; w: number; n: number };
|
|
89
|
+
const a = blocks[blocks.length - 2] as { sum: number; w: number; n: number };
|
|
90
|
+
if (a.sum / a.w <= b.sum / b.w) break;
|
|
91
|
+
blocks.splice(-2, 2, { sum: a.sum + b.sum, w: a.w + b.w, n: a.n + b.n });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const out: number[] = [];
|
|
95
|
+
for (const b of blocks) for (let k = 0; k < b.n; k++) out.push(b.sum / b.w + (off[out.length] as number));
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Where everything of a layered graph is drawn, from the sizes of its nodes, its stubs
|
|
101
|
+
* and its clusters' labels (`lbl:<box id>`): left to right, or top to bottom when
|
|
102
|
+
* `down`. Each container is laid out inside out, a box being one item of its parent:
|
|
103
|
+
* its layers side by side, an item at the start of its layer; across the flow, each
|
|
104
|
+
* item as near as its order allows to the items it is joined to (sweeps of weighted
|
|
105
|
+
* isotonic regression), so chains run straight. An edge runs along its points and
|
|
106
|
+
* bends only in the gap between two layers; it enters and leaves a box where its
|
|
107
|
+
* point inside is, and never passes a node or a box's label.
|
|
108
|
+
*/
|
|
109
|
+
export function layoutGraph(g: Pick<Arrangement, 'clusters' | 'slots' | 'edgePaths'>, sizes: ReadonlyMap<string, Size>, down = false): GraphLayout {
|
|
110
|
+
const S = SPACE[down ? 'down' : 'across'];
|
|
111
|
+
const len = (s: Size) => (down ? s.h : s.w);
|
|
112
|
+
const brd = (s: Size) => (down ? s.w : s.h);
|
|
113
|
+
const boxIds = new Set(g.clusters.map((b) => b.id));
|
|
114
|
+
const parentOf = new Map(g.clusters.map((b) => [b.id, b.parent]));
|
|
115
|
+
const label = (box: string): Size => sizes.get(`lbl:${box}`) ?? { w: 0, h: 0 };
|
|
116
|
+
// A box's padding: along the flow, and across it. Its label is in a band at its top: across
|
|
117
|
+
// the flow left to right; along it top to bottom, where a gap under the label lets edges in.
|
|
118
|
+
const padOf = (box: string) =>
|
|
119
|
+
down
|
|
120
|
+
? { u0: label(box).h + S.pad + S.gap / 2, u1: S.pad, v0: S.pad, v1: S.padEnd, minU: 0, minV: label(box).w + 2 * S.pad }
|
|
121
|
+
: { u0: S.pad, u1: S.pad, v0: label(box).h + S.pad, v1: S.padEnd, minU: label(box).w + 2 * S.pad, minV: 0 };
|
|
122
|
+
const extent = new Map<string, { u: number; v: number }>(); // a box's size, along and across
|
|
123
|
+
const slotSize = (id: string): { u: number; v: number } => {
|
|
124
|
+
if (boxIds.has(id)) return extent.get(id) ?? { u: 0, v: 0 };
|
|
125
|
+
if (isPoint(id)) return { u: 0, v: 0 };
|
|
126
|
+
const s = sizes.get(id) ?? { w: 0, h: 0 };
|
|
127
|
+
return { u: len(s), v: brd(s) };
|
|
128
|
+
};
|
|
129
|
+
interface Local {
|
|
130
|
+
rows: string[][];
|
|
131
|
+
start: number[];
|
|
132
|
+
width: number[];
|
|
133
|
+
top: Map<string, number>;
|
|
134
|
+
u: number;
|
|
135
|
+
v: number;
|
|
136
|
+
}
|
|
137
|
+
const local = new Map<string, Local>();
|
|
138
|
+
const layerIn = new Map<string, Map<string, number>>();
|
|
139
|
+
const depth = (c: string): number => (c === '' ? 0 : 1 + depth(parentOf.get(c) ?? ''));
|
|
140
|
+
const containers = ['', ...g.clusters.map((b) => b.id)].sort((a, b) => depth(b) - depth(a));
|
|
141
|
+
// Where an edge meets a slot, across the flow from the slot's top: a node's middle,
|
|
142
|
+
// a point, or (a box) where the edge's step inside it is.
|
|
143
|
+
const attach = (ri: number, si: number): number => {
|
|
144
|
+
const steps = g.edgePaths[ri]?.steps ?? [];
|
|
145
|
+
const s = steps[si] as EdgePathStep;
|
|
146
|
+
if (isPoint(s.id)) return 0;
|
|
147
|
+
if (!boxIds.has(s.id)) return slotSize(s.id).v / 2;
|
|
148
|
+
const inner = steps[si - 1]?.in === s.id ? si - 1 : si + 1;
|
|
149
|
+
const step = steps[inner] as EdgePathStep;
|
|
150
|
+
return padOf(s.id).v0 + (local.get(s.id)?.top.get(step.id) ?? 0) + attach(ri, inner);
|
|
151
|
+
};
|
|
152
|
+
for (const c of containers) {
|
|
153
|
+
const rows = (g.slots[c] ?? []).map((r) => [...r]);
|
|
154
|
+
layerIn.set(c, new Map(rows.flatMap((r, i) => r.map((id) => [id, i] as const))));
|
|
155
|
+
const width = rows.map((r) => Math.max(0, ...r.map((id) => slotSize(id).u)));
|
|
156
|
+
const gap = c === '' ? S.gap : S.boxGap;
|
|
157
|
+
const start: number[] = [];
|
|
158
|
+
width.forEach((_, i) => start.push(i ? (start[i - 1] as number) + (width[i - 1] as number) + gap : 0));
|
|
159
|
+
// The joins inside this container: slot a of layer l to slot b of layer l + 1.
|
|
160
|
+
const links: { a: string; b: string; oa: number; ob: number }[] = [];
|
|
161
|
+
g.edgePaths.forEach((r, ri) =>
|
|
162
|
+
r.steps.forEach((s, si) => {
|
|
163
|
+
const p = r.steps[si - 1];
|
|
164
|
+
if (si && p && p.in === c && s.in === c && layerIn.get(c)?.get(p.id) !== layerIn.get(c)?.get(s.id)) links.push({ a: p.id, b: s.id, oa: attach(ri, si - 1), ob: attach(ri, si) });
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
// Points sit close to each other and to a node, further from a box (its next-pass mark is on its edge).
|
|
168
|
+
const sepOf = (a: string, b: string | undefined) =>
|
|
169
|
+
b === undefined ? 0 : boxIds.has(a) || boxIds.has(b) || !(isPoint(a) || isPoint(b)) ? S.sep : S.pointSep;
|
|
170
|
+
const top = new Map<string, number>();
|
|
171
|
+
for (const r of rows) {
|
|
172
|
+
let v = 0;
|
|
173
|
+
r.forEach((id, i) => {
|
|
174
|
+
top.set(id, v);
|
|
175
|
+
v += slotSize(id).v + sepOf(id, r[i + 1]);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const place = (r: string[], want: (id: string) => number[]) => {
|
|
179
|
+
const desired = r.map((id) => {
|
|
180
|
+
const xs = want(id);
|
|
181
|
+
return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : (top.get(id) ?? 0);
|
|
182
|
+
});
|
|
183
|
+
const gaps = r.map((id, i) => slotSize(id).v + sepOf(id, r[i + 1]));
|
|
184
|
+
// The nodes first, as if the points between them were fixed in size; then the points, around the nodes.
|
|
185
|
+
const real = r.map((id, i) => (isPoint(id) ? -1 : i)).filter((i) => i >= 0);
|
|
186
|
+
const span = (i: number, j: number) => gaps.slice(i, j).reduce((a, b) => a + b, 0);
|
|
187
|
+
const at = placeRow(
|
|
188
|
+
real.map((i) => desired[i] as number),
|
|
189
|
+
real.map((i, k) => (k + 1 < real.length ? span(i, real[k + 1] as number) : 0)),
|
|
190
|
+
real.map(() => 1),
|
|
191
|
+
);
|
|
192
|
+
real.forEach((i, k) => (desired[i] = at[k] as number));
|
|
193
|
+
placeRow(desired, gaps, r.map((id) => (isPoint(id) ? 1 : 1e6))).forEach((v, i) => top.set(r[i] as string, Math.round(v * 100) / 100));
|
|
194
|
+
};
|
|
195
|
+
// A node lines up with the nodes it is joined to, when it has any on that side, else with the points;
|
|
196
|
+
// a point lines up with whatever it is joined to, so a chain runs straight and a long edge goes round it.
|
|
197
|
+
const near = (id: string, ls: { other: string; at: number }[]) => {
|
|
198
|
+
const real = isPoint(id) ? ls : ls.filter((x) => !isPoint(x.other));
|
|
199
|
+
return (real.length ? real : ls).map((x) => x.at);
|
|
200
|
+
};
|
|
201
|
+
const fromPrev = (id: string) => near(id, links.filter((l) => l.b === id).map((l) => ({ other: l.a, at: (top.get(l.a) ?? 0) + l.oa - l.ob })));
|
|
202
|
+
const fromNext = (id: string) => near(id, links.filter((l) => l.a === id).map((l) => ({ other: l.b, at: (top.get(l.b) ?? 0) + l.ob - l.oa })));
|
|
203
|
+
for (let sweep = 0; sweep < 8; sweep++) {
|
|
204
|
+
const order = rows.map((_, i) => i);
|
|
205
|
+
if (sweep % 2) order.reverse();
|
|
206
|
+
for (const i of order) place(rows[i] as string[], sweep % 2 ? fromNext : fromPrev);
|
|
207
|
+
}
|
|
208
|
+
for (const r of rows) place(r, (id) => [...fromPrev(id), ...fromNext(id)]);
|
|
209
|
+
const lo = top.size ? Math.min(...top.values()) : 0;
|
|
210
|
+
for (const [id, v] of top) top.set(id, v - lo);
|
|
211
|
+
const u = (start[start.length - 1] ?? 0) + (width[width.length - 1] ?? 0);
|
|
212
|
+
const v = Math.max(0, ...rows.flat().map((id) => (top.get(id) ?? 0) + slotSize(id).v));
|
|
213
|
+
local.set(c, { rows, start, width, top, u, v });
|
|
214
|
+
if (c !== '') {
|
|
215
|
+
const p = padOf(c);
|
|
216
|
+
extent.set(c, { u: Math.max(p.u0 + u + p.u1, p.minU), v: Math.max(p.v0 + v + p.v1, p.minV) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Inside out, then outside in: each container's place in the drawing.
|
|
221
|
+
const at: Record<string, Rect> = {};
|
|
222
|
+
const origin = new Map<string, { u: number; v: number }>();
|
|
223
|
+
const rect = (u: number, v: number, s: { u: number; v: number }): Rect => (down ? { x: v, y: u, w: s.v, h: s.u } : { x: u, y: v, w: s.u, h: s.v });
|
|
224
|
+
const put = (c: string, ou: number, ov: number) => {
|
|
225
|
+
origin.set(c, { u: ou, v: ov });
|
|
226
|
+
const l = local.get(c) as Local;
|
|
227
|
+
l.rows.forEach((r, i) =>
|
|
228
|
+
r.forEach((id) => {
|
|
229
|
+
if (isPoint(id)) return;
|
|
230
|
+
const u = ou + (l.start[i] as number);
|
|
231
|
+
const v = ov + (l.top.get(id) ?? 0);
|
|
232
|
+
at[id] = rect(u, v, slotSize(id));
|
|
233
|
+
if (boxIds.has(id)) {
|
|
234
|
+
const p = padOf(id);
|
|
235
|
+
put(id, u + p.u0, v + p.v0);
|
|
236
|
+
}
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
put('', S.margin, S.margin);
|
|
241
|
+
const whole = local.get('') as Local;
|
|
242
|
+
const U = whole.u + 2 * S.margin;
|
|
243
|
+
const V = whole.v + 2 * S.margin;
|
|
244
|
+
|
|
245
|
+
// A container's layer l: where it starts and ends along the flow, in the drawing.
|
|
246
|
+
const col = (c: string, l: number): [number, number] => {
|
|
247
|
+
const lc = local.get(c) as Local;
|
|
248
|
+
const o = origin.get(c) ?? { u: 0, v: 0 };
|
|
249
|
+
const c0 = o.u + (lc.start[l] ?? 0);
|
|
250
|
+
return [c0, c0 + (lc.width[l] ?? 0)];
|
|
251
|
+
};
|
|
252
|
+
const layer = (c: string, id: string) => layerIn.get(c)?.get(id) ?? 0;
|
|
253
|
+
const byOf = (s: EdgePathStep | undefined): string => (s && isPoint(s.id) ? (/\|by:([^|]*)\|/.exec(s.id)?.[1] ?? '') : '');
|
|
254
|
+
const edges = g.edgePaths.map((r, ri) => {
|
|
255
|
+
const vOf = (si: number): number => {
|
|
256
|
+
const s = r.steps[si] as EdgePathStep;
|
|
257
|
+
const o = origin.get(s.in) ?? { u: 0, v: 0 };
|
|
258
|
+
return o.v + (local.get(s.in)?.top.get(s.id) ?? 0) + attach(ri, si);
|
|
259
|
+
};
|
|
260
|
+
const runs: Stretch[] = r.steps.map((s, si): Stretch => {
|
|
261
|
+
const prev = r.steps[si - 1];
|
|
262
|
+
const next = r.steps[si + 1];
|
|
263
|
+
const [c0, c1] = col(s.in, layer(s.in, s.id));
|
|
264
|
+
const v = vOf(si);
|
|
265
|
+
if (si === 0) return { a: c0 + slotSize(s.id).u, b: c1, at: v };
|
|
266
|
+
if (si === r.steps.length - 1) return { a: c0, b: c0, at: v };
|
|
267
|
+
if (isPoint(s.id)) {
|
|
268
|
+
const box = byOf(s);
|
|
269
|
+
// Beside a box: from where it left the box's side, or up to where it goes in.
|
|
270
|
+
if (box && prev?.id === box) return { a: col(box, layer(box, r.steps[si - 2]?.id ?? '') + 1)[0], b: c1, at: v };
|
|
271
|
+
if (box && next?.id === box) {
|
|
272
|
+
const ln = layer(box, r.steps[si + 2]?.id ?? '');
|
|
273
|
+
const gap = ln > 0 ? col(box, ln - 1)[1] : (at[box] ? (down ? at[box].y : at[box].x) : c0) + label(box).h + S.pad;
|
|
274
|
+
return { a: c0, b: gap, at: v };
|
|
275
|
+
}
|
|
276
|
+
return { a: c0, b: c1, at: v };
|
|
277
|
+
}
|
|
278
|
+
const p = padOf(s.id);
|
|
279
|
+
const inner = local.get(s.id) as Local;
|
|
280
|
+
// Out of a box: at its far side, or through its side after the layer it leaves.
|
|
281
|
+
if (prev?.in === s.id) {
|
|
282
|
+
if (byOf(next) === s.id) {
|
|
283
|
+
const u = col(s.id, layer(s.id, prev.id) + 1)[0];
|
|
284
|
+
return { a: u, b: u, at: vOf(si + 1) };
|
|
285
|
+
}
|
|
286
|
+
return { a: c0 + p.u0 + inner.u, b: c1, at: v };
|
|
287
|
+
}
|
|
288
|
+
// Into a box: at its near side, or through its side before the layer it goes to.
|
|
289
|
+
if (byOf(prev) === s.id) {
|
|
290
|
+
const u = col(s.id, layer(s.id, next?.id ?? ''))[0];
|
|
291
|
+
return { a: u, b: u, at: vOf(si + 1) };
|
|
292
|
+
}
|
|
293
|
+
return { a: c0, b: c0 + p.u0, at: v };
|
|
294
|
+
});
|
|
295
|
+
return { from: r.from, to: r.to, style: r.style, runs };
|
|
296
|
+
});
|
|
297
|
+
// Each box's return to its next round: from its last layer's item that reaches furthest
|
|
298
|
+
// across the flow, along the band past the content, to its first layer's.
|
|
299
|
+
const returns = g.clusters.flatMap((b): LoopReturn[] => {
|
|
300
|
+
if (!b.cycle) return [];
|
|
301
|
+
const l = local.get(b.id);
|
|
302
|
+
const o = origin.get(b.id);
|
|
303
|
+
if (!l || !o) return [];
|
|
304
|
+
const outer = (row: readonly string[] | undefined) => {
|
|
305
|
+
const ids = (row ?? []).filter((id) => !isPoint(id));
|
|
306
|
+
let best: { u: number; v: number } | null = null;
|
|
307
|
+
for (const id of ids) {
|
|
308
|
+
const s = slotSize(id);
|
|
309
|
+
const v = (l.top.get(id) ?? 0) + s.v;
|
|
310
|
+
if (!best || v > best.v) best = { u: s.u, v };
|
|
311
|
+
}
|
|
312
|
+
return best;
|
|
313
|
+
};
|
|
314
|
+
const real = l.rows.map((r, i) => (r.some((id) => !isPoint(id)) ? i : -1)).filter((i) => i >= 0);
|
|
315
|
+
const fi = real[0] ?? 0;
|
|
316
|
+
const li = real[real.length - 1] ?? 0;
|
|
317
|
+
const last = outer(l.rows[li]);
|
|
318
|
+
const first = outer(l.rows[fi]);
|
|
319
|
+
if (!last || !first) return [];
|
|
320
|
+
// An item's middle along the flow, as wide as its layer; one layer: either side of its middle.
|
|
321
|
+
let from = o.u + (l.start[li] ?? 0) + (l.width[li] ?? 0) / 2;
|
|
322
|
+
let to = o.u + (l.start[fi] ?? 0) + (l.width[fi] ?? 0) / 2;
|
|
323
|
+
if (from - to < 4 * RETURN_R) {
|
|
324
|
+
const d = Math.max(2 * RETURN_R, Math.min(28, Math.min(last.u, first.u) / 4));
|
|
325
|
+
from = (from + to) / 2 + d;
|
|
326
|
+
to = from - 2 * d;
|
|
327
|
+
}
|
|
328
|
+
return [{ box: b.id, from, v0: o.v + last.v, to, v1: o.v + first.v, band: o.v + l.v + RETURN_BAND / 2 }];
|
|
329
|
+
});
|
|
330
|
+
return { w: down ? V : U, h: down ? U : V, at, edges, returns, down };
|
|
331
|
+
}
|
package/src/graph/layout.ts
CHANGED
|
@@ -149,21 +149,42 @@ export function layoutGraph(g: Pick<Arrangement, 'clusters' | 'slots' | 'edgePat
|
|
|
149
149
|
const step = steps[inner] as EdgePathStep;
|
|
150
150
|
return padOf(s.id).v0 + (local.get(s.id)?.top.get(step.id) ?? 0) + attach(ri, inner);
|
|
151
151
|
};
|
|
152
|
+
for (const c of containers) layerIn.set(c, new Map((g.slots[c] ?? []).flatMap((r, i) => r.map((id) => [id, i] as const))));
|
|
153
|
+
// Each container's joins, as steps of the edge paths: slot a of layer l to slot b of layer l + 1.
|
|
154
|
+
const joinsIn = new Map<string, [number, number][]>();
|
|
155
|
+
g.edgePaths.forEach((r, ri) =>
|
|
156
|
+
r.steps.forEach((s, si) => {
|
|
157
|
+
const p = r.steps[si - 1];
|
|
158
|
+
if (!si || !p || p.in !== s.in) return;
|
|
159
|
+
const lv = layerIn.get(s.in);
|
|
160
|
+
if (!lv || lv.get(p.id) === lv.get(s.id)) return;
|
|
161
|
+
const js = joinsIn.get(s.in);
|
|
162
|
+
if (js) js.push([ri, si]);
|
|
163
|
+
else joinsIn.set(s.in, [[ri, si]]);
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
152
166
|
for (const c of containers) {
|
|
153
167
|
const rows = (g.slots[c] ?? []).map((r) => [...r]);
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
const width = rows.map((r) => {
|
|
169
|
+
let w = 0;
|
|
170
|
+
for (const id of r) w = Math.max(w, slotSize(id).u);
|
|
171
|
+
return w;
|
|
172
|
+
});
|
|
156
173
|
const gap = c === '' ? S.gap : S.boxGap;
|
|
157
174
|
const start: number[] = [];
|
|
158
175
|
width.forEach((_, i) => start.push(i ? (start[i - 1] as number) + (width[i - 1] as number) + gap : 0));
|
|
159
176
|
// The joins inside this container: slot a of layer l to slot b of layer l + 1.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
177
|
+
type Link = { a: string; b: string; oa: number; ob: number };
|
|
178
|
+
const links: Link[] = (joinsIn.get(c) ?? []).map(([ri, si]) => {
|
|
179
|
+
const steps = (g.edgePaths[ri] as (typeof g.edgePaths)[number]).steps;
|
|
180
|
+
return { a: (steps[si - 1] as EdgePathStep).id, b: (steps[si] as EdgePathStep).id, oa: attach(ri, si - 1), ob: attach(ri, si) };
|
|
181
|
+
});
|
|
182
|
+
const intoOf = new Map<string, Link[]>();
|
|
183
|
+
const outOf = new Map<string, Link[]>();
|
|
184
|
+
for (const l of links) {
|
|
185
|
+
(intoOf.get(l.b) ?? intoOf.set(l.b, []).get(l.b))?.push(l);
|
|
186
|
+
(outOf.get(l.a) ?? outOf.set(l.a, []).get(l.a))?.push(l);
|
|
187
|
+
}
|
|
167
188
|
// Points sit close to each other and to a node, further from a box (its next-pass mark is on its edge).
|
|
168
189
|
const sepOf = (a: string, b: string | undefined) =>
|
|
169
190
|
b === undefined ? 0 : boxIds.has(a) || boxIds.has(b) || !(isPoint(a) || isPoint(b)) ? S.sep : S.pointSep;
|
|
@@ -198,18 +219,20 @@ export function layoutGraph(g: Pick<Arrangement, 'clusters' | 'slots' | 'edgePat
|
|
|
198
219
|
const real = isPoint(id) ? ls : ls.filter((x) => !isPoint(x.other));
|
|
199
220
|
return (real.length ? real : ls).map((x) => x.at);
|
|
200
221
|
};
|
|
201
|
-
const fromPrev = (id: string) => near(id,
|
|
202
|
-
const fromNext = (id: string) => near(id,
|
|
222
|
+
const fromPrev = (id: string) => near(id, (intoOf.get(id) ?? []).map((l) => ({ other: l.a, at: (top.get(l.a) ?? 0) + l.oa - l.ob })));
|
|
223
|
+
const fromNext = (id: string) => near(id, (outOf.get(id) ?? []).map((l) => ({ other: l.b, at: (top.get(l.b) ?? 0) + l.ob - l.oa })));
|
|
203
224
|
for (let sweep = 0; sweep < 8; sweep++) {
|
|
204
225
|
const order = rows.map((_, i) => i);
|
|
205
226
|
if (sweep % 2) order.reverse();
|
|
206
227
|
for (const i of order) place(rows[i] as string[], sweep % 2 ? fromNext : fromPrev);
|
|
207
228
|
}
|
|
208
229
|
for (const r of rows) place(r, (id) => [...fromPrev(id), ...fromNext(id)]);
|
|
209
|
-
|
|
230
|
+
let lo = top.size ? Infinity : 0;
|
|
231
|
+
for (const x of top.values()) lo = Math.min(lo, x);
|
|
210
232
|
for (const [id, v] of top) top.set(id, v - lo);
|
|
211
233
|
const u = (start[start.length - 1] ?? 0) + (width[width.length - 1] ?? 0);
|
|
212
|
-
|
|
234
|
+
let v = 0;
|
|
235
|
+
for (const r of rows) for (const id of r) v = Math.max(v, (top.get(id) ?? 0) + slotSize(id).v);
|
|
213
236
|
local.set(c, { rows, start, width, top, u, v });
|
|
214
237
|
if (c !== '') {
|
|
215
238
|
const p = padOf(c);
|
|
@@ -18,11 +18,34 @@ export function neighbours(
|
|
|
18
18
|
const x = nodes.get(id);
|
|
19
19
|
return !x ? id : x.cluster ? inCluster(x.label) : x.label;
|
|
20
20
|
};
|
|
21
|
+
const ix = indexOf(g);
|
|
22
|
+
const stubs = ix.stubsTo.get(n.id) ?? [];
|
|
23
|
+
const edges = ix.edgesTo.get(n.id) ?? [];
|
|
21
24
|
const into = (style: EdgeStyle) => [
|
|
22
|
-
...
|
|
23
|
-
...
|
|
25
|
+
...stubs.filter((s) => s.style === style).map((s) => s.label),
|
|
26
|
+
...edges.filter((e) => e.style === style).map((e) => label(e.from)),
|
|
24
27
|
];
|
|
25
|
-
return { solid: into('solid'), dotted: into('dotted'), dashed: into('dashed'), later: [...new Set(
|
|
28
|
+
return { solid: into('solid'), dotted: into('dotted'), dashed: into('dashed'), later: [...new Set((ix.edgesFrom.get(n.id) ?? []).map((e) => label(e.to)))] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Links = Pick<GraphInput, 'edges' | 'stubs'>;
|
|
32
|
+
type LinkIndex = { stubsTo: Map<string, Links['stubs'][number][]>; edgesTo: Map<string, Links['edges'][number][]>; edgesFrom: Map<string, Links['edges'][number][]> };
|
|
33
|
+
|
|
34
|
+
/** A graph's stubs and edges by the node they lead to, and its edges by the node they start at, in their order; once per graph. */
|
|
35
|
+
const indexes = new WeakMap<Links, LinkIndex>();
|
|
36
|
+
function indexOf(g: Links): LinkIndex {
|
|
37
|
+
let ix = indexes.get(g);
|
|
38
|
+
if (!ix) {
|
|
39
|
+
const add = <T>(m: Map<string, T[]>, k: string, v: T) => (m.get(k) ?? m.set(k, []).get(k))?.push(v);
|
|
40
|
+
ix = { stubsTo: new Map(), edgesTo: new Map(), edgesFrom: new Map() };
|
|
41
|
+
for (const s of g.stubs) add(ix.stubsTo, s.to, s);
|
|
42
|
+
for (const e of g.edges) {
|
|
43
|
+
add(ix.edgesTo, e.to, e);
|
|
44
|
+
add(ix.edgesFrom, e.from, e);
|
|
45
|
+
}
|
|
46
|
+
indexes.set(g, ix);
|
|
47
|
+
}
|
|
48
|
+
return ix;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
/**
|
package/src/lanes/stack.ts
CHANGED
|
@@ -36,9 +36,14 @@ const STEP_MS = 60e3;
|
|
|
36
36
|
export function fitRange(lanes: readonly Lane[], now: number): TimeRange | null {
|
|
37
37
|
const bars = lanes.flatMap((l) => l.bars);
|
|
38
38
|
if (!bars.length) return null;
|
|
39
|
-
|
|
39
|
+
let start = Infinity;
|
|
40
|
+
let last = -Infinity;
|
|
41
|
+
for (const b of bars) {
|
|
42
|
+
start = Math.min(start, b.start);
|
|
43
|
+
last = Math.max(last, b.end ?? now);
|
|
44
|
+
}
|
|
40
45
|
const running = bars.some((b) => b.end === null);
|
|
41
|
-
const end = running ? start + Math.ceil((now - start + MARGIN_MS) / STEP_MS) * STEP_MS :
|
|
46
|
+
const end = running ? start + Math.ceil((now - start + MARGIN_MS) / STEP_MS) * STEP_MS : last;
|
|
42
47
|
return { start, end };
|
|
43
48
|
}
|
|
44
49
|
|
package/src/table/types.ts
CHANGED
|
@@ -56,6 +56,9 @@ export const isSortable = <R>(c: DataColumn<R>): boolean => c.sortable ?? !!c.co
|
|
|
56
56
|
* its cell when that is a string or a number; equal rows keep their order. The rows
|
|
57
57
|
* as they are when no column matches.
|
|
58
58
|
*/
|
|
59
|
+
/** The order `localeCompare` gives with `numeric`, made once. */
|
|
60
|
+
const numericText = new Intl.Collator(undefined, { numeric: true });
|
|
61
|
+
|
|
59
62
|
export function sortRows<R>(rows: readonly R[], columns: readonly DataColumn<R>[], sort: SortState | null): R[] {
|
|
60
63
|
const c = sort && columns.find((x) => x.id === sort.id);
|
|
61
64
|
if (!sort || !c) return [...rows];
|
|
@@ -68,6 +71,6 @@ export function sortRows<R>(rows: readonly R[], columns: readonly DataColumn<R>[
|
|
|
68
71
|
};
|
|
69
72
|
const keyed = rows.map((r, i) => ({ r, i, v: compare ? 0 : value(r, i) }));
|
|
70
73
|
const cmp = (a: (typeof keyed)[number], b: (typeof keyed)[number]) =>
|
|
71
|
-
compare ? compare(a.r, b.r) : typeof a.v === 'number' && typeof b.v === 'number' ? a.v - b.v : String(a.v)
|
|
74
|
+
compare ? compare(a.r, b.r) : typeof a.v === 'number' && typeof b.v === 'number' ? a.v - b.v : numericText.compare(String(a.v), String(b.v));
|
|
72
75
|
return keyed.sort((a, b) => sign * cmp(a, b) || a.i - b.i).map((x) => x.r);
|
|
73
76
|
}
|