@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.
- package/README.md +67 -19
- package/dist/{algorithms-CsGNehct.d.mts → algorithms-D1cgly0g.d.mts} +145 -6
- package/dist/{algorithms-DF1pSQGv.mjs → algorithms-DBpH74hR.mjs} +673 -891
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-DyCJJfTe.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- 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 +8 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +1 -1
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +8 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +1 -1
- 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 +43 -11
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +22 -2
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +8 -4
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +24 -2
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +8 -4
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +1 -1
- 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 +11 -6
- package/dist/{index-D51lJnt2.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-DWmo1mIp.d.mts → index-CNvqxPLJ.d.mts} +82 -14
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +152 -17
- 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-BfXeTXRf.d.mts → queries-B6quF529.d.mts} +1 -1
- package/dist/{queries-KirMDR7e.mjs → queries-BMM0XAv_.mjs} +23 -17
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -1
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +10 -1
- package/dist/{types-DNYdIU21.d.mts → types-BAEQTwK_.d.mts} +46 -3
- package/package.json +47 -5
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- /package/dist/{adjacency-list-GeL1Cu-L.mjs → adjacency-list-DQ32Mmhx.mjs} +0 -0
- /package/dist/{edge-list-BcZ0h6zz.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
- /package/dist/{validate-TtH-x3JV.mjs → validate-BsfSOv0S.mjs} +0 -0
|
@@ -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 };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
|
|
2
|
+
import { LayoutOptions } from "./index.mjs";
|
|
3
|
+
import { ElkNode } from "elkjs/lib/elk-api";
|
|
4
|
+
|
|
5
|
+
//#region src/layout/elk.d.ts
|
|
6
|
+
/** Minimal interface an injected ELK instance must satisfy. */
|
|
7
|
+
interface ElkLike {
|
|
8
|
+
layout(graph: ElkNode): Promise<ElkNode>;
|
|
9
|
+
}
|
|
10
|
+
interface ElkLayoutOptions extends LayoutOptions {
|
|
11
|
+
/**
|
|
12
|
+
* ELK algorithm. Common choices: `'layered'` (default), `'mrtree'`,
|
|
13
|
+
* `'force'`, `'stress'`, `'radial'`, `'rectpacking'`.
|
|
14
|
+
*/
|
|
15
|
+
algorithm?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Raw ELK layout options, spread onto the root last (override everything).
|
|
18
|
+
* See https://eclipse.dev/elk/reference/options.html
|
|
19
|
+
*/
|
|
20
|
+
layoutOptions?: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Injected ELK instance — e.g. one constructed with a web worker factory.
|
|
23
|
+
* Defaults to `new ELK()` (in-process).
|
|
24
|
+
*/
|
|
25
|
+
elk?: ElkLike;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Lay out a graph with ELK (via `elkjs`, an optional peer dependency).
|
|
29
|
+
* Pure: returns a new {@link VisualGraph} with node positions/sizes, routed
|
|
30
|
+
* edge `points`, and computed edge label rects (edge `x`/`y`/`width`/`height`).
|
|
31
|
+
* Hierarchy (`parentId`) and ports are first-class — ELK is the engine of
|
|
32
|
+
* choice for compound and port-aware graphs. Child node coordinates are
|
|
33
|
+
* relative to their parent (matching xyflow's convention).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { getElkLayout } from '@statelyai/graph/layout/elk';
|
|
38
|
+
*
|
|
39
|
+
* const laidOut = await getElkLayout(graph, {
|
|
40
|
+
* algorithm: 'layered',
|
|
41
|
+
* measure: (node) => measureText(node.label),
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function getElkLayout(graph: Graph | VisualGraph, options?: ElkLayoutOptions): Promise<VisualGraph>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { ElkLayoutOptions, ElkLike, getElkLayout };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
|
|
2
|
+
import { fromELK, toELK } from "../formats/elk/index.mjs";
|
|
3
|
+
import { getNodeSize } from "./index.mjs";
|
|
4
|
+
import ELK from "elkjs";
|
|
5
|
+
|
|
6
|
+
//#region src/layout/elk.ts
|
|
7
|
+
let defaultElk;
|
|
8
|
+
/**
|
|
9
|
+
* Lay out a graph with ELK (via `elkjs`, an optional peer dependency).
|
|
10
|
+
* Pure: returns a new {@link VisualGraph} with node positions/sizes, routed
|
|
11
|
+
* edge `points`, and computed edge label rects (edge `x`/`y`/`width`/`height`).
|
|
12
|
+
* Hierarchy (`parentId`) and ports are first-class — ELK is the engine of
|
|
13
|
+
* choice for compound and port-aware graphs. Child node coordinates are
|
|
14
|
+
* relative to their parent (matching xyflow's convention).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { getElkLayout } from '@statelyai/graph/layout/elk';
|
|
19
|
+
*
|
|
20
|
+
* const laidOut = await getElkLayout(graph, {
|
|
21
|
+
* algorithm: 'layered',
|
|
22
|
+
* measure: (node) => measureText(node.label),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
async function getElkLayout(graph, options) {
|
|
27
|
+
const sized = createVisualGraph({
|
|
28
|
+
id: graph.id,
|
|
29
|
+
mode: graph.mode,
|
|
30
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
31
|
+
direction: options?.direction ?? graph.direction,
|
|
32
|
+
data: graph.data,
|
|
33
|
+
...graph.style !== void 0 && { style: graph.style },
|
|
34
|
+
nodes: graph.nodes.map((node) => ({
|
|
35
|
+
...toNodeConfig(node),
|
|
36
|
+
...getNodeSize(node, options)
|
|
37
|
+
})),
|
|
38
|
+
edges: graph.edges.map((edge) => toEdgeConfig(edge))
|
|
39
|
+
});
|
|
40
|
+
const root = toELK(sized);
|
|
41
|
+
const layerOf = options?.constraints?.layer;
|
|
42
|
+
let hasPartitions = false;
|
|
43
|
+
if (layerOf) {
|
|
44
|
+
const nodeById = new Map(sized.nodes.map((node) => [node.id, node]));
|
|
45
|
+
const stack = [...root.children ?? []];
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
const child = stack.pop();
|
|
48
|
+
const node = nodeById.get(child.id);
|
|
49
|
+
const layer = node === void 0 ? void 0 : layerOf(node);
|
|
50
|
+
if (layer !== void 0) {
|
|
51
|
+
hasPartitions = true;
|
|
52
|
+
child.layoutOptions = {
|
|
53
|
+
...child.layoutOptions,
|
|
54
|
+
"elk.partitioning.partition": String(layer)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
stack.push(...child.children ?? []);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
root.layoutOptions = {
|
|
61
|
+
"elk.algorithm": options?.algorithm ?? "layered",
|
|
62
|
+
...hasPartitions && { "elk.partitioning.activate": "true" },
|
|
63
|
+
...options?.spacing?.node !== void 0 && { "elk.spacing.nodeNode": String(options.spacing.node) },
|
|
64
|
+
...options?.spacing?.layer !== void 0 && { "elk.layered.spacing.nodeNodeBetweenLayers": String(options.spacing.layer) },
|
|
65
|
+
...options?.seed !== void 0 && { "elk.randomSeed": String(options.seed) },
|
|
66
|
+
...root.layoutOptions,
|
|
67
|
+
...options?.layoutOptions
|
|
68
|
+
};
|
|
69
|
+
return fromELK(await (options?.elk ?? (defaultElk ??= new ELK())).layout(root));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { getElkLayout };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
|
|
2
|
+
import { LayoutOptions } from "./index.mjs";
|
|
3
|
+
import { ForceAtlas2Settings } from "graphology-layout-forceatlas2";
|
|
4
|
+
|
|
5
|
+
//#region src/layout/forceatlas2.d.ts
|
|
6
|
+
interface ForceAtlas2LayoutOptions extends LayoutOptions {
|
|
7
|
+
/** Number of ForceAtlas2 iterations. Default: 100. */
|
|
8
|
+
iterations?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Pass-through engine settings (`scalingRatio`, `gravity`, `linLogMode`,
|
|
11
|
+
* `barnesHutOptimize`, …). See graphology-layout-forceatlas2.
|
|
12
|
+
*/
|
|
13
|
+
settings?: ForceAtlas2Settings;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One-shot ForceAtlas2 layout via graphology (optional peer dependencies
|
|
17
|
+
* `graphology` + `graphology-layout-forceatlas2`). Runs the synchronous
|
|
18
|
+
* engine for `options.iterations` iterations and returns a positioned
|
|
19
|
+
* {@link VisualGraph}.
|
|
20
|
+
*
|
|
21
|
+
* - Deterministic: FA2 itself has no randomness, but it requires initial
|
|
22
|
+
* positions. Positioned nodes start from their current centers
|
|
23
|
+
* (incremental relayout); unpositioned nodes start in a seeded scatter
|
|
24
|
+
* driven by `options.seed` — the same seed always produces the same
|
|
25
|
+
* layout.
|
|
26
|
+
* - `options.isFixed` pins positioned nodes at their current `x`/`y` via
|
|
27
|
+
* FA2's `fixed` node attribute (honored natively by the engine).
|
|
28
|
+
* - Per-edge `weight` feeds FA2's attraction (its default edge-weight
|
|
29
|
+
* getter reads the `weight` attribute; tune with
|
|
30
|
+
* `settings.edgeWeightInfluence`).
|
|
31
|
+
* - Self-loops are skipped (zero-distance edges destabilize the forces);
|
|
32
|
+
* parallel edges are kept (multigraph). Hierarchy is ignored (force
|
|
33
|
+
* layouts are flat).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { getForceAtlas2Layout } from '@statelyai/graph/layout/forceatlas2';
|
|
38
|
+
*
|
|
39
|
+
* const laidOut = getForceAtlas2Layout(graph, {
|
|
40
|
+
* seed: 42,
|
|
41
|
+
* iterations: 200,
|
|
42
|
+
* settings: { gravity: 2 },
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare function getForceAtlas2Layout(graph: Graph | VisualGraph, options?: ForceAtlas2LayoutOptions): VisualGraph;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ForceAtlas2LayoutOptions, getForceAtlas2Layout };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { f as createVisualGraph, n as toNodeConfig, t as toEdgeConfig } from "../config-Dt5u1gSf.mjs";
|
|
2
|
+
import { getNodeSize } from "./index.mjs";
|
|
3
|
+
import GraphologyGraph from "graphology";
|
|
4
|
+
import forceAtlas2 from "graphology-layout-forceatlas2";
|
|
5
|
+
|
|
6
|
+
//#region src/layout/forceatlas2.ts
|
|
7
|
+
/** mulberry32 — same seeded PRNG the rest of the library uses. */
|
|
8
|
+
function mulberry32(seed) {
|
|
9
|
+
let s = seed | 0;
|
|
10
|
+
return () => {
|
|
11
|
+
s = s + 1831565813 | 0;
|
|
12
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
13
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
14
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One-shot ForceAtlas2 layout via graphology (optional peer dependencies
|
|
19
|
+
* `graphology` + `graphology-layout-forceatlas2`). Runs the synchronous
|
|
20
|
+
* engine for `options.iterations` iterations and returns a positioned
|
|
21
|
+
* {@link VisualGraph}.
|
|
22
|
+
*
|
|
23
|
+
* - Deterministic: FA2 itself has no randomness, but it requires initial
|
|
24
|
+
* positions. Positioned nodes start from their current centers
|
|
25
|
+
* (incremental relayout); unpositioned nodes start in a seeded scatter
|
|
26
|
+
* driven by `options.seed` — the same seed always produces the same
|
|
27
|
+
* layout.
|
|
28
|
+
* - `options.isFixed` pins positioned nodes at their current `x`/`y` via
|
|
29
|
+
* FA2's `fixed` node attribute (honored natively by the engine).
|
|
30
|
+
* - Per-edge `weight` feeds FA2's attraction (its default edge-weight
|
|
31
|
+
* getter reads the `weight` attribute; tune with
|
|
32
|
+
* `settings.edgeWeightInfluence`).
|
|
33
|
+
* - Self-loops are skipped (zero-distance edges destabilize the forces);
|
|
34
|
+
* parallel edges are kept (multigraph). Hierarchy is ignored (force
|
|
35
|
+
* layouts are flat).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { getForceAtlas2Layout } from '@statelyai/graph/layout/forceatlas2';
|
|
40
|
+
*
|
|
41
|
+
* const laidOut = getForceAtlas2Layout(graph, {
|
|
42
|
+
* seed: 42,
|
|
43
|
+
* iterations: 200,
|
|
44
|
+
* settings: { gravity: 2 },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
function getForceAtlas2Layout(graph, options) {
|
|
49
|
+
const rng = mulberry32(options?.seed ?? 1);
|
|
50
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
51
|
+
const scatterRadius = Math.max(80, 30 * Math.sqrt(graph.nodes.length));
|
|
52
|
+
const engineGraph = new GraphologyGraph({ multi: true });
|
|
53
|
+
for (const node of graph.nodes) {
|
|
54
|
+
const size = getNodeSize(node, options);
|
|
55
|
+
sizes.set(node.id, size);
|
|
56
|
+
if (node.x !== void 0 && node.y !== void 0) engineGraph.addNode(node.id, {
|
|
57
|
+
x: node.x + size.width / 2,
|
|
58
|
+
y: node.y + size.height / 2,
|
|
59
|
+
...options?.isFixed?.(node) && { fixed: true }
|
|
60
|
+
});
|
|
61
|
+
else {
|
|
62
|
+
const angle = rng() * 2 * Math.PI;
|
|
63
|
+
const radius = scatterRadius * Math.sqrt(rng());
|
|
64
|
+
engineGraph.addNode(node.id, {
|
|
65
|
+
x: Math.cos(angle) * radius,
|
|
66
|
+
y: Math.sin(angle) * radius
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const edge of graph.edges) {
|
|
71
|
+
if (!sizes.has(edge.sourceId) || !sizes.has(edge.targetId) || edge.sourceId === edge.targetId) continue;
|
|
72
|
+
engineGraph.addEdgeWithKey(edge.id, edge.sourceId, edge.targetId, edge.weight !== void 0 ? { weight: edge.weight } : {});
|
|
73
|
+
}
|
|
74
|
+
const positions = forceAtlas2(engineGraph, {
|
|
75
|
+
iterations: options?.iterations ?? 100,
|
|
76
|
+
...options?.settings !== void 0 && { settings: options.settings }
|
|
77
|
+
});
|
|
78
|
+
return createVisualGraph({
|
|
79
|
+
id: graph.id,
|
|
80
|
+
mode: graph.mode,
|
|
81
|
+
initialNodeId: graph.initialNodeId ?? void 0,
|
|
82
|
+
direction: graph.direction,
|
|
83
|
+
data: graph.data,
|
|
84
|
+
...graph.style !== void 0 && { style: graph.style },
|
|
85
|
+
nodes: graph.nodes.map((node) => {
|
|
86
|
+
const size = sizes.get(node.id);
|
|
87
|
+
const position = positions[node.id];
|
|
88
|
+
return {
|
|
89
|
+
...toNodeConfig(node),
|
|
90
|
+
...size,
|
|
91
|
+
x: position.x - size.width / 2,
|
|
92
|
+
y: position.y - size.height / 2
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
edges: graph.edges.map((edge) => toEdgeConfig(edge))
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { getForceAtlas2Layout };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { I as VisualGraph, f as Graph } from "../types-BAEQTwK_.mjs";
|
|
2
|
+
import { LayoutOptions } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/layout/graphviz.d.ts
|
|
5
|
+
/** Graphviz layout engines supported by the adapter. */
|
|
6
|
+
type GraphvizEngine = 'dot' | 'neato' | 'fdp' | 'sfdp' | 'circo' | 'twopi' | 'osage' | 'patchwork';
|
|
7
|
+
interface GraphvizLayoutOptions extends LayoutOptions {
|
|
8
|
+
/** Graphviz layout engine. Defaults to `'dot'`. */
|
|
9
|
+
engine?: GraphvizEngine;
|
|
10
|
+
/**
|
|
11
|
+
* Raw Graphviz graph attributes, emitted last (override everything).
|
|
12
|
+
* See https://graphviz.org/doc/info/attrs.html
|
|
13
|
+
*/
|
|
14
|
+
graphAttributes?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Lay out a graph with Graphviz (via `@hpcc-js/wasm-graphviz`, an optional
|
|
18
|
+
* peer dependency). Pure: returns a new {@link VisualGraph} with node
|
|
19
|
+
* positions/sizes, routed edge `points` (`routing: 'splines'` — Graphviz
|
|
20
|
+
* b-spline control points, tail → head, endpoints included), and computed
|
|
21
|
+
* edge label rects (edge `x`/`y`).
|
|
22
|
+
*
|
|
23
|
+
* Node sizes are resolved via {@link getNodeSize} and passed to Graphviz as
|
|
24
|
+
* fixed sizes, so layout never depends on Graphviz's own text measurement.
|
|
25
|
+
*
|
|
26
|
+
* Notes:
|
|
27
|
+
* - Compound graphs (`parentId`) are not supported — use `getElkLayout`, or
|
|
28
|
+
* `getFlattenedGraph()` the graph first. (Graphviz clusters: planned.)
|
|
29
|
+
* - In a directed graph, edges with `mode: 'undirected'` are laid out as
|
|
30
|
+
* directed but drawn without arrowheads (`dir=none`); in an undirected
|
|
31
|
+
* graph, per-edge `mode: 'directed'` overrides are ignored (DOT `graph`
|
|
32
|
+
* has no directed edge operator).
|
|
33
|
+
* - `options.seed` maps to the Graphviz `start` attribute (used by the
|
|
34
|
+
* randomized engines neato/fdp/sfdp; ignored by deterministic engines).
|
|
35
|
+
* - `options.constraints.layer` maps to `{ rank=same; … }` groups (`dot`
|
|
36
|
+
* engine only — the other engines have no rank concept).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { getGraphvizLayout } from '@statelyai/graph/layout/graphviz';
|
|
41
|
+
*
|
|
42
|
+
* const laidOut = await getGraphvizLayout(graph, {
|
|
43
|
+
* engine: 'dot',
|
|
44
|
+
* measure: (node) => measureText(node.label),
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare function getGraphvizLayout(graph: Graph | VisualGraph, options?: GraphvizLayoutOptions): Promise<VisualGraph>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { GraphvizEngine, GraphvizLayoutOptions, getGraphvizLayout };
|