@weasel-js/diagram 1.9.1 → 1.9.3
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/README.md +53 -7
- package/dist/{chunk-6VYT4WCM.js → chunk-ERT3XWBS.js} +286 -110
- package/dist/chunk-ERT3XWBS.js.map +1 -0
- package/dist/index.d.ts +229 -28
- package/dist/index.js +363 -78
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +1 -1
- package/dist/{layout.entry-ClWUwH16.d.ts → layout.entry-KHbx5uGC.d.ts} +338 -275
- package/dist/layout.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-6VYT4WCM.js.map +0 -1
package/README.md
CHANGED
|
@@ -95,19 +95,65 @@ relaxation seeded from the current positions, so re-running it keeps relaxing.
|
|
|
95
95
|
### From data
|
|
96
96
|
|
|
97
97
|
`diagramScene` turns plain `{ nodes, edges }` into a laid-out scene, and
|
|
98
|
-
`DiagramView` draws
|
|
98
|
+
`DiagramView` draws it, with pan, zoom and picking:
|
|
99
99
|
|
|
100
100
|
```tsx
|
|
101
101
|
const specs = useMemo(() => diagramScene(data, { nodeStyle }), [data]);
|
|
102
102
|
return <DiagramView specs={specs} width={360} height={600} selected={id} onSelect={setId} />;
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
Every box is a `buildBody` body, so a data node takes the same knobs a body
|
|
106
|
+
does — `outline`, `rows`, `padding`, `gap`, `verticalAlign`, `ports` — plus `pinned`, a
|
|
107
|
+
starting `at`, and a minimum `width`/`height`; `lines` is shorthand for one
|
|
108
|
+
label row each. An edge can name `fromPort`/`toPort`, its own `router`,
|
|
109
|
+
`waypoints` and `labelPlacement`.
|
|
110
|
+
|
|
111
|
+
`groups` draws a box around some nodes, with an optional `label` inside it:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
diagramScene({ nodes, edges, groups: [{ id: 'ingest', members: ['fetch', 'decode'], label: 'ingest' }] });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`layered` and `tree` keep a group's members together. Each group gets a column
|
|
118
|
+
on the cross axis that nothing else enters, in every rank from its first
|
|
119
|
+
member's to its last, and rank gaps widen where a box needs the room. The box
|
|
120
|
+
is an ordinary scene node that depends on its members and derives its pose from
|
|
121
|
+
theirs (`groupDerivePose`), so dragging a member carries the box along. An edge
|
|
122
|
+
whose `from` or `to` names a group meets the box, and ranks against the
|
|
123
|
+
group's first stage. Groups are flat, and `force` ignores them. A scene built
|
|
124
|
+
any other way joins in with a node whose trait carries `group` and whose
|
|
125
|
+
`dependsOn` lists its members.
|
|
126
|
+
|
|
127
|
+
Text is sized from character counts unless `measure` is given, so layout runs
|
|
128
|
+
without a canvas. The layout defaults to `layered`, picked by name or passed as
|
|
129
|
+
any `LayoutFn`; `layoutOptions` merge over `order: 'barycenter'`, and
|
|
130
|
+
`'none'` leaves every node at its `at`.
|
|
131
|
+
|
|
132
|
+
`DiagramView` is the front door for the whole package. The data owns the
|
|
133
|
+
diagram: a new `specs` array is reconciled into the scene the view already
|
|
134
|
+
holds (`reconcileSpecs` in core), so the view, the pick and anything moved
|
|
135
|
+
since survive it. Editing reports rather than writes —
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<DiagramView
|
|
139
|
+
ref={view}
|
|
140
|
+
specs={specs}
|
|
141
|
+
onMove={(moves) => setNodes((ns) => withPositions(ns, moves))}
|
|
142
|
+
onConnect={(edge) => setEdges((es) => [...es, edge])}
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
— `onMove` turns on dragging and hears every node a drag or a layout run
|
|
147
|
+
moved; `onConnect` makes ports grabbable and hears the edge a connect
|
|
148
|
+
describes. Store a move as the node's `at`, with `pinned: true` unless the
|
|
149
|
+
scene uses `layout: 'none'`, or the next `diagramScene` lays it out again.
|
|
150
|
+
`view.current.layout('layered')` re-runs a layout animated from where the
|
|
151
|
+
nodes stand; `'force'` relaxes live.
|
|
152
|
+
|
|
153
|
+
Labels paint only in a registered font family, and the default is
|
|
154
|
+
sans-serif, so call `registerCanvasFont('sans-serif')` once or they render
|
|
155
|
+
blank. `minScale` stops the initial fit from shrinking a wide diagram past
|
|
156
|
+
readable; the rest is a pan away.
|
|
111
157
|
|
|
112
158
|
## Design notes
|
|
113
159
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { rectPath, polygonFromPoints, PathBuilder, PATH_M, PATH_L, PATH_C, flattenCubic, PATH_Q, PATH_Z, AUTO_POSE_DESCRIPTOR, rotatePoint, createSimulation } from '@weasel-js/core/math';
|
|
2
|
+
import { cssFamilyName } from '@weasel-js/core';
|
|
2
3
|
|
|
3
4
|
// src/outline.ts
|
|
4
5
|
var LEAN = 0.2;
|
|
@@ -188,6 +189,7 @@ var dataKeyReader = (node) => {
|
|
|
188
189
|
const trait = data[DIAGRAM_TRAIT_KEY];
|
|
189
190
|
if (trait === null || typeof trait !== "object") return null;
|
|
190
191
|
if ("from" in trait && "to" in trait) return null;
|
|
192
|
+
if ("label" in trait || "anchor" in trait) return null;
|
|
191
193
|
return trait;
|
|
192
194
|
};
|
|
193
195
|
function createDiagramNodes(entries) {
|
|
@@ -272,20 +274,21 @@ function portOf(node, pose, portId, opts = {}) {
|
|
|
272
274
|
function typeOf(spec) {
|
|
273
275
|
return spec.type === void 0 ? {} : { type: spec.type };
|
|
274
276
|
}
|
|
275
|
-
|
|
276
|
-
// src/body.ts
|
|
277
277
|
var DEFAULT_PADDING = 8;
|
|
278
278
|
var DEFAULT_GAP = 4;
|
|
279
279
|
var FALLBACK_LINE = 16;
|
|
280
280
|
function canvasMeasure(ctx) {
|
|
281
281
|
return (text, style) => {
|
|
282
282
|
const size = style.fontSize ?? FALLBACK_LINE;
|
|
283
|
-
ctx.font = `${style.bold === true ? "bold " : ""}${size}px ${style.fontFamily ?? "sans-serif"}`;
|
|
283
|
+
ctx.font = `${style.bold === true ? "bold " : ""}${size}px ${cssFamilyName(style.fontFamily ?? "sans-serif")}`;
|
|
284
284
|
return { width: ctx.measureText(text).width, height: Math.ceil(size * 1.2) };
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
function measureRow(row, measure) {
|
|
288
|
-
const text = (s, style) => measure === void 0 ? {
|
|
288
|
+
const text = (s, style) => measure === void 0 ? {
|
|
289
|
+
width: s.length * (style?.fontSize ?? FALLBACK_LINE) * 0.6,
|
|
290
|
+
height: style?.fontSize === void 0 ? FALLBACK_LINE : Math.ceil(style.fontSize * 1.2)
|
|
291
|
+
} : measure(s, style ?? {});
|
|
289
292
|
switch (row.kind) {
|
|
290
293
|
case "label":
|
|
291
294
|
return text(row.text, row.style);
|
|
@@ -333,9 +336,13 @@ function layoutBody(spec, bounds, measure) {
|
|
|
333
336
|
const gap = spec.gap ?? DEFAULT_GAP;
|
|
334
337
|
const inner = contentBox(spec.outline, bounds);
|
|
335
338
|
const width = Math.max(inner.width - padding * 2, 0);
|
|
336
|
-
|
|
339
|
+
const heights = spec.rows.map((row) => measureRow(row, measure).height);
|
|
340
|
+
const used = heights.reduce((sum, h) => sum + h, 0) + gap * Math.max(heights.length - 1, 0);
|
|
341
|
+
const slack = Math.max(inner.height - padding * 2 - used, 0);
|
|
342
|
+
const share = { top: 0, center: 0.5, bottom: 1 }[spec.verticalAlign ?? "center"];
|
|
343
|
+
let y = inner.y + padding + slack * share;
|
|
337
344
|
return spec.rows.map((row, index) => {
|
|
338
|
-
const h =
|
|
345
|
+
const h = heights[index];
|
|
339
346
|
const box = { row, index, x: inner.x + padding, y, width, height: h };
|
|
340
347
|
y += h + gap;
|
|
341
348
|
return box;
|
|
@@ -366,13 +373,13 @@ function layoutRowPorts(row, box, measure) {
|
|
|
366
373
|
}
|
|
367
374
|
function bodyTrait(spec, bounds, measure) {
|
|
368
375
|
const outline = spec.outline;
|
|
369
|
-
const compass = [
|
|
376
|
+
const compass = spec.ports ?? [
|
|
370
377
|
{ id: "n", at: COMPASS.n },
|
|
371
378
|
{ id: "e", at: COMPASS.e },
|
|
372
379
|
{ id: "s", at: COMPASS.s },
|
|
373
380
|
{ id: "w", at: COMPASS.w }
|
|
374
381
|
];
|
|
375
|
-
if (bounds.height <= 0) return { outline, ports: compass };
|
|
382
|
+
if (bounds.height <= 0) return { outline, ports: [...compass] };
|
|
376
383
|
const rowPorts = [];
|
|
377
384
|
let hasLeft = false;
|
|
378
385
|
let hasRight = false;
|
|
@@ -388,7 +395,7 @@ function bodyTrait(spec, bounds, measure) {
|
|
|
388
395
|
));
|
|
389
396
|
}
|
|
390
397
|
}
|
|
391
|
-
const kept = compass.filter((p) => !(p.id === "w" && hasLeft) && !(p.id === "e" && hasRight));
|
|
398
|
+
const kept = spec.ports !== void 0 ? compass : compass.filter((p) => !(p.id === "w" && hasLeft) && !(p.id === "e" && hasRight));
|
|
392
399
|
return { outline, ports: [...kept, ...rowPorts] };
|
|
393
400
|
}
|
|
394
401
|
function withType(spec, type) {
|
|
@@ -537,112 +544,164 @@ function graphOrder(graph) {
|
|
|
537
544
|
return (id) => index.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
538
545
|
}
|
|
539
546
|
|
|
540
|
-
// src/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const key = /* @__PURE__ */ new Map();
|
|
549
|
-
for (const [i, n] of row.entries()) {
|
|
550
|
-
const at = neighbors(n.id).map((id) => index.get(id)).filter((v) => v !== void 0);
|
|
551
|
-
key.set(n.id, at.length === 0 ? i : at.reduce((s, v) => s + v, 0) / at.length);
|
|
552
|
-
}
|
|
553
|
-
row.sort((a, b) => key.get(a.id) - key.get(b.id) || index.get(a.id) - index.get(b.id));
|
|
554
|
-
reindex(row);
|
|
547
|
+
// src/cluster.ts
|
|
548
|
+
function insetOf(inset) {
|
|
549
|
+
if (typeof inset === "number") return { top: inset, right: inset, bottom: inset, left: inset };
|
|
550
|
+
return {
|
|
551
|
+
top: inset?.top ?? DEFAULT_GROUP_INSET,
|
|
552
|
+
right: inset?.right ?? DEFAULT_GROUP_INSET,
|
|
553
|
+
bottom: inset?.bottom ?? DEFAULT_GROUP_INSET,
|
|
554
|
+
left: inset?.left ?? DEFAULT_GROUP_INSET
|
|
555
555
|
};
|
|
556
|
-
const ups = (id) => graph.incoming(id).filter((e) => !back.has(e.id)).map((e) => e.from);
|
|
557
|
-
const downs = (id) => graph.outgoing(id).filter((e) => !back.has(e.id)).map((e) => e.to);
|
|
558
|
-
for (let s = 0; s < SWEEPS; s++) {
|
|
559
|
-
for (let r = 1; r < out.length; r++) reorder(out[r], ups);
|
|
560
|
-
for (let r = out.length - 2; r >= 0; r--) reorder(out[r], downs);
|
|
561
|
-
}
|
|
562
|
-
return out;
|
|
563
556
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const walk = (id) => {
|
|
571
|
-
if (done.has(id)) return;
|
|
572
|
-
onStack.add(id);
|
|
573
|
-
for (const edge of graph.outgoing(id)) {
|
|
574
|
-
if (onStack.has(edge.to)) back.add(edge.id);
|
|
575
|
-
else walk(edge.to);
|
|
576
|
-
}
|
|
577
|
-
onStack.delete(id);
|
|
578
|
-
done.add(id);
|
|
579
|
-
};
|
|
580
|
-
for (const node of graph.nodes) walk(node.id);
|
|
581
|
-
return back;
|
|
557
|
+
var DEFAULT_GROUP_INSET = 12;
|
|
558
|
+
function canonical(inset, axes) {
|
|
559
|
+
if (axes.rank === "y") {
|
|
560
|
+
return axes.sign === 1 ? { crossLo: inset.left, crossHi: inset.right, rankLo: inset.top, rankHi: inset.bottom } : { crossLo: inset.left, crossHi: inset.right, rankLo: inset.bottom, rankHi: inset.top };
|
|
561
|
+
}
|
|
562
|
+
return axes.sign === 1 ? { crossLo: inset.top, crossHi: inset.bottom, rankLo: inset.left, rankHi: inset.right } : { crossLo: inset.top, crossHi: inset.bottom, rankLo: inset.right, rankHi: inset.left };
|
|
582
563
|
}
|
|
583
|
-
function
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
resolving.delete(id);
|
|
597
|
-
ranks.set(id, rank);
|
|
598
|
-
return rank;
|
|
599
|
-
};
|
|
600
|
-
for (const node of graph.nodes) rankOf(node.id);
|
|
601
|
-
return ranks;
|
|
564
|
+
function rankGaps(graph, rows, axes, rankGap) {
|
|
565
|
+
const gaps = rows.slice(1).map(() => rankGap);
|
|
566
|
+
const ends = /* @__PURE__ */ new Map();
|
|
567
|
+
const starts = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const span of groupSpans(graph, rows)) {
|
|
569
|
+
const inset = canonical(span.group.inset, axes);
|
|
570
|
+
ends.set(span.hi, Math.max(ends.get(span.hi) ?? 0, inset.rankHi));
|
|
571
|
+
starts.set(span.lo, Math.max(starts.get(span.lo) ?? 0, inset.rankLo));
|
|
572
|
+
}
|
|
573
|
+
for (let i = 0; i < gaps.length; i++) {
|
|
574
|
+
gaps[i] = Math.max(gaps[i], rankGap + (ends.get(i) ?? 0) + (starts.get(i + 1) ?? 0));
|
|
575
|
+
}
|
|
576
|
+
return gaps;
|
|
602
577
|
}
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
for (const node of graph.nodes) {
|
|
612
|
-
const rank = ranks.get(node.id) ?? 0;
|
|
613
|
-
const list = byRank.get(rank);
|
|
614
|
-
if (list === void 0) byRank.set(rank, [node]);
|
|
615
|
-
else list.push(node);
|
|
578
|
+
function groupSpans(graph, rows) {
|
|
579
|
+
const rankOf = /* @__PURE__ */ new Map();
|
|
580
|
+
rows.forEach((row, i) => row.forEach((n) => rankOf.set(n.id, i)));
|
|
581
|
+
const spans = [];
|
|
582
|
+
for (const group of graph.groups ?? []) {
|
|
583
|
+
const ranks = group.members.map((id) => rankOf.get(id)).filter((r) => r !== void 0);
|
|
584
|
+
if (ranks.length === 0) continue;
|
|
585
|
+
spans.push({ group, lo: Math.min(...ranks), hi: Math.max(...ranks) });
|
|
616
586
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
for (const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
587
|
+
return spans;
|
|
588
|
+
}
|
|
589
|
+
function packClusters(graph, rows, preferred, axes, nodeGap) {
|
|
590
|
+
const spans = groupSpans(graph, rows);
|
|
591
|
+
const spanOf = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const span of spans) for (const id of span.group.members) spanOf.set(id, span);
|
|
593
|
+
const width = (n) => extent(n.bounds, axes.cross);
|
|
594
|
+
const pref = (n) => preferred.get(n.id) ?? 0;
|
|
595
|
+
const packedWidth = (ns) => ns.reduce((s, n) => s + width(n), 0) + Math.max(0, ns.length - 1) * nodeGap;
|
|
596
|
+
const insets = new Map(spans.map((s) => [s, canonical(s.group.inset, axes)]));
|
|
597
|
+
const inner = /* @__PURE__ */ new Map();
|
|
598
|
+
const centerOf = /* @__PURE__ */ new Map();
|
|
599
|
+
for (const span of spans) {
|
|
600
|
+
let widest = 0;
|
|
601
|
+
let sum = 0;
|
|
602
|
+
let count = 0;
|
|
603
|
+
for (const row of rows) {
|
|
604
|
+
const members = row.filter((n) => spanOf.get(n.id) === span);
|
|
605
|
+
widest = Math.max(widest, packedWidth(members));
|
|
606
|
+
for (const n of members) {
|
|
607
|
+
sum += pref(n) + width(n) / 2;
|
|
608
|
+
count++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
inner.set(span, widest);
|
|
612
|
+
centerOf.set(span, count === 0 ? 0 : sum / count);
|
|
626
613
|
}
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const row = byRank.get(rank);
|
|
631
|
-
const p = packed.get(rank);
|
|
632
|
-
const indent = (widest - p.span) / 2;
|
|
633
|
-
const depth = Math.max(0, ...row.map((n) => extent(n.bounds, axes.rank)));
|
|
614
|
+
const outer = (span) => inner.get(span) + insets.get(span).crossLo + insets.get(span).crossHi;
|
|
615
|
+
const itemsByRow = rows.map((row, r) => {
|
|
616
|
+
const items = [];
|
|
634
617
|
for (const node of row) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
618
|
+
if (!spanOf.has(node.id)) items.push({ kind: "node", node, center: pref(node) + width(node) / 2 });
|
|
619
|
+
}
|
|
620
|
+
for (const span of spans) {
|
|
621
|
+
if (r < span.lo || r > span.hi) continue;
|
|
622
|
+
items.push({
|
|
623
|
+
kind: "group",
|
|
624
|
+
span,
|
|
625
|
+
members: row.filter((n) => spanOf.get(n.id) === span),
|
|
626
|
+
center: centerOf.get(span)
|
|
640
627
|
});
|
|
641
628
|
}
|
|
642
|
-
|
|
629
|
+
return items.map((item, i) => ({ item, i })).sort((a, b) => a.item.center - b.item.center || a.i - b.i).map(({ item }) => item);
|
|
630
|
+
});
|
|
631
|
+
const start = new Map(spans.map((s) => [s, centerOf.get(s) - outer(s) / 2]));
|
|
632
|
+
const at = /* @__PURE__ */ new Map();
|
|
633
|
+
for (let pass = 0; pass <= spans.length; pass++) {
|
|
634
|
+
let raised = false;
|
|
635
|
+
for (const items of itemsByRow) {
|
|
636
|
+
let cursor = -Infinity;
|
|
637
|
+
for (const item of items) {
|
|
638
|
+
if (item.kind === "node") {
|
|
639
|
+
const x2 = Math.max(pref(item.node), cursor);
|
|
640
|
+
at.set(item.node.id, x2);
|
|
641
|
+
cursor = x2 + width(item.node) + nodeGap;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const s = start.get(item.span);
|
|
645
|
+
const x = Math.max(s, cursor);
|
|
646
|
+
if (x > s) {
|
|
647
|
+
start.set(item.span, x);
|
|
648
|
+
raised = true;
|
|
649
|
+
}
|
|
650
|
+
const inset = insets.get(item.span);
|
|
651
|
+
let m = x + inset.crossLo + (inner.get(item.span) - packedWidth(item.members)) / 2;
|
|
652
|
+
for (const node of item.members) {
|
|
653
|
+
at.set(node.id, m);
|
|
654
|
+
m += width(node) + nodeGap;
|
|
655
|
+
}
|
|
656
|
+
cursor = x + outer(item.span) + nodeGap;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (!raised) break;
|
|
643
660
|
}
|
|
644
|
-
return
|
|
645
|
-
}
|
|
661
|
+
return at;
|
|
662
|
+
}
|
|
663
|
+
function expandGroupEdges(edges, groups) {
|
|
664
|
+
const byId = new Map(groups.map((g) => [g.id, g]));
|
|
665
|
+
if (byId.size === 0) return [...edges];
|
|
666
|
+
const groupOf = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const g of groups) for (const m of g.members) groupOf.set(m, g.id);
|
|
668
|
+
const entries = /* @__PURE__ */ new Map();
|
|
669
|
+
const exits = /* @__PURE__ */ new Map();
|
|
670
|
+
for (const g of groups) {
|
|
671
|
+
const inside = new Set(g.members);
|
|
672
|
+
const pointedAt = /* @__PURE__ */ new Set();
|
|
673
|
+
const pointing = /* @__PURE__ */ new Set();
|
|
674
|
+
for (const e of edges) {
|
|
675
|
+
if (inside.has(e.from) && inside.has(e.to) && e.from !== e.to) {
|
|
676
|
+
pointedAt.add(e.to);
|
|
677
|
+
pointing.add(e.from);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const entry = g.members.filter((m) => !pointedAt.has(m));
|
|
681
|
+
const exit = g.members.filter((m) => !pointing.has(m));
|
|
682
|
+
entries.set(g.id, entry.length > 0 ? entry : g.members);
|
|
683
|
+
exits.set(g.id, exit.length > 0 ? exit : g.members);
|
|
684
|
+
}
|
|
685
|
+
const out = [];
|
|
686
|
+
for (const e of edges) {
|
|
687
|
+
const fromGroup = byId.has(e.from) ? e.from : void 0;
|
|
688
|
+
const toGroup = byId.has(e.to) ? e.to : void 0;
|
|
689
|
+
if (fromGroup === void 0 && toGroup === void 0) {
|
|
690
|
+
out.push(e);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const fromOwner = fromGroup ?? groupOf.get(e.from);
|
|
694
|
+
const toOwner = toGroup ?? groupOf.get(e.to);
|
|
695
|
+
if (fromOwner !== void 0 && fromOwner === toOwner) continue;
|
|
696
|
+
const froms = fromGroup !== void 0 ? exits.get(fromGroup) : [e.from];
|
|
697
|
+
const tos = toGroup !== void 0 ? entries.get(toGroup) : [e.to];
|
|
698
|
+
let k = 0;
|
|
699
|
+
for (const from of froms) {
|
|
700
|
+
for (const to of tos) out.push({ id: `${e.id}#${k++}`, from, to });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
646
705
|
var DEFAULTS = {
|
|
647
706
|
iterations: 300,
|
|
648
707
|
linkDistance: 160,
|
|
@@ -798,6 +857,113 @@ function separate(bodies) {
|
|
|
798
857
|
});
|
|
799
858
|
}
|
|
800
859
|
|
|
860
|
+
// src/barycenter.ts
|
|
861
|
+
var SWEEPS = 4;
|
|
862
|
+
function barycenterOrder(graph, rows, back) {
|
|
863
|
+
const out = rows.map((row) => [...row]);
|
|
864
|
+
const index = /* @__PURE__ */ new Map();
|
|
865
|
+
const reindex = (row) => row.forEach((n, i) => index.set(n.id, i));
|
|
866
|
+
out.forEach(reindex);
|
|
867
|
+
const reorder = (row, neighbors) => {
|
|
868
|
+
const key = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const [i, n] of row.entries()) {
|
|
870
|
+
const at = neighbors(n.id).map((id) => index.get(id)).filter((v) => v !== void 0);
|
|
871
|
+
key.set(n.id, at.length === 0 ? i : at.reduce((s, v) => s + v, 0) / at.length);
|
|
872
|
+
}
|
|
873
|
+
row.sort((a, b) => key.get(a.id) - key.get(b.id) || index.get(a.id) - index.get(b.id));
|
|
874
|
+
reindex(row);
|
|
875
|
+
};
|
|
876
|
+
const ups = (id) => graph.incoming(id).filter((e) => !back.has(e.id)).map((e) => e.from);
|
|
877
|
+
const downs = (id) => graph.outgoing(id).filter((e) => !back.has(e.id)).map((e) => e.to);
|
|
878
|
+
for (let s = 0; s < SWEEPS; s++) {
|
|
879
|
+
for (let r = 1; r < out.length; r++) reorder(out[r], ups);
|
|
880
|
+
for (let r = out.length - 2; r >= 0; r--) reorder(out[r], downs);
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/layered.ts
|
|
886
|
+
function backEdges(graph) {
|
|
887
|
+
const back = /* @__PURE__ */ new Set();
|
|
888
|
+
const done = /* @__PURE__ */ new Set();
|
|
889
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
890
|
+
const walk = (id) => {
|
|
891
|
+
if (done.has(id)) return;
|
|
892
|
+
onStack.add(id);
|
|
893
|
+
for (const edge of graph.outgoing(id)) {
|
|
894
|
+
if (onStack.has(edge.to)) back.add(edge.id);
|
|
895
|
+
else walk(edge.to);
|
|
896
|
+
}
|
|
897
|
+
onStack.delete(id);
|
|
898
|
+
done.add(id);
|
|
899
|
+
};
|
|
900
|
+
for (const node of graph.nodes) walk(node.id);
|
|
901
|
+
return back;
|
|
902
|
+
}
|
|
903
|
+
function ranksOf(graph, back) {
|
|
904
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
905
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
906
|
+
const rankOf = (id) => {
|
|
907
|
+
const known = ranks.get(id);
|
|
908
|
+
if (known !== void 0) return known;
|
|
909
|
+
if (resolving.has(id)) return 0;
|
|
910
|
+
resolving.add(id);
|
|
911
|
+
let rank = 0;
|
|
912
|
+
for (const edge of graph.incoming(id)) {
|
|
913
|
+
if (back.has(edge.id)) continue;
|
|
914
|
+
rank = Math.max(rank, rankOf(edge.from) + 1);
|
|
915
|
+
}
|
|
916
|
+
resolving.delete(id);
|
|
917
|
+
ranks.set(id, rank);
|
|
918
|
+
return rank;
|
|
919
|
+
};
|
|
920
|
+
for (const node of graph.nodes) rankOf(node.id);
|
|
921
|
+
return ranks;
|
|
922
|
+
}
|
|
923
|
+
var layered = (graph, opts = {}) => {
|
|
924
|
+
const axes = axesFor(opts.direction);
|
|
925
|
+
const nodeGap = opts.nodeGap ?? DEFAULT_NODE_GAP;
|
|
926
|
+
const rankGap = opts.rankGap ?? DEFAULT_RANK_GAP;
|
|
927
|
+
const order = graphOrder(graph);
|
|
928
|
+
const back = backEdges(graph);
|
|
929
|
+
const ranks = ranksOf(graph, back);
|
|
930
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
931
|
+
for (const node of graph.nodes) {
|
|
932
|
+
const rank = ranks.get(node.id) ?? 0;
|
|
933
|
+
const list = byRank.get(rank);
|
|
934
|
+
if (list === void 0) byRank.set(rank, [node]);
|
|
935
|
+
else list.push(node);
|
|
936
|
+
}
|
|
937
|
+
const rows = [...byRank.keys()].sort((a, b) => a - b);
|
|
938
|
+
let ordered = rows.map((rank) => seededOrder(byRank.get(rank), axes.cross, order));
|
|
939
|
+
if (opts.order === "barycenter") ordered = barycenterOrder(graph, ordered, back);
|
|
940
|
+
const packed = ordered.map((row) => packAcross(row, axes.cross, nodeGap));
|
|
941
|
+
const widest = Math.max(0, ...packed.map((p) => p.span));
|
|
942
|
+
const preferred = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const [i, row] of ordered.entries()) {
|
|
944
|
+
const indent = (widest - packed[i].span) / 2;
|
|
945
|
+
for (const node of row) preferred.set(node.id, indent + packed[i].at.get(node.id));
|
|
946
|
+
}
|
|
947
|
+
const grouped = (graph.groups?.length ?? 0) > 0;
|
|
948
|
+
const crossAt = grouped ? packClusters(graph, ordered, preferred, axes, nodeGap) : preferred;
|
|
949
|
+
const gaps = grouped ? rankGaps(graph, ordered, axes, rankGap) : ordered.slice(1).map(() => rankGap);
|
|
950
|
+
const slots = /* @__PURE__ */ new Map();
|
|
951
|
+
let along = 0;
|
|
952
|
+
for (const [i, row] of ordered.entries()) {
|
|
953
|
+
const depth = Math.max(0, ...row.map((n) => extent(n.bounds, axes.rank)));
|
|
954
|
+
for (const node of row) {
|
|
955
|
+
slots.set(node.id, {
|
|
956
|
+
cross: crossAt.get(node.id),
|
|
957
|
+
// Centered in its rank's band, so a tall node does not push its
|
|
958
|
+
// neighbors' edges off the line they read along.
|
|
959
|
+
rank: along + (depth - extent(node.bounds, axes.rank)) / 2
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
along += depth + (gaps[i] ?? 0);
|
|
963
|
+
}
|
|
964
|
+
return settle(graph, slots, opts);
|
|
965
|
+
};
|
|
966
|
+
|
|
801
967
|
// src/tree.ts
|
|
802
968
|
function forestOf(graph, crossAxis) {
|
|
803
969
|
const order = graphOrder(graph);
|
|
@@ -841,11 +1007,15 @@ var tree = (graph, opts = {}) => {
|
|
|
841
1007
|
const at = depth.get(node.id) ?? 0;
|
|
842
1008
|
bands.set(at, Math.max(bands.get(at) ?? 0, extent(node.bounds, axes.rank)));
|
|
843
1009
|
}
|
|
1010
|
+
const depths = [...bands.keys()].sort((a, b) => a - b);
|
|
1011
|
+
const rowsAt = depths.map((at) => graph.nodes.filter((n) => (depth.get(n.id) ?? 0) === at));
|
|
1012
|
+
const grouped = (graph.groups?.length ?? 0) > 0;
|
|
1013
|
+
const gaps = grouped ? rankGaps(graph, rowsAt, axes, rankGap) : depths.slice(1).map(() => rankGap);
|
|
844
1014
|
const bandStart = /* @__PURE__ */ new Map();
|
|
845
1015
|
let along = 0;
|
|
846
|
-
for (const at of
|
|
1016
|
+
for (const [i, at] of depths.entries()) {
|
|
847
1017
|
bandStart.set(at, along);
|
|
848
|
-
along += bands.get(at) +
|
|
1018
|
+
along += bands.get(at) + (gaps[i] ?? 0);
|
|
849
1019
|
}
|
|
850
1020
|
const slots = /* @__PURE__ */ new Map();
|
|
851
1021
|
const measured = /* @__PURE__ */ new Map();
|
|
@@ -881,9 +1051,15 @@ var tree = (graph, opts = {}) => {
|
|
|
881
1051
|
place(root, cursor);
|
|
882
1052
|
cursor += measure(root) + nodeGap;
|
|
883
1053
|
}
|
|
1054
|
+
if (grouped) {
|
|
1055
|
+
const preferred = new Map([...slots].map(([id, slot]) => [id, slot.cross]));
|
|
1056
|
+
const ordered = rowsAt.map((row) => [...row].sort((a, b) => preferred.get(a.id) - preferred.get(b.id)));
|
|
1057
|
+
const crossAt = packClusters(graph, ordered, preferred, axes, nodeGap);
|
|
1058
|
+
for (const [id, slot] of slots) slots.set(id, { ...slot, cross: crossAt.get(id) ?? slot.cross });
|
|
1059
|
+
}
|
|
884
1060
|
return settle(graph, slots, opts);
|
|
885
1061
|
};
|
|
886
1062
|
|
|
887
|
-
export { COMPASS, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, extent, force, forceRelaxation, forestOf, graphOrder, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, pinnedSet, portOf, portsOf, portsOnOutline, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree };
|
|
888
|
-
//# sourceMappingURL=chunk-
|
|
889
|
-
//# sourceMappingURL=chunk-
|
|
1063
|
+
export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, center, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, extent, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, portOf, portsOf, portsOnOutline, rankGaps, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree };
|
|
1064
|
+
//# sourceMappingURL=chunk-ERT3XWBS.js.map
|
|
1065
|
+
//# sourceMappingURL=chunk-ERT3XWBS.js.map
|