@statelyai/graph 1.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 (77) hide show
  1. package/README.md +121 -44
  2. package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
  3. package/dist/algorithms-D1cgly0g.d.mts +452 -0
  4. package/dist/algorithms-DBpH74hR.mjs +3309 -0
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/config-Dt5u1gSf.mjs +793 -0
  8. package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
  9. package/dist/format-support.mjs +38 -11
  10. package/dist/formats/adjacency-list/index.d.mts +1 -1
  11. package/dist/formats/adjacency-list/index.mjs +1 -1
  12. package/dist/formats/converter/index.d.mts +1 -1
  13. package/dist/formats/converter/index.mjs +1 -1
  14. package/dist/formats/cytoscape/index.d.mts +4 -4
  15. package/dist/formats/cytoscape/index.mjs +10 -4
  16. package/dist/formats/d2/index.d.mts +1 -1
  17. package/dist/formats/d2/index.mjs +26 -12
  18. package/dist/formats/d3/index.d.mts +4 -4
  19. package/dist/formats/d3/index.mjs +10 -4
  20. package/dist/formats/dot/index.d.mts +1 -1
  21. package/dist/formats/dot/index.mjs +22 -6
  22. package/dist/formats/edge-list/index.d.mts +1 -1
  23. package/dist/formats/edge-list/index.mjs +1 -1
  24. package/dist/formats/elk/index.d.mts +1 -1
  25. package/dist/formats/elk/index.mjs +63 -24
  26. package/dist/formats/gexf/index.d.mts +1 -1
  27. package/dist/formats/gexf/index.mjs +43 -16
  28. package/dist/formats/gml/index.d.mts +4 -4
  29. package/dist/formats/gml/index.mjs +28 -15
  30. package/dist/formats/graphml/index.d.mts +1 -1
  31. package/dist/formats/graphml/index.mjs +96 -23
  32. package/dist/formats/jgf/index.d.mts +4 -4
  33. package/dist/formats/jgf/index.mjs +12 -5
  34. package/dist/formats/mermaid/index.d.mts +1 -1
  35. package/dist/formats/mermaid/index.mjs +49 -12
  36. package/dist/formats/tgf/index.d.mts +4 -4
  37. package/dist/formats/tgf/index.mjs +4 -4
  38. package/dist/formats/xyflow/index.d.mts +12 -6
  39. package/dist/formats/xyflow/index.mjs +42 -10
  40. package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
  41. package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
  42. package/dist/index.d.mts +6 -6
  43. package/dist/index.mjs +290 -307
  44. package/dist/layout/cytoscape.d.mts +66 -0
  45. package/dist/layout/cytoscape.mjs +114 -0
  46. package/dist/layout/d3-force.d.mts +52 -0
  47. package/dist/layout/d3-force.mjs +127 -0
  48. package/dist/layout/d3-hierarchy.d.mts +39 -0
  49. package/dist/layout/d3-hierarchy.mjs +135 -0
  50. package/dist/layout/dagre.d.mts +32 -0
  51. package/dist/layout/dagre.mjs +99 -0
  52. package/dist/layout/elk.d.mts +47 -0
  53. package/dist/layout/elk.mjs +73 -0
  54. package/dist/layout/forceatlas2.d.mts +48 -0
  55. package/dist/layout/forceatlas2.mjs +100 -0
  56. package/dist/layout/graphviz.d.mts +50 -0
  57. package/dist/layout/graphviz.mjs +179 -0
  58. package/dist/layout/index.d.mts +185 -0
  59. package/dist/layout/index.mjs +181 -0
  60. package/dist/layout/webcola.d.mts +40 -0
  61. package/dist/layout/webcola.mjs +104 -0
  62. package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
  63. package/dist/queries-BMM0XAv_.mjs +986 -0
  64. package/dist/queries.d.mts +1 -1
  65. package/dist/queries.mjs +1 -768
  66. package/dist/schemas.d.mts +19 -1
  67. package/dist/schemas.mjs +32 -84
  68. package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
  69. package/dist/validate-BsfSOv0S.mjs +190 -0
  70. package/package.json +59 -7
  71. package/schemas/edge.schema.json +27 -0
  72. package/schemas/graph.schema.json +27 -0
  73. package/dist/algorithms-Ba7o7niK.mjs +0 -2394
  74. package/dist/algorithms-fTqmvhzP.d.mts +0 -178
  75. package/dist/indexing-DR8M1vBy.mjs +0 -137
  76. /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
  77. /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
@@ -0,0 +1,66 @@
1
+ import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
2
+ import { LayoutOptions } from "./index.mjs";
3
+ import cytoscape from "cytoscape";
4
+
5
+ //#region src/layout/cytoscape.d.ts
6
+
7
+ /**
8
+ * Minimal interface an injected cytoscape factory must satisfy — the
9
+ * `cytoscape()` function itself. Inject your own when you've registered
10
+ * layout extensions via `cytoscape.use(...)` (cola, fcose, dagre, …).
11
+ */
12
+ type CytoscapeLike = (options: cytoscape.CytoscapeOptions) => cytoscape.Core;
13
+ interface CytoscapeLayoutOptions extends LayoutOptions {
14
+ /**
15
+ * Cytoscape layout name. Built-ins: `'grid'`, `'circle'`, `'concentric'`,
16
+ * `'breadthfirst'`, `'cose'` (default), `'random'`. Extension layouts work
17
+ * when registered on an injected {@link CytoscapeLayoutOptions.cy} factory.
18
+ */
19
+ name?: string;
20
+ /**
21
+ * Raw cytoscape layout options, merged into the layout call last (override
22
+ * everything, including `name`). Engine-specific tuning — spacing knobs,
23
+ * `boundingBox`, `roots`, iteration counts — goes here; the options vary
24
+ * too much between cytoscape layouts to map `LayoutOptions.spacing`
25
+ * generically. See https://js.cytoscape.org/#layouts
26
+ */
27
+ layoutOptions?: Record<string, unknown>;
28
+ /**
29
+ * Injected cytoscape factory — e.g. one with extensions registered via
30
+ * `cytoscape.use(...)`. Defaults to the imported `cytoscape`.
31
+ */
32
+ cy?: CytoscapeLike;
33
+ }
34
+ /**
35
+ * Lay out a graph with Cytoscape.js (`cytoscape`, an optional peer
36
+ * dependency) — one call unlocks its whole layout ecosystem. Pure: returns a
37
+ * new {@link VisualGraph} with node positions and sizes. Cytoscape layouts
38
+ * position nodes only; edges keep their fields but gain no route `points`.
39
+ * Compound graphs are supported via cytoscape `parent`; parent nodes get
40
+ * cytoscape's computed compound dimensions. All coordinates are absolute
41
+ * (cytoscape does not produce parent-relative positions).
42
+ *
43
+ * Runs headless with `styleEnabled: true` so resolved node sizes
44
+ * ({@link getNodeSize}) participate in overlap avoidance and spacing.
45
+ *
46
+ * Option mapping is deliberately minimal: `measure` resolves sizes,
47
+ * `isFixed` nodes with an existing `x`/`y` are locked in place (cytoscape
48
+ * layouts skip locked nodes), and everything else — including `direction`
49
+ * and `spacing` — is engine-specific and belongs in
50
+ * {@link CytoscapeLayoutOptions.layoutOptions}. `seed` is ignored: the
51
+ * discrete layouts (grid, circle, concentric, breadthfirst) are
52
+ * deterministic, and cose is not seedable.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { getCytoscapeLayout } from '@statelyai/graph/layout/cytoscape';
57
+ *
58
+ * const laidOut = await getCytoscapeLayout(graph, {
59
+ * name: 'breadthfirst',
60
+ * layoutOptions: { roots: ['start'] },
61
+ * });
62
+ * ```
63
+ */
64
+ declare function getCytoscapeLayout(graph: Graph | VisualGraph, options?: CytoscapeLayoutOptions): Promise<VisualGraph>;
65
+ //#endregion
66
+ export { CytoscapeLayoutOptions, CytoscapeLike, getCytoscapeLayout };
@@ -0,0 +1,114 @@
1
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
2
+ import { getNodeSize } from "./index.mjs";
3
+ import cytoscape from "cytoscape";
4
+
5
+ //#region src/layout/cytoscape.ts
6
+ /**
7
+ * Lay out a graph with Cytoscape.js (`cytoscape`, an optional peer
8
+ * dependency) — one call unlocks its whole layout ecosystem. Pure: returns a
9
+ * new {@link VisualGraph} with node positions and sizes. Cytoscape layouts
10
+ * position nodes only; edges keep their fields but gain no route `points`.
11
+ * Compound graphs are supported via cytoscape `parent`; parent nodes get
12
+ * cytoscape's computed compound dimensions. All coordinates are absolute
13
+ * (cytoscape does not produce parent-relative positions).
14
+ *
15
+ * Runs headless with `styleEnabled: true` so resolved node sizes
16
+ * ({@link getNodeSize}) participate in overlap avoidance and spacing.
17
+ *
18
+ * Option mapping is deliberately minimal: `measure` resolves sizes,
19
+ * `isFixed` nodes with an existing `x`/`y` are locked in place (cytoscape
20
+ * layouts skip locked nodes), and everything else — including `direction`
21
+ * and `spacing` — is engine-specific and belongs in
22
+ * {@link CytoscapeLayoutOptions.layoutOptions}. `seed` is ignored: the
23
+ * discrete layouts (grid, circle, concentric, breadthfirst) are
24
+ * deterministic, and cose is not seedable.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { getCytoscapeLayout } from '@statelyai/graph/layout/cytoscape';
29
+ *
30
+ * const laidOut = await getCytoscapeLayout(graph, {
31
+ * name: 'breadthfirst',
32
+ * layoutOptions: { roots: ['start'] },
33
+ * });
34
+ * ```
35
+ */
36
+ async function getCytoscapeLayout(graph, options) {
37
+ const elements = [];
38
+ for (const node of graph.nodes) {
39
+ const size = getNodeSize(node, options);
40
+ const hasPosition = node.x !== void 0 && node.y !== void 0;
41
+ elements.push({
42
+ group: "nodes",
43
+ data: {
44
+ id: node.id,
45
+ ...node.parentId != null && { parent: node.parentId },
46
+ width: size.width,
47
+ height: size.height
48
+ },
49
+ ...hasPosition && { position: {
50
+ x: node.x + size.width / 2,
51
+ y: node.y + size.height / 2
52
+ } },
53
+ ...hasPosition && options?.isFixed?.(node) && { locked: true }
54
+ });
55
+ }
56
+ for (const edge of graph.edges) elements.push({
57
+ group: "edges",
58
+ data: {
59
+ id: edge.id,
60
+ source: edge.sourceId,
61
+ target: edge.targetId
62
+ }
63
+ });
64
+ const cy = (options?.cy ?? cytoscape)({
65
+ headless: true,
66
+ styleEnabled: true,
67
+ style: [{
68
+ selector: "node",
69
+ style: {
70
+ width: "data(width)",
71
+ height: "data(height)",
72
+ shape: "rectangle"
73
+ }
74
+ }],
75
+ elements
76
+ });
77
+ try {
78
+ const layout = cy.layout({
79
+ name: options?.name ?? "cose",
80
+ animate: false,
81
+ ...options?.layoutOptions
82
+ });
83
+ const stopped = layout.promiseOn("layoutstop");
84
+ layout.run();
85
+ await stopped;
86
+ return createVisualGraph({
87
+ id: graph.id,
88
+ mode: graph.mode,
89
+ initialNodeId: graph.initialNodeId ?? void 0,
90
+ direction: options?.direction ?? graph.direction,
91
+ data: graph.data,
92
+ ...graph.style !== void 0 && { style: graph.style },
93
+ nodes: graph.nodes.map((node) => {
94
+ const ele = cy.getElementById(node.id);
95
+ const width = ele.width();
96
+ const height = ele.height();
97
+ const position = ele.position();
98
+ return {
99
+ ...toNodeConfig(node),
100
+ width,
101
+ height,
102
+ x: position.x - width / 2,
103
+ y: position.y - height / 2
104
+ };
105
+ }),
106
+ edges: graph.edges.map((edge) => toEdgeConfig(edge))
107
+ });
108
+ } finally {
109
+ cy.destroy();
110
+ }
111
+ }
112
+
113
+ //#endregion
114
+ export { getCytoscapeLayout };
@@ -0,0 +1,52 @@
1
+ import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
2
+ import { LayoutFrame, LayoutOptions } from "./index.mjs";
3
+
4
+ //#region src/layout/d3-force.d.ts
5
+ interface ForceLayoutOptions extends LayoutOptions {
6
+ /** Target distance between linked nodes. Default: 80. */
7
+ linkDistance?: number;
8
+ /** Many-body charge strength (negative repels). Default: -300. */
9
+ chargeStrength?: number;
10
+ /** Number of simulation ticks. Default: 300 (d3's natural cooling span). */
11
+ iterations?: number;
12
+ }
13
+ /**
14
+ * Iterative force-directed layout via d3-force (optional peer dependency).
15
+ * Each `next()` advances one simulation tick and yields a {@link LayoutFrame}
16
+ * (top-left node positions by id + remaining `alpha`); the caller owns pacing
17
+ * (e.g. one tick per animation frame via {@link applyLayoutFrame}) and
18
+ * cancellation (stop iterating). The generator's return value is the settled
19
+ * {@link VisualGraph}.
20
+ *
21
+ * - Deterministic: `options.seed` drives d3's `randomSource`; the same seed
22
+ * always produces the same layout.
23
+ * - `options.isFixed` pins nodes at their current position (d3 `fx`/`fy`).
24
+ * - Hierarchy is ignored (force layouts are flat); per-edge `mode` is
25
+ * irrelevant (forces are symmetric).
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { genForceLayout } from '@statelyai/graph/layout/d3-force';
30
+ * import { applyLayoutFrame } from '@statelyai/graph/layout';
31
+ *
32
+ * for (const frame of genForceLayout(graph, { seed: 42 })) {
33
+ * applyLayoutFrame(graph, frame);
34
+ * render(graph);
35
+ * }
36
+ * ```
37
+ */
38
+ declare function genForceLayout(graph: Graph | VisualGraph, options?: ForceLayoutOptions): Generator<LayoutFrame, VisualGraph>;
39
+ /**
40
+ * Run {@link genForceLayout} to completion and return the settled
41
+ * {@link VisualGraph}. Convenience for non-animated use.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { getForceLayout } from '@statelyai/graph/layout/d3-force';
46
+ *
47
+ * const laidOut = getForceLayout(graph, { seed: 42 });
48
+ * ```
49
+ */
50
+ declare function getForceLayout(graph: Graph | VisualGraph, options?: ForceLayoutOptions): VisualGraph;
51
+ //#endregion
52
+ export { ForceLayoutOptions, genForceLayout, getForceLayout };
@@ -0,0 +1,127 @@
1
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
2
+ import { getNodeSize } from "./index.mjs";
3
+ import { forceCenter, forceLink, forceManyBody, forceSimulation } from "d3-force";
4
+
5
+ //#region src/layout/d3-force.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
+ * Iterative force-directed layout via d3-force (optional peer dependency).
18
+ * Each `next()` advances one simulation tick and yields a {@link LayoutFrame}
19
+ * (top-left node positions by id + remaining `alpha`); the caller owns pacing
20
+ * (e.g. one tick per animation frame via {@link applyLayoutFrame}) and
21
+ * cancellation (stop iterating). The generator's return value is the settled
22
+ * {@link VisualGraph}.
23
+ *
24
+ * - Deterministic: `options.seed` drives d3's `randomSource`; the same seed
25
+ * always produces the same layout.
26
+ * - `options.isFixed` pins nodes at their current position (d3 `fx`/`fy`).
27
+ * - Hierarchy is ignored (force layouts are flat); per-edge `mode` is
28
+ * irrelevant (forces are symmetric).
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { genForceLayout } from '@statelyai/graph/layout/d3-force';
33
+ * import { applyLayoutFrame } from '@statelyai/graph/layout';
34
+ *
35
+ * for (const frame of genForceLayout(graph, { seed: 42 })) {
36
+ * applyLayoutFrame(graph, frame);
37
+ * render(graph);
38
+ * }
39
+ * ```
40
+ */
41
+ function* genForceLayout(graph, options) {
42
+ const rng = mulberry32(options?.seed ?? 1);
43
+ const sizes = /* @__PURE__ */ new Map();
44
+ const scatterRadius = Math.max(80, 30 * Math.sqrt(graph.nodes.length));
45
+ const simNodes = graph.nodes.map((node) => {
46
+ const size = getNodeSize(node, options);
47
+ sizes.set(node.id, size);
48
+ const simNode = {
49
+ id: node.id,
50
+ ...size
51
+ };
52
+ if (node.x !== void 0 && node.y !== void 0) {
53
+ simNode.x = node.x + size.width / 2;
54
+ simNode.y = node.y + size.height / 2;
55
+ if (options?.isFixed?.(node)) {
56
+ simNode.fx = simNode.x;
57
+ simNode.fy = simNode.y;
58
+ }
59
+ } else {
60
+ const angle = rng() * 2 * Math.PI;
61
+ const radius = scatterRadius * Math.sqrt(rng());
62
+ simNode.x = Math.cos(angle) * radius;
63
+ simNode.y = Math.sin(angle) * radius;
64
+ }
65
+ return simNode;
66
+ });
67
+ const simLinks = graph.edges.filter((edge) => sizes.has(edge.sourceId) && sizes.has(edge.targetId) && edge.sourceId !== edge.targetId).map((edge) => ({
68
+ source: edge.sourceId,
69
+ target: edge.targetId
70
+ }));
71
+ const simulation = forceSimulation(simNodes).randomSource(mulberry32(options?.seed ?? 1)).force("link", forceLink(simLinks).id((d) => d.id).distance(options?.linkDistance ?? 80)).force("charge", forceManyBody().strength(options?.chargeStrength ?? -300)).force("center", forceCenter(0, 0)).stop();
72
+ const iterations = options?.iterations ?? 300;
73
+ for (let i = 0; i < iterations; i++) {
74
+ simulation.tick();
75
+ const positions = {};
76
+ for (const simNode of simNodes) positions[simNode.id] = {
77
+ x: (simNode.x ?? 0) - simNode.width / 2,
78
+ y: (simNode.y ?? 0) - simNode.height / 2
79
+ };
80
+ yield {
81
+ positions,
82
+ alpha: simulation.alpha()
83
+ };
84
+ if (simulation.alpha() < simulation.alphaMin()) break;
85
+ }
86
+ const byId = new Map(simNodes.map((simNode) => [simNode.id, simNode]));
87
+ return createVisualGraph({
88
+ id: graph.id,
89
+ mode: graph.mode,
90
+ initialNodeId: graph.initialNodeId ?? void 0,
91
+ direction: graph.direction,
92
+ data: graph.data,
93
+ ...graph.style !== void 0 && { style: graph.style },
94
+ nodes: graph.nodes.map((node) => {
95
+ const size = sizes.get(node.id);
96
+ const simNode = byId.get(node.id);
97
+ return {
98
+ ...toNodeConfig(node),
99
+ ...size,
100
+ x: (simNode.x ?? 0) - size.width / 2,
101
+ y: (simNode.y ?? 0) - size.height / 2
102
+ };
103
+ }),
104
+ edges: graph.edges.map((edge) => toEdgeConfig(edge))
105
+ });
106
+ }
107
+ /**
108
+ * Run {@link genForceLayout} to completion and return the settled
109
+ * {@link VisualGraph}. Convenience for non-animated use.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * import { getForceLayout } from '@statelyai/graph/layout/d3-force';
114
+ *
115
+ * const laidOut = getForceLayout(graph, { seed: 42 });
116
+ * ```
117
+ */
118
+ function getForceLayout(graph, options) {
119
+ const generator = genForceLayout(graph, options);
120
+ for (;;) {
121
+ const step = generator.next();
122
+ if (step.done) return step.value;
123
+ }
124
+ }
125
+
126
+ //#endregion
127
+ export { genForceLayout, getForceLayout };
@@ -0,0 +1,39 @@
1
+ import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
2
+ import { LayoutOptions } from "./index.mjs";
3
+
4
+ //#region src/layout/d3-hierarchy.d.ts
5
+ interface TidyTreeLayoutOptions extends LayoutOptions {
6
+ /**
7
+ * Node to use as the tree root. Defaults to `graph.initialNodeId`, falling
8
+ * back to the graph's zero-in-degree nodes (multiple → laid out as a
9
+ * forest).
10
+ */
11
+ rootId?: string;
12
+ }
13
+ /**
14
+ * Lay out a graph as a tidy tree (Reingold–Tilford) with `d3-hierarchy`
15
+ * (an optional peer dependency). Pure and synchronous: returns a new
16
+ * {@link VisualGraph} with node positions/sizes.
17
+ *
18
+ * Root selection: `options.rootId` → `graph.initialNodeId` → the graph's
19
+ * zero-in-degree nodes. Multiple zero-in-degree nodes are laid out as a
20
+ * forest (side by side under an invisible synthetic root that is dropped
21
+ * from the output). If no root can be found (every node has incoming edges,
22
+ * i.e. the graph is cyclic), this throws — pass `rootId` to break the tie.
23
+ *
24
+ * Graphs that aren't strict trees are handled via a *spanning tree*: BFS
25
+ * from the root over effective edge direction picks each node's tree parent
26
+ * (first edge to reach it); only that spanning tree drives positions. All
27
+ * edges — including the extra cross/forward/back edges — are preserved
28
+ * untouched in the output. Tidy tree does not route edges (no `points`).
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { getTidyTreeLayout } from '@statelyai/graph/layout/d3-hierarchy';
33
+ *
34
+ * const laidOut = getTidyTreeLayout(graph, { direction: 'right' });
35
+ * ```
36
+ */
37
+ declare function getTidyTreeLayout(graph: Graph | VisualGraph, options?: TidyTreeLayoutOptions): VisualGraph;
38
+ //#endregion
39
+ export { TidyTreeLayoutOptions, getTidyTreeLayout };
@@ -0,0 +1,135 @@
1
+ import { T as getSuccessors, w as getSources } from "../queries-BMM0XAv_.mjs";
2
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig, y as hasNode } from "../config-Dt5u1gSf.mjs";
3
+ import { getNodeSize } from "./index.mjs";
4
+ import { hierarchy, tree } from "d3-hierarchy";
5
+
6
+ //#region src/layout/d3-hierarchy.ts
7
+ const DEFAULT_NODE_SPACING = 50;
8
+ const DEFAULT_LAYER_SPACING = 50;
9
+ /**
10
+ * Lay out a graph as a tidy tree (Reingold–Tilford) with `d3-hierarchy`
11
+ * (an optional peer dependency). Pure and synchronous: returns a new
12
+ * {@link VisualGraph} with node positions/sizes.
13
+ *
14
+ * Root selection: `options.rootId` → `graph.initialNodeId` → the graph's
15
+ * zero-in-degree nodes. Multiple zero-in-degree nodes are laid out as a
16
+ * forest (side by side under an invisible synthetic root that is dropped
17
+ * from the output). If no root can be found (every node has incoming edges,
18
+ * i.e. the graph is cyclic), this throws — pass `rootId` to break the tie.
19
+ *
20
+ * Graphs that aren't strict trees are handled via a *spanning tree*: BFS
21
+ * from the root over effective edge direction picks each node's tree parent
22
+ * (first edge to reach it); only that spanning tree drives positions. All
23
+ * edges — including the extra cross/forward/back edges — are preserved
24
+ * untouched in the output. Tidy tree does not route edges (no `points`).
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { getTidyTreeLayout } from '@statelyai/graph/layout/d3-hierarchy';
29
+ *
30
+ * const laidOut = getTidyTreeLayout(graph, { direction: 'right' });
31
+ * ```
32
+ */
33
+ function getTidyTreeLayout(graph, options) {
34
+ const direction = options?.direction ?? graph.direction ?? "down";
35
+ if (graph.nodes.length === 0) return createVisualGraph({
36
+ id: graph.id,
37
+ mode: graph.mode,
38
+ direction,
39
+ data: graph.data,
40
+ ...graph.style !== void 0 && { style: graph.style },
41
+ nodes: [],
42
+ edges: graph.edges.map(toEdgeConfig)
43
+ });
44
+ let rootIds;
45
+ if (options?.rootId !== void 0) {
46
+ if (!hasNode(graph, options.rootId)) throw new Error(`getTidyTreeLayout: rootId "${options.rootId}" does not exist in graph "${graph.id}". Pass the id of an existing node.`);
47
+ rootIds = [options.rootId];
48
+ } else if (graph.initialNodeId != null && hasNode(graph, graph.initialNodeId)) rootIds = [graph.initialNodeId];
49
+ else {
50
+ rootIds = getSources(graph).map((node) => node.id);
51
+ if (rootIds.length === 0) throw new Error(`getTidyTreeLayout: no tree root found in graph "${graph.id}" — all ${graph.nodes.length} nodes have incoming edges (the graph is cyclic). Pass options.rootId or set graph.initialNodeId to choose where the tree starts.`);
52
+ }
53
+ const visited = /* @__PURE__ */ new Set();
54
+ const buildSpanningTree = (rootId) => {
55
+ const root = {
56
+ id: rootId,
57
+ children: []
58
+ };
59
+ visited.add(rootId);
60
+ const queue = [root];
61
+ while (queue.length > 0) {
62
+ const datum = queue.shift();
63
+ for (const successor of getSuccessors(graph, datum.id)) {
64
+ if (visited.has(successor.id)) continue;
65
+ visited.add(successor.id);
66
+ const child = {
67
+ id: successor.id,
68
+ children: []
69
+ };
70
+ datum.children.push(child);
71
+ queue.push(child);
72
+ }
73
+ }
74
+ return root;
75
+ };
76
+ const trees = rootIds.map(buildSpanningTree);
77
+ for (const node of graph.nodes) if (!visited.has(node.id)) trees.push(buildSpanningTree(node.id));
78
+ const isHorizontal = direction === "left" || direction === "right";
79
+ const sizes = new Map(graph.nodes.map((node) => [node.id, getNodeSize(node, options)]));
80
+ let maxBreadth = 0;
81
+ let maxDepth = 0;
82
+ for (const size of sizes.values()) {
83
+ maxBreadth = Math.max(maxBreadth, isHorizontal ? size.height : size.width);
84
+ maxDepth = Math.max(maxDepth, isHorizontal ? size.width : size.height);
85
+ }
86
+ const breadthStep = maxBreadth + (options?.spacing?.node ?? DEFAULT_NODE_SPACING);
87
+ const depthStep = maxDepth + (options?.spacing?.layer ?? DEFAULT_LAYER_SPACING);
88
+ const isForest = trees.length > 1;
89
+ const rootDatum = isForest ? {
90
+ id: null,
91
+ children: trees
92
+ } : trees[0];
93
+ const laidOut = tree().nodeSize([breadthStep, depthStep])(hierarchy(rootDatum));
94
+ const centers = /* @__PURE__ */ new Map();
95
+ for (const point of laidOut.descendants()) {
96
+ if (point.data.id === null) continue;
97
+ const breadth = point.x;
98
+ const depth = point.y - (isForest ? depthStep : 0);
99
+ centers.set(point.data.id, direction === "down" ? {
100
+ x: breadth,
101
+ y: depth
102
+ } : direction === "up" ? {
103
+ x: breadth,
104
+ y: -depth
105
+ } : direction === "right" ? {
106
+ x: depth,
107
+ y: breadth
108
+ } : {
109
+ x: -depth,
110
+ y: breadth
111
+ });
112
+ }
113
+ return createVisualGraph({
114
+ id: graph.id,
115
+ mode: graph.mode,
116
+ initialNodeId: graph.initialNodeId ?? void 0,
117
+ direction,
118
+ data: graph.data,
119
+ ...graph.style !== void 0 && { style: graph.style },
120
+ nodes: graph.nodes.map((node) => {
121
+ const size = sizes.get(node.id);
122
+ const center = centers.get(node.id);
123
+ return {
124
+ ...toNodeConfig(node),
125
+ ...size,
126
+ x: center.x - size.width / 2,
127
+ y: center.y - size.height / 2
128
+ };
129
+ }),
130
+ edges: graph.edges.map(toEdgeConfig)
131
+ });
132
+ }
133
+
134
+ //#endregion
135
+ export { getTidyTreeLayout };
@@ -0,0 +1,32 @@
1
+ import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
2
+ import { LayoutOptions } from "./index.mjs";
3
+
4
+ //#region src/layout/dagre.d.ts
5
+ interface DagreLayoutOptions extends LayoutOptions {
6
+ /**
7
+ * Raw dagre graph options, spread onto `setGraph` last (override
8
+ * everything). See https://github.com/dagrejs/dagre/wiki#configuring-the-layout
9
+ */
10
+ graphOptions?: Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Lay out a graph with dagre (`@dagrejs/dagre`, an optional peer dependency).
14
+ * Pure and synchronous: returns a new {@link VisualGraph} with node
15
+ * positions/sizes, polyline edge `points`, and computed edge label rects
16
+ * (dagre's own `edge.x/y` label convention maps directly onto ours).
17
+ * Compound graphs are supported via dagre's `setParent`. All coordinates are
18
+ * absolute (dagre does not produce parent-relative positions).
19
+ *
20
+ * Per-edge `mode` is ignored by dagre (it layers everything by authored
21
+ * direction) — for mixed graphs prefer {@link getElkLayout}.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { getDagreLayout } from '@statelyai/graph/layout/dagre';
26
+ *
27
+ * const laidOut = getDagreLayout(graph, { direction: 'right' });
28
+ * ```
29
+ */
30
+ declare function getDagreLayout(graph: Graph | VisualGraph, options?: DagreLayoutOptions): VisualGraph;
31
+ //#endregion
32
+ export { DagreLayoutOptions, getDagreLayout };
@@ -0,0 +1,99 @@
1
+ import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
2
+ import { getNodeSize } from "./index.mjs";
3
+ import dagre from "@dagrejs/dagre";
4
+
5
+ //#region src/layout/dagre.ts
6
+ const DIRECTION_TO_RANKDIR = {
7
+ down: "TB",
8
+ up: "BT",
9
+ right: "LR",
10
+ left: "RL"
11
+ };
12
+ /**
13
+ * Lay out a graph with dagre (`@dagrejs/dagre`, an optional peer dependency).
14
+ * Pure and synchronous: returns a new {@link VisualGraph} with node
15
+ * positions/sizes, polyline edge `points`, and computed edge label rects
16
+ * (dagre's own `edge.x/y` label convention maps directly onto ours).
17
+ * Compound graphs are supported via dagre's `setParent`. All coordinates are
18
+ * absolute (dagre does not produce parent-relative positions).
19
+ *
20
+ * Per-edge `mode` is ignored by dagre (it layers everything by authored
21
+ * direction) — for mixed graphs prefer {@link getElkLayout}.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { getDagreLayout } from '@statelyai/graph/layout/dagre';
26
+ *
27
+ * const laidOut = getDagreLayout(graph, { direction: 'right' });
28
+ * ```
29
+ */
30
+ function getDagreLayout(graph, options) {
31
+ const g = new dagre.graphlib.Graph({
32
+ multigraph: true,
33
+ compound: true
34
+ });
35
+ g.setGraph({
36
+ rankdir: DIRECTION_TO_RANKDIR[options?.direction ?? graph.direction ?? "down"],
37
+ ...options?.spacing?.node !== void 0 && { nodesep: options.spacing.node },
38
+ ...options?.spacing?.layer !== void 0 && { ranksep: options.spacing.layer },
39
+ ...options?.graphOptions
40
+ });
41
+ g.setDefaultEdgeLabel(() => ({}));
42
+ const sizes = /* @__PURE__ */ new Map();
43
+ for (const node of graph.nodes) {
44
+ const size = getNodeSize(node, options);
45
+ sizes.set(node.id, size);
46
+ g.setNode(node.id, { ...size });
47
+ }
48
+ for (const node of graph.nodes) if (node.parentId != null) g.setParent(node.id, node.parentId);
49
+ for (const edge of graph.edges) {
50
+ const hasLabelBox = edge.label != null && edge.width !== void 0 && edge.height !== void 0 && edge.width > 0 && edge.height > 0;
51
+ g.setEdge(edge.sourceId, edge.targetId, hasLabelBox ? {
52
+ width: edge.width,
53
+ height: edge.height,
54
+ labelpos: "c"
55
+ } : {}, edge.id);
56
+ }
57
+ dagre.layout(g);
58
+ return createVisualGraph({
59
+ id: graph.id,
60
+ mode: graph.mode,
61
+ initialNodeId: graph.initialNodeId ?? void 0,
62
+ direction: options?.direction ?? graph.direction,
63
+ data: graph.data,
64
+ ...graph.style !== void 0 && { style: graph.style },
65
+ nodes: graph.nodes.map((node) => {
66
+ const size = sizes.get(node.id);
67
+ const positioned = g.node(node.id);
68
+ return {
69
+ ...toNodeConfig(node),
70
+ ...size,
71
+ x: positioned.x - size.width / 2,
72
+ y: positioned.y - size.height / 2
73
+ };
74
+ }),
75
+ edges: graph.edges.map((edge) => {
76
+ const laidOut = g.edge(edge.sourceId, edge.targetId, edge.id);
77
+ const config = toEdgeConfig(edge);
78
+ if (laidOut?.points !== void 0) {
79
+ config.points = laidOut.points.map((p) => ({
80
+ x: p.x,
81
+ y: p.y
82
+ }));
83
+ config.routing = "polyline";
84
+ }
85
+ if (laidOut?.x !== void 0 && laidOut?.y !== void 0) {
86
+ const labelWidth = laidOut.width ?? edge.width ?? 0;
87
+ const labelHeight = laidOut.height ?? edge.height ?? 0;
88
+ config.x = laidOut.x - labelWidth / 2;
89
+ config.y = laidOut.y - labelHeight / 2;
90
+ config.width = labelWidth;
91
+ config.height = labelHeight;
92
+ }
93
+ return config;
94
+ })
95
+ });
96
+ }
97
+
98
+ //#endregion
99
+ export { getDagreLayout };