@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.
- package/README.md +121 -44
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
- package/dist/algorithms-D1cgly0g.d.mts +452 -0
- package/dist/algorithms-DBpH74hR.mjs +3309 -0
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- package/dist/format-support.mjs +38 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +4 -4
- package/dist/formats/cytoscape/index.mjs +10 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +10 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +22 -6
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +63 -24
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +43 -16
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +28 -15
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +96 -23
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +12 -5
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +4 -4
- package/dist/formats/tgf/index.mjs +4 -4
- package/dist/formats/xyflow/index.d.mts +12 -6
- package/dist/formats/xyflow/index.mjs +42 -10
- package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +290 -307
- package/dist/layout/cytoscape.d.mts +66 -0
- package/dist/layout/cytoscape.mjs +114 -0
- package/dist/layout/d3-force.d.mts +52 -0
- package/dist/layout/d3-force.mjs +127 -0
- package/dist/layout/d3-hierarchy.d.mts +39 -0
- package/dist/layout/d3-hierarchy.mjs +135 -0
- package/dist/layout/dagre.d.mts +32 -0
- package/dist/layout/dagre.mjs +99 -0
- package/dist/layout/elk.d.mts +47 -0
- package/dist/layout/elk.mjs +73 -0
- package/dist/layout/forceatlas2.d.mts +48 -0
- package/dist/layout/forceatlas2.mjs +100 -0
- package/dist/layout/graphviz.d.mts +50 -0
- package/dist/layout/graphviz.mjs +179 -0
- package/dist/layout/index.d.mts +185 -0
- package/dist/layout/index.mjs +181 -0
- package/dist/layout/webcola.d.mts +40 -0
- package/dist/layout/webcola.mjs +104 -0
- package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
- package/dist/queries-BMM0XAv_.mjs +986 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +32 -84
- package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
- package/dist/validate-BsfSOv0S.mjs +190 -0
- package/package.json +59 -7
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- package/dist/algorithms-Ba7o7niK.mjs +0 -2394
- package/dist/algorithms-fTqmvhzP.d.mts +0 -178
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /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 };
|