@statelyai/graph 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +67 -19
  2. package/dist/{algorithms-CsGNehct.d.mts → algorithms-D1cgly0g.d.mts} +145 -6
  3. package/dist/{algorithms-DF1pSQGv.mjs → algorithms-DBpH74hR.mjs} +673 -891
  4. package/dist/algorithms.d.mts +2 -2
  5. package/dist/algorithms.mjs +2 -2
  6. package/dist/config-Dt5u1gSf.mjs +793 -0
  7. package/dist/{converter-DyCJJfTe.mjs → converter-DB6Rg6Vd.mjs} +2 -2
  8. package/dist/formats/adjacency-list/index.d.mts +1 -1
  9. package/dist/formats/adjacency-list/index.mjs +1 -1
  10. package/dist/formats/converter/index.d.mts +1 -1
  11. package/dist/formats/converter/index.mjs +1 -1
  12. package/dist/formats/cytoscape/index.d.mts +4 -4
  13. package/dist/formats/cytoscape/index.mjs +8 -4
  14. package/dist/formats/d2/index.d.mts +1 -1
  15. package/dist/formats/d2/index.mjs +1 -1
  16. package/dist/formats/d3/index.d.mts +4 -4
  17. package/dist/formats/d3/index.mjs +8 -4
  18. package/dist/formats/dot/index.d.mts +1 -1
  19. package/dist/formats/dot/index.mjs +1 -1
  20. package/dist/formats/edge-list/index.d.mts +1 -1
  21. package/dist/formats/edge-list/index.mjs +1 -1
  22. package/dist/formats/elk/index.d.mts +1 -1
  23. package/dist/formats/elk/index.mjs +43 -11
  24. package/dist/formats/gexf/index.d.mts +1 -1
  25. package/dist/formats/gexf/index.mjs +22 -2
  26. package/dist/formats/gml/index.d.mts +4 -4
  27. package/dist/formats/gml/index.mjs +8 -4
  28. package/dist/formats/graphml/index.d.mts +1 -1
  29. package/dist/formats/graphml/index.mjs +24 -2
  30. package/dist/formats/jgf/index.d.mts +4 -4
  31. package/dist/formats/jgf/index.mjs +8 -4
  32. package/dist/formats/mermaid/index.d.mts +1 -1
  33. package/dist/formats/mermaid/index.mjs +1 -1
  34. package/dist/formats/tgf/index.d.mts +4 -4
  35. package/dist/formats/tgf/index.mjs +4 -4
  36. package/dist/formats/xyflow/index.d.mts +12 -6
  37. package/dist/formats/xyflow/index.mjs +11 -6
  38. package/dist/{index-D51lJnt2.d.mts → index-BlbSWUvH.d.mts} +1 -1
  39. package/dist/{index-DWmo1mIp.d.mts → index-CNvqxPLJ.d.mts} +82 -14
  40. package/dist/index.d.mts +6 -6
  41. package/dist/index.mjs +152 -17
  42. package/dist/layout/cytoscape.d.mts +66 -0
  43. package/dist/layout/cytoscape.mjs +114 -0
  44. package/dist/layout/d3-force.d.mts +52 -0
  45. package/dist/layout/d3-force.mjs +127 -0
  46. package/dist/layout/d3-hierarchy.d.mts +39 -0
  47. package/dist/layout/d3-hierarchy.mjs +135 -0
  48. package/dist/layout/dagre.d.mts +32 -0
  49. package/dist/layout/dagre.mjs +99 -0
  50. package/dist/layout/elk.d.mts +47 -0
  51. package/dist/layout/elk.mjs +73 -0
  52. package/dist/layout/forceatlas2.d.mts +48 -0
  53. package/dist/layout/forceatlas2.mjs +100 -0
  54. package/dist/layout/graphviz.d.mts +50 -0
  55. package/dist/layout/graphviz.mjs +179 -0
  56. package/dist/layout/index.d.mts +185 -0
  57. package/dist/layout/index.mjs +181 -0
  58. package/dist/layout/webcola.d.mts +40 -0
  59. package/dist/layout/webcola.mjs +104 -0
  60. package/dist/{queries-BfXeTXRf.d.mts → queries-B6quF529.d.mts} +1 -1
  61. package/dist/{queries-KirMDR7e.mjs → queries-BMM0XAv_.mjs} +23 -17
  62. package/dist/queries.d.mts +1 -1
  63. package/dist/queries.mjs +1 -1
  64. package/dist/schemas.d.mts +19 -1
  65. package/dist/schemas.mjs +10 -1
  66. package/dist/{types-DNYdIU21.d.mts → types-BAEQTwK_.d.mts} +46 -3
  67. package/package.json +47 -5
  68. package/schemas/edge.schema.json +27 -0
  69. package/schemas/graph.schema.json +27 -0
  70. /package/dist/{adjacency-list-GeL1Cu-L.mjs → adjacency-list-DQ32Mmhx.mjs} +0 -0
  71. /package/dist/{edge-list-BcZ0h6zz.mjs → edge-list-CA9UTvn2.mjs} +0 -0
  72. /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
  73. /package/dist/{validate-TtH-x3JV.mjs → validate-BsfSOv0S.mjs} +0 -0
@@ -0,0 +1,179 @@
1
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
2
+ import { getNodeSize } from "./index.mjs";
3
+ import { Graphviz } from "@hpcc-js/wasm-graphviz";
4
+
5
+ //#region src/layout/graphviz.ts
6
+ const POINTS_PER_INCH = 72;
7
+ const DOT_KEYWORDS = new Set([
8
+ "node",
9
+ "edge",
10
+ "graph",
11
+ "digraph",
12
+ "subgraph",
13
+ "strict"
14
+ ]);
15
+ function escapeId(id) {
16
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id) && !DOT_KEYWORDS.has(id.toLowerCase())) return id;
17
+ return `"${id.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
18
+ }
19
+ function escapeLabel(label) {
20
+ return label.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
21
+ }
22
+ const DIRECTION_TO_RANKDIR = {
23
+ down: "TB",
24
+ up: "BT",
25
+ right: "LR",
26
+ left: "RL"
27
+ };
28
+ function parsePoint(pos) {
29
+ const [x, y] = pos.split(",").map(Number);
30
+ return {
31
+ x,
32
+ y
33
+ };
34
+ }
35
+ /**
36
+ * Parse a Graphviz spline `pos` string into route points (tail → head),
37
+ * including the arrow endpoints: `s,x,y` (tail arrow tip) is prepended and
38
+ * `e,x,y` (head arrow tip) is appended to the b-spline control points.
39
+ */
40
+ function parseSplinePos(pos) {
41
+ let start;
42
+ let end;
43
+ const controls = [];
44
+ for (const token of pos.trim().split(/\s+/)) if (token.startsWith("e,")) end = parsePoint(token.slice(2));
45
+ else if (token.startsWith("s,")) start = parsePoint(token.slice(2));
46
+ else controls.push(parsePoint(token));
47
+ const points = [...controls];
48
+ if (start) points.unshift(start);
49
+ if (end) points.push(end);
50
+ return points;
51
+ }
52
+ let graphvizPromise;
53
+ /**
54
+ * Lay out a graph with Graphviz (via `@hpcc-js/wasm-graphviz`, an optional
55
+ * peer dependency). Pure: returns a new {@link VisualGraph} with node
56
+ * positions/sizes, routed edge `points` (`routing: 'splines'` — Graphviz
57
+ * b-spline control points, tail → head, endpoints included), and computed
58
+ * edge label rects (edge `x`/`y`).
59
+ *
60
+ * Node sizes are resolved via {@link getNodeSize} and passed to Graphviz as
61
+ * fixed sizes, so layout never depends on Graphviz's own text measurement.
62
+ *
63
+ * Notes:
64
+ * - Compound graphs (`parentId`) are not supported — use `getElkLayout`, or
65
+ * `getFlattenedGraph()` the graph first. (Graphviz clusters: planned.)
66
+ * - In a directed graph, edges with `mode: 'undirected'` are laid out as
67
+ * directed but drawn without arrowheads (`dir=none`); in an undirected
68
+ * graph, per-edge `mode: 'directed'` overrides are ignored (DOT `graph`
69
+ * has no directed edge operator).
70
+ * - `options.seed` maps to the Graphviz `start` attribute (used by the
71
+ * randomized engines neato/fdp/sfdp; ignored by deterministic engines).
72
+ * - `options.constraints.layer` maps to `{ rank=same; … }` groups (`dot`
73
+ * engine only — the other engines have no rank concept).
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { getGraphvizLayout } from '@statelyai/graph/layout/graphviz';
78
+ *
79
+ * const laidOut = await getGraphvizLayout(graph, {
80
+ * engine: 'dot',
81
+ * measure: (node) => measureText(node.label),
82
+ * });
83
+ * ```
84
+ */
85
+ async function getGraphvizLayout(graph, options) {
86
+ const compoundNode = graph.nodes.find((node) => node.parentId != null);
87
+ if (compoundNode) throw new Error(`getGraphvizLayout: compound graphs are not supported by the Graphviz adapter yet (node "${compoundNode.id}" has parentId "${compoundNode.parentId}"). Use getElkLayout for hierarchical layout, or getFlattenedGraph() the graph first.`);
88
+ const engine = options?.engine ?? "dot";
89
+ const isDirected = graph.mode !== "undirected";
90
+ const direction = options?.direction ?? graph.direction;
91
+ const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
92
+ const lines = [];
93
+ lines.push(`${isDirected ? "digraph" : "graph"} ${escapeId(graph.id)} {`);
94
+ if (direction) lines.push(` rankdir=${DIRECTION_TO_RANKDIR[direction] ?? "TB"};`);
95
+ if (options?.spacing?.node !== void 0) lines.push(` nodesep=${options.spacing.node / POINTS_PER_INCH};`);
96
+ if (options?.spacing?.layer !== void 0) lines.push(` ranksep=${options.spacing.layer / POINTS_PER_INCH};`);
97
+ if (options?.seed !== void 0) lines.push(` start=${options.seed};`);
98
+ for (const [key, value] of Object.entries(options?.graphAttributes ?? {})) lines.push(` ${escapeId(key)}="${escapeLabel(value)}";`);
99
+ for (const node of graph.nodes) {
100
+ const { width, height } = sizes.get(node.id);
101
+ lines.push(` ${escapeId(node.id)} [width=${width / POINTS_PER_INCH}, height=${height / POINTS_PER_INCH}, fixedsize=true, shape=box];`);
102
+ }
103
+ const layerOf = options?.constraints?.layer;
104
+ if (engine === "dot" && layerOf) {
105
+ const layers = /* @__PURE__ */ new Map();
106
+ for (const node of graph.nodes) {
107
+ const layer = layerOf(node);
108
+ if (layer === void 0) continue;
109
+ let ids = layers.get(layer);
110
+ if (ids === void 0) layers.set(layer, ids = []);
111
+ ids.push(node.id);
112
+ }
113
+ for (const [, ids] of [...layers].sort((a, b) => a[0] - b[0])) lines.push(` { rank=same; ${ids.map(escapeId).join("; ")}; }`);
114
+ }
115
+ for (const edge of graph.edges) {
116
+ const attrs = [];
117
+ if (edge.label) attrs.push(`label="${escapeLabel(edge.label)}"`);
118
+ if (isDirected && edge.mode === "undirected") attrs.push("dir=none");
119
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
120
+ lines.push(` ${escapeId(edge.sourceId)} ${isDirected ? "->" : "--"} ${escapeId(edge.targetId)}${attrStr};`);
121
+ }
122
+ lines.push("}");
123
+ const dot = lines.join("\n");
124
+ const graphviz = await (graphvizPromise ??= Graphviz.load());
125
+ let outputJson;
126
+ try {
127
+ outputJson = graphviz.layout(dot, "json", engine);
128
+ } catch (e) {
129
+ throw new Error(`getGraphvizLayout: Graphviz (engine "${engine}") failed — ${e instanceof Error ? e.message : String(e)}`);
130
+ }
131
+ const output = JSON.parse(outputJson);
132
+ const yMax = output.bb ? Number(output.bb.split(",")[3]) : 0;
133
+ const flip = (p) => ({
134
+ x: p.x,
135
+ y: yMax - p.y
136
+ });
137
+ const objectsByName = new Map((output.objects ?? []).map((obj) => [obj.name, obj]));
138
+ const outputEdges = output.edges ?? [];
139
+ return createVisualGraph({
140
+ id: graph.id,
141
+ mode: graph.mode,
142
+ initialNodeId: graph.initialNodeId ?? void 0,
143
+ direction,
144
+ data: graph.data,
145
+ ...graph.style !== void 0 && { style: graph.style },
146
+ nodes: graph.nodes.map((node) => {
147
+ const config = toNodeConfig(node);
148
+ const obj = objectsByName.get(node.id);
149
+ const size = sizes.get(node.id);
150
+ const width = obj?.width !== void 0 ? Number(obj.width) * POINTS_PER_INCH : size.width;
151
+ const height = obj?.height !== void 0 ? Number(obj.height) * POINTS_PER_INCH : size.height;
152
+ if (obj?.pos !== void 0) {
153
+ const center = flip(parsePoint(obj.pos));
154
+ config.x = center.x - width / 2;
155
+ config.y = center.y - height / 2;
156
+ }
157
+ config.width = width;
158
+ config.height = height;
159
+ return config;
160
+ }),
161
+ edges: graph.edges.map((edge, i) => {
162
+ const config = toEdgeConfig(edge);
163
+ const out = outputEdges[i];
164
+ if (out?.pos !== void 0) {
165
+ config.points = parseSplinePos(out.pos).map(flip);
166
+ config.routing = "splines";
167
+ }
168
+ if (out?.lp !== void 0) {
169
+ const center = flip(parsePoint(out.lp));
170
+ config.x = center.x - (edge.width ?? 0) / 2;
171
+ config.y = center.y - (edge.height ?? 0) / 2;
172
+ }
173
+ return config;
174
+ })
175
+ });
176
+ }
177
+
178
+ //#endregion
179
+ export { getGraphvizLayout };
@@ -0,0 +1,185 @@
1
+ import { I as VisualGraph, d as EntityRect, f as Graph, k as Point, y as GraphNode } from "../types-BAEQTwK_.mjs";
2
+
3
+ //#region src/layout/index.d.ts
4
+
5
+ /**
6
+ * Common options understood by every layout adapter. Engine-specific options
7
+ * extend this per adapter.
8
+ */
9
+ interface LayoutOptions {
10
+ /**
11
+ * Layout direction. Defaults to `graph.direction ?? 'down'`.
12
+ */
13
+ direction?: 'up' | 'down' | 'left' | 'right';
14
+ /**
15
+ * Spacing hints (engines map these to their native options where the
16
+ * mapping is well-defined; engines without one ignore them — see each
17
+ * adapter's JSDoc).
18
+ */
19
+ spacing?: {
20
+ /** Space between sibling nodes. */
21
+ node?: number;
22
+ /** Space between layers/ranks (hierarchical engines). */
23
+ layer?: number;
24
+ };
25
+ /**
26
+ * Node size resolver. Text measurement belongs to the renderer, so layout
27
+ * adapters never guess: sizes come from this callback, falling back to the
28
+ * node's own `width`/`height`, falling back to {@link DEFAULT_NODE_SIZE}.
29
+ */
30
+ measure?: (node: GraphNode) => {
31
+ width: number;
32
+ height: number;
33
+ };
34
+ /**
35
+ * Pinned nodes keep their current `x`/`y` (for engines that support
36
+ * fixing, e.g. d3-force `fx`/`fy`).
37
+ */
38
+ isFixed?: (node: GraphNode) => boolean;
39
+ /** Seed for engines with randomness — same seed, same layout. */
40
+ seed?: number;
41
+ /**
42
+ * Portable layout constraints. **Advisory**, like port `direction`: engines
43
+ * that can express a constraint honor it, others ignore it. Per-adapter
44
+ * support is documented on each adapter.
45
+ */
46
+ constraints?: LayoutConstraints;
47
+ }
48
+ /**
49
+ * Portable constraints understood by (some) layout adapters.
50
+ *
51
+ * | Constraint | ELK | Graphviz (dot) | dagre | force engines |
52
+ * |------------|-----|----------------|-------|----------------|
53
+ * | `layer` | partitions | `rank=same` groups | ignored | ignored |
54
+ */
55
+ interface LayoutConstraints {
56
+ /**
57
+ * Assign nodes to ordered layers along the flow axis (`0`, `1`, `2`, …;
58
+ * `undefined` leaves the node unconstrained). Nodes with the same value
59
+ * land in the same layer; smaller values come earlier in the layout
60
+ * direction. ELK maps this to partitions
61
+ * (`elk.partitioning.partition`); the Graphviz `dot` engine maps it to
62
+ * `{ rank=same; … }` groups (same-layer grouping — ordering *between*
63
+ * constrained layers still follows the edges).
64
+ */
65
+ layer?: (node: GraphNode) => number | undefined;
66
+ }
67
+ /**
68
+ * A one-shot layout: pure function from graph to a positioned
69
+ * {@link VisualGraph}. Async when the engine is async (ELK, Graphviz WASM) —
70
+ * engine async-ness is not ours to hide.
71
+ */
72
+ type LayoutFn<O extends LayoutOptions = LayoutOptions> = (graph: Graph | VisualGraph, options?: O) => VisualGraph | Promise<VisualGraph>;
73
+ /**
74
+ * One frame of an iterative (physics) layout: node positions by id, plus the
75
+ * simulation's remaining energy. Positions are node *top-left* corners,
76
+ * consistent with `VisualNode.x/y`.
77
+ */
78
+ interface LayoutFrame {
79
+ positions: Record<string, Point>;
80
+ /** Remaining simulation energy, 1 → 0. */
81
+ alpha: number;
82
+ }
83
+ /**
84
+ * An iterative layout (force simulations): each `next()` advances one tick
85
+ * and yields a {@link LayoutFrame}; the caller owns pacing (e.g. one tick per
86
+ * animation frame) and cancellation (drop the generator). The generator's
87
+ * return value is the settled {@link VisualGraph}.
88
+ */
89
+ type IterativeLayoutFn<O extends LayoutOptions = LayoutOptions> = (graph: Graph | VisualGraph, options?: O) => Generator<LayoutFrame, VisualGraph>;
90
+ /** Fallback node size when neither `measure` nor node dimensions are set. */
91
+ declare const DEFAULT_NODE_SIZE: {
92
+ readonly width: 100;
93
+ readonly height: 50;
94
+ };
95
+ /**
96
+ * Resolve a node's layout size: `options.measure` → node `width`/`height` →
97
+ * {@link DEFAULT_NODE_SIZE}. Zero sizes count as unset (layout engines
98
+ * overlap zero-sized nodes).
99
+ */
100
+ declare function getNodeSize(node: GraphNode, options?: Pick<LayoutOptions, 'measure'>): {
101
+ width: number;
102
+ height: number;
103
+ };
104
+ /**
105
+ * **Mutable.** Write a {@link LayoutFrame}'s positions onto the graph's nodes
106
+ * in place. Positions are non-structural, so this is safe under the index
107
+ * contract (no `invalidateIndex` needed) and cheap enough for per-animation-
108
+ * frame use. Nodes absent from the frame are left untouched.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * for (const frame of genForceLayout(graph)) {
113
+ * applyLayoutFrame(graph, frame);
114
+ * render(graph);
115
+ * }
116
+ * ```
117
+ */
118
+ declare function applyLayoutFrame(graph: Graph, frame: LayoutFrame): void;
119
+ /**
120
+ * Bounding rect of all positioned nodes (and edge route points, when
121
+ * present). Returns a zero rect for graphs with no geometry.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const bounds = getLayoutBounds(laidOut);
126
+ * svg.setAttribute('viewBox', `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`);
127
+ * ```
128
+ */
129
+ declare function getLayoutBounds(graph: Graph | VisualGraph): EntityRect;
130
+ interface LayoutTransitionOptions {
131
+ /** Number of frames to yield. Default: 30. */
132
+ steps?: number;
133
+ /**
134
+ * Easing function mapping linear progress `t` (0 → 1] to eased progress.
135
+ * Default: smoothstep (`t² · (3 − 2t)`).
136
+ */
137
+ ease?: (t: number) => number;
138
+ }
139
+ /**
140
+ * Animate between two layouts of the same graph: yields interpolated
141
+ * {@link LayoutFrame}s from the node positions in `from` to those in `to`
142
+ * (drive them with {@link applyLayoutFrame}, e.g. one per animation frame),
143
+ * and returns `to`. This is what makes layouts swappable live — lay out with
144
+ * one engine, re-lay out with another, and tween between them.
145
+ *
146
+ * Nodes are matched by id; nodes without a position in `from` (or absent from
147
+ * it) start at their `to` position. Edge routes are not interpolated — frames
148
+ * carry node positions only; hide or re-route edges during the transition.
149
+ * `alpha` cools linearly 1 → 0 like the physics layouts.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * const next = await getElkLayout(graph);
154
+ * for (const frame of genLayoutTransition(graph, next)) {
155
+ * applyLayoutFrame(graph, frame);
156
+ * render(graph);
157
+ * }
158
+ * ```
159
+ */
160
+ declare function genLayoutTransition(from: Graph | VisualGraph, to: VisualGraph, options?: LayoutTransitionOptions): Generator<LayoutFrame, VisualGraph>;
161
+ /**
162
+ * **Mutable.** Shift the graph's geometry by `(dx, dy)` in place: node
163
+ * positions, edge route `points`, and edge label rects. Non-structural, so no
164
+ * index invalidation is needed.
165
+ *
166
+ * Hierarchy-aware: child nodes (`parentId` set) use parent-relative
167
+ * coordinates (the ELK/xyflow convention), so only top-level nodes are
168
+ * shifted — children move with their parents. Likewise, an edge's geometry is
169
+ * shifted only when its containing coordinate system is the root (the LCA of
170
+ * its endpoints is no node).
171
+ */
172
+ declare function translateGraph(graph: Graph, dx: number, dy: number): void;
173
+ /**
174
+ * **Mutable.** Translate the graph in place so its {@link getLayoutBounds}
175
+ * center coincides with `rect`'s center — e.g. center a fresh layout in the
176
+ * viewport. Graphs without geometry are left untouched.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * centerGraph(laidOut, { x: 0, y: 0, width: canvas.width, height: canvas.height });
181
+ * ```
182
+ */
183
+ declare function centerGraph(graph: Graph, rect: EntityRect): void;
184
+ //#endregion
185
+ export { DEFAULT_NODE_SIZE, IterativeLayoutFn, LayoutConstraints, LayoutFn, LayoutFrame, LayoutOptions, LayoutTransitionOptions, applyLayoutFrame, centerGraph, genLayoutTransition, getLayoutBounds, getNodeSize, translateGraph };
@@ -0,0 +1,181 @@
1
+ import { d as getLCA } from "../queries-BMM0XAv_.mjs";
2
+
3
+ //#region src/layout/index.ts
4
+ /** Fallback node size when neither `measure` nor node dimensions are set. */
5
+ const DEFAULT_NODE_SIZE = {
6
+ width: 100,
7
+ height: 50
8
+ };
9
+ /**
10
+ * Resolve a node's layout size: `options.measure` → node `width`/`height` →
11
+ * {@link DEFAULT_NODE_SIZE}. Zero sizes count as unset (layout engines
12
+ * overlap zero-sized nodes).
13
+ */
14
+ function getNodeSize(node, options) {
15
+ const measured = options?.measure?.(node);
16
+ if (measured) return measured;
17
+ const width = node.width !== void 0 && node.width > 0 ? node.width : 0;
18
+ const height = node.height !== void 0 && node.height > 0 ? node.height : 0;
19
+ return {
20
+ width: width || DEFAULT_NODE_SIZE.width,
21
+ height: height || DEFAULT_NODE_SIZE.height
22
+ };
23
+ }
24
+ /**
25
+ * **Mutable.** Write a {@link LayoutFrame}'s positions onto the graph's nodes
26
+ * in place. Positions are non-structural, so this is safe under the index
27
+ * contract (no `invalidateIndex` needed) and cheap enough for per-animation-
28
+ * frame use. Nodes absent from the frame are left untouched.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * for (const frame of genForceLayout(graph)) {
33
+ * applyLayoutFrame(graph, frame);
34
+ * render(graph);
35
+ * }
36
+ * ```
37
+ */
38
+ function applyLayoutFrame(graph, frame) {
39
+ for (const node of graph.nodes) {
40
+ const position = frame.positions[node.id];
41
+ if (position !== void 0) {
42
+ node.x = position.x;
43
+ node.y = position.y;
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Bounding rect of all positioned nodes (and edge route points, when
49
+ * present). Returns a zero rect for graphs with no geometry.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const bounds = getLayoutBounds(laidOut);
54
+ * svg.setAttribute('viewBox', `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`);
55
+ * ```
56
+ */
57
+ function getLayoutBounds(graph) {
58
+ let minX = Infinity;
59
+ let minY = Infinity;
60
+ let maxX = -Infinity;
61
+ let maxY = -Infinity;
62
+ for (const node of graph.nodes) {
63
+ if (node.x === void 0 || node.y === void 0) continue;
64
+ minX = Math.min(minX, node.x);
65
+ minY = Math.min(minY, node.y);
66
+ maxX = Math.max(maxX, node.x + (node.width ?? 0));
67
+ maxY = Math.max(maxY, node.y + (node.height ?? 0));
68
+ }
69
+ for (const edge of graph.edges) for (const point of edge.points ?? []) {
70
+ minX = Math.min(minX, point.x);
71
+ minY = Math.min(minY, point.y);
72
+ maxX = Math.max(maxX, point.x);
73
+ maxY = Math.max(maxY, point.y);
74
+ }
75
+ if (minX === Infinity) return {
76
+ x: 0,
77
+ y: 0,
78
+ width: 0,
79
+ height: 0
80
+ };
81
+ return {
82
+ x: minX,
83
+ y: minY,
84
+ width: maxX - minX,
85
+ height: maxY - minY
86
+ };
87
+ }
88
+ /**
89
+ * Animate between two layouts of the same graph: yields interpolated
90
+ * {@link LayoutFrame}s from the node positions in `from` to those in `to`
91
+ * (drive them with {@link applyLayoutFrame}, e.g. one per animation frame),
92
+ * and returns `to`. This is what makes layouts swappable live — lay out with
93
+ * one engine, re-lay out with another, and tween between them.
94
+ *
95
+ * Nodes are matched by id; nodes without a position in `from` (or absent from
96
+ * it) start at their `to` position. Edge routes are not interpolated — frames
97
+ * carry node positions only; hide or re-route edges during the transition.
98
+ * `alpha` cools linearly 1 → 0 like the physics layouts.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const next = await getElkLayout(graph);
103
+ * for (const frame of genLayoutTransition(graph, next)) {
104
+ * applyLayoutFrame(graph, frame);
105
+ * render(graph);
106
+ * }
107
+ * ```
108
+ */
109
+ function* genLayoutTransition(from, to, options) {
110
+ const steps = Math.max(1, Math.floor(options?.steps ?? 30));
111
+ const ease = options?.ease ?? ((t) => t * t * (3 - 2 * t));
112
+ const fromById = new Map(from.nodes.map((node) => [node.id, node]));
113
+ const tweens = to.nodes.map((node) => {
114
+ const fromNode = fromById.get(node.id);
115
+ const hasStart = fromNode?.x !== void 0 && fromNode.y !== void 0;
116
+ return {
117
+ id: node.id,
118
+ startX: hasStart ? fromNode.x : node.x,
119
+ startY: hasStart ? fromNode.y : node.y,
120
+ endX: node.x,
121
+ endY: node.y
122
+ };
123
+ });
124
+ for (let step = 1; step <= steps; step++) {
125
+ const t = ease(step / steps);
126
+ const positions = {};
127
+ for (const tween of tweens) positions[tween.id] = {
128
+ x: tween.startX + (tween.endX - tween.startX) * t,
129
+ y: tween.startY + (tween.endY - tween.startY) * t
130
+ };
131
+ yield {
132
+ positions,
133
+ alpha: 1 - step / steps
134
+ };
135
+ }
136
+ return to;
137
+ }
138
+ /**
139
+ * **Mutable.** Shift the graph's geometry by `(dx, dy)` in place: node
140
+ * positions, edge route `points`, and edge label rects. Non-structural, so no
141
+ * index invalidation is needed.
142
+ *
143
+ * Hierarchy-aware: child nodes (`parentId` set) use parent-relative
144
+ * coordinates (the ELK/xyflow convention), so only top-level nodes are
145
+ * shifted — children move with their parents. Likewise, an edge's geometry is
146
+ * shifted only when its containing coordinate system is the root (the LCA of
147
+ * its endpoints is no node).
148
+ */
149
+ function translateGraph(graph, dx, dy) {
150
+ for (const node of graph.nodes) {
151
+ if (node.parentId != null) continue;
152
+ if (node.x !== void 0) node.x += dx;
153
+ if (node.y !== void 0) node.y += dy;
154
+ }
155
+ for (const edge of graph.edges) {
156
+ if (getLCA(graph, edge.sourceId, edge.targetId) !== void 0) continue;
157
+ if (edge.x !== void 0) edge.x += dx;
158
+ if (edge.y !== void 0) edge.y += dy;
159
+ for (const point of edge.points ?? []) {
160
+ point.x += dx;
161
+ point.y += dy;
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * **Mutable.** Translate the graph in place so its {@link getLayoutBounds}
167
+ * center coincides with `rect`'s center — e.g. center a fresh layout in the
168
+ * viewport. Graphs without geometry are left untouched.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * centerGraph(laidOut, { x: 0, y: 0, width: canvas.width, height: canvas.height });
173
+ * ```
174
+ */
175
+ function centerGraph(graph, rect) {
176
+ const bounds = getLayoutBounds(graph);
177
+ translateGraph(graph, rect.x + rect.width / 2 - (bounds.x + bounds.width / 2), rect.y + rect.height / 2 - (bounds.y + bounds.height / 2));
178
+ }
179
+
180
+ //#endregion
181
+ export { DEFAULT_NODE_SIZE, applyLayoutFrame, centerGraph, genLayoutTransition, getLayoutBounds, getNodeSize, translateGraph };
@@ -0,0 +1,40 @@
1
+ import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
2
+ import { LayoutOptions } from "./index.mjs";
3
+
4
+ //#region src/layout/webcola.d.ts
5
+ interface ColaLayoutOptions extends LayoutOptions {
6
+ /** Target distance between linked nodes. Default: 80. */
7
+ linkDistance?: number;
8
+ }
9
+ /**
10
+ * Constraint-based layout via WebCola (optional peer dependency). Runs the
11
+ * solver to convergence synchronously (`keepRunning: false`) and returns a
12
+ * positioned {@link VisualGraph}.
13
+ *
14
+ * - Overlap avoidance is always on: node rects (from `options.measure` →
15
+ * node `width`/`height` → defaults) are kept disjoint.
16
+ * - Deterministic: WebCola itself has no `Math.random` in its 2D solver
17
+ * (its descent uses an internally seeded PRNG), so the seeded initial
18
+ * scatter (`options.seed`) fully pins the result — same seed, same layout.
19
+ * - `options.isFixed` pins positioned nodes at their current `x`/`y` (cola's
20
+ * `fixed` flag). During overlap projection cola holds fixed nodes with a
21
+ * large-but-finite weight, so pinning is within a small tolerance, not
22
+ * exact.
23
+ * - `options.direction ?? graph.direction` set → DAG-flow constraints via
24
+ * cola's `flowLayout`: edges are separated along the axis ('down'/'up' →
25
+ * `y`, 'left'/'right' → `x`) by at least `spacing.layer` (default 50).
26
+ * Note: cola only separates source-before-target along the axis — 'up' and
27
+ * 'left' flow the same way as 'down'/'right', not reversed.
28
+ * - Hierarchy is ignored (flat layout); self-loops are skipped by the solver
29
+ * but kept in the output graph.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { getColaLayout } from '@statelyai/graph/layout/webcola';
34
+ *
35
+ * const laidOut = getColaLayout(graph, { seed: 42, direction: 'down' });
36
+ * ```
37
+ */
38
+ declare function getColaLayout(graph: Graph | VisualGraph, options?: ColaLayoutOptions): VisualGraph;
39
+ //#endregion
40
+ export { ColaLayoutOptions, getColaLayout };
@@ -0,0 +1,104 @@
1
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
2
+ import { getNodeSize } from "./index.mjs";
3
+ import { Layout } from "webcola";
4
+
5
+ //#region src/layout/webcola.ts
6
+ /** mulberry32 — same seeded PRNG the rest of the library uses. */
7
+ function mulberry32(seed) {
8
+ let s = seed | 0;
9
+ return () => {
10
+ s = s + 1831565813 | 0;
11
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
12
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
13
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
14
+ };
15
+ }
16
+ /**
17
+ * Constraint-based layout via WebCola (optional peer dependency). Runs the
18
+ * solver to convergence synchronously (`keepRunning: false`) and returns a
19
+ * positioned {@link VisualGraph}.
20
+ *
21
+ * - Overlap avoidance is always on: node rects (from `options.measure` →
22
+ * node `width`/`height` → defaults) are kept disjoint.
23
+ * - Deterministic: WebCola itself has no `Math.random` in its 2D solver
24
+ * (its descent uses an internally seeded PRNG), so the seeded initial
25
+ * scatter (`options.seed`) fully pins the result — same seed, same layout.
26
+ * - `options.isFixed` pins positioned nodes at their current `x`/`y` (cola's
27
+ * `fixed` flag). During overlap projection cola holds fixed nodes with a
28
+ * large-but-finite weight, so pinning is within a small tolerance, not
29
+ * exact.
30
+ * - `options.direction ?? graph.direction` set → DAG-flow constraints via
31
+ * cola's `flowLayout`: edges are separated along the axis ('down'/'up' →
32
+ * `y`, 'left'/'right' → `x`) by at least `spacing.layer` (default 50).
33
+ * Note: cola only separates source-before-target along the axis — 'up' and
34
+ * 'left' flow the same way as 'down'/'right', not reversed.
35
+ * - Hierarchy is ignored (flat layout); self-loops are skipped by the solver
36
+ * but kept in the output graph.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { getColaLayout } from '@statelyai/graph/layout/webcola';
41
+ *
42
+ * const laidOut = getColaLayout(graph, { seed: 42, direction: 'down' });
43
+ * ```
44
+ */
45
+ function getColaLayout(graph, options) {
46
+ const rng = mulberry32(options?.seed ?? 1);
47
+ const sizes = /* @__PURE__ */ new Map();
48
+ const scatterRadius = Math.max(80, 30 * Math.sqrt(graph.nodes.length));
49
+ const colaNodes = graph.nodes.map((node) => {
50
+ const size = getNodeSize(node, options);
51
+ sizes.set(node.id, size);
52
+ const colaNode = {
53
+ id: node.id,
54
+ ...size
55
+ };
56
+ if (node.x !== void 0 && node.y !== void 0) {
57
+ colaNode.x = node.x + size.width / 2;
58
+ colaNode.y = node.y + size.height / 2;
59
+ if (options?.isFixed?.(node)) colaNode.fixed = 1;
60
+ } else {
61
+ const angle = rng() * 2 * Math.PI;
62
+ const radius = scatterRadius * Math.sqrt(rng());
63
+ colaNode.x = Math.cos(angle) * radius;
64
+ colaNode.y = Math.sin(angle) * radius;
65
+ }
66
+ return colaNode;
67
+ });
68
+ const indexById = new Map(graph.nodes.map((node, i) => [node.id, i]));
69
+ const colaLinks = graph.edges.filter((edge) => indexById.has(edge.sourceId) && indexById.has(edge.targetId) && edge.sourceId !== edge.targetId).map((edge) => ({
70
+ source: indexById.get(edge.sourceId),
71
+ target: indexById.get(edge.targetId)
72
+ }));
73
+ const layout = new Layout().nodes(colaNodes).links(colaLinks).linkDistance(options?.linkDistance ?? 80).avoidOverlaps(true);
74
+ const direction = options?.direction ?? graph.direction;
75
+ if (direction !== void 0) {
76
+ const axis = direction === "left" || direction === "right" ? "x" : "y";
77
+ layout.flowLayout(axis, options?.spacing?.layer ?? 50);
78
+ }
79
+ const centerGraph = !colaNodes.some((colaNode) => colaNode.fixed);
80
+ layout.start(30, 30, 60, 0, false, centerGraph);
81
+ const byId = new Map(colaNodes.map((colaNode) => [colaNode.id, colaNode]));
82
+ return createVisualGraph({
83
+ id: graph.id,
84
+ mode: graph.mode,
85
+ initialNodeId: graph.initialNodeId ?? void 0,
86
+ direction: graph.direction,
87
+ data: graph.data,
88
+ ...graph.style !== void 0 && { style: graph.style },
89
+ nodes: graph.nodes.map((node) => {
90
+ const size = sizes.get(node.id);
91
+ const colaNode = byId.get(node.id);
92
+ return {
93
+ ...toNodeConfig(node),
94
+ ...size,
95
+ x: (colaNode.x ?? 0) - size.width / 2,
96
+ y: (colaNode.y ?? 0) - size.height / 2
97
+ };
98
+ }),
99
+ edges: graph.edges.map((edge) => toEdgeConfig(edge))
100
+ });
101
+ }
102
+
103
+ //#endregion
104
+ export { getColaLayout };