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