@statelyai/graph 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +12 -11
  2. package/dist/{algorithms-NWSB2RWj.mjs → algorithms-DldwenLt.mjs} +11 -6
  3. package/dist/algorithms.d.mts +1 -1
  4. package/dist/algorithms.mjs +1 -1
  5. package/dist/{converter-CchokMDg.mjs → converter-B5CUD0r9.mjs} +2 -2
  6. package/dist/formats/adjacency-list/index.d.mts +1 -1
  7. package/dist/formats/adjacency-list/index.mjs +1 -1
  8. package/dist/formats/converter/index.d.mts +2 -2
  9. package/dist/formats/converter/index.mjs +1 -1
  10. package/dist/formats/cytoscape/index.d.mts +1 -1
  11. package/dist/formats/cytoscape/index.mjs +4 -4
  12. package/dist/formats/d3/index.d.mts +1 -1
  13. package/dist/formats/d3/index.mjs +1 -1
  14. package/dist/formats/dot/index.d.mts +1 -1
  15. package/dist/formats/dot/index.mjs +1 -1
  16. package/dist/formats/edge-list/index.d.mts +1 -1
  17. package/dist/formats/edge-list/index.mjs +1 -1
  18. package/dist/formats/elk/index.d.mts +61 -0
  19. package/dist/formats/elk/index.mjs +176 -0
  20. package/dist/formats/gexf/index.d.mts +1 -1
  21. package/dist/formats/gexf/index.mjs +5 -5
  22. package/dist/formats/gml/index.d.mts +1 -1
  23. package/dist/formats/gml/index.mjs +3 -3
  24. package/dist/formats/graphml/index.d.mts +1 -1
  25. package/dist/formats/graphml/index.mjs +2 -2
  26. package/dist/formats/jgf/index.d.mts +1 -1
  27. package/dist/formats/jgf/index.mjs +4 -4
  28. package/dist/formats/mermaid/index.d.mts +46 -33
  29. package/dist/formats/mermaid/index.mjs +319 -35
  30. package/dist/formats/tgf/index.d.mts +1 -1
  31. package/dist/formats/tgf/index.mjs +1 -1
  32. package/dist/formats/xyflow/index.d.mts +73 -0
  33. package/dist/formats/xyflow/index.mjs +133 -0
  34. package/dist/index.d.mts +1 -1
  35. package/dist/index.mjs +5 -5
  36. package/dist/{indexing-eNDrXdDA.mjs → indexing-DyfgLuzw.mjs} +6 -5
  37. package/dist/queries.d.mts +2 -2
  38. package/dist/queries.mjs +9 -9
  39. package/dist/schemas.d.mts +37 -4
  40. package/dist/schemas.mjs +26 -5
  41. package/dist/{types-BDXC1O5b.d.mts → types-FBZCrmnG.d.mts} +17 -6
  42. package/package.json +9 -1
  43. package/schemas/edge.schema.json +32 -1
  44. package/schemas/graph.schema.json +114 -4
  45. package/schemas/node.schema.json +45 -2
  46. /package/dist/{adjacency-list-ITO40kmn.mjs → adjacency-list-fldj-QAL.mjs} +0 -0
  47. /package/dist/{edge-list-CgX6bBIF.mjs → edge-list-Br05wXMg.mjs} +0 -0
package/README.md CHANGED
@@ -4,17 +4,6 @@ A TypeScript graph library built on plain JSON objects. Supports directed/undire
4
4
 
5
5
  Made from our experience at [stately.ai](https://stately.ai), where we build visual tools for complex systems.
6
6
 
7
- ## Why this library?
8
-
9
- Graph file formats (GEXF, GraphML) define how to _store_ graphs. Visualization libraries (Cytoscape.js, D3) define how to _render_ them. Neither gives you a good way to _work with_ them in between.
10
-
11
- This library is the computational layer: plain JSON objects in, algorithms and mutations, plain JSON objects out. No classes, no DOM, no rendering engine — just data and functions.
12
-
13
- ```
14
- GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSON() → render
15
- ```
16
-
17
- Your `Graph` is a plain object that survives `JSON.stringify`, `structuredClone`, `postMessage`, and `localStorage` without adapters. Format converters are the I/O ports — read from any supported format, do your work, export to whatever your renderer or database expects.
18
7
 
19
8
  ## Install
20
9
 
@@ -126,6 +115,18 @@ const back = cytoscapeConverter.from(cyto);
126
115
  const myConverter = createFormatConverter(myToFn, myFromFn);
127
116
  ```
128
117
 
118
+ ## Why this library?
119
+
120
+ Graph file formats (GEXF, GraphML) define how to _store_ graphs. Visualization libraries (Cytoscape.js, D3) define how to _render_ them. Neither gives you a good way to _work with_ them in between.
121
+
122
+ This library is the computational layer: plain JSON objects in, algorithms and mutations, plain JSON objects out. No classes, no DOM, no rendering engine; just data and functions.
123
+
124
+ ```
125
+ GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSON() → render
126
+ ```
127
+
128
+ Your `Graph` is a plain object that survives `JSON.stringify`, `structuredClone`, `postMessage`, and `localStorage` without adapters. Format converters are the I/O ports: read from any supported format, do your work, export to whatever your renderer or database expects.
129
+
129
130
  ## API
130
131
 
131
132
  ### Graph Creation
@@ -1,12 +1,14 @@
1
- import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-eNDrXdDA.mjs";
1
+ import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-DyfgLuzw.mjs";
2
2
 
3
3
  //#region src/graph.ts
4
4
  function resolveNode(config) {
5
+ if (!config.id) throw new Error("Node id must be a non-empty string");
6
+ if (config.parentId === "") throw new Error("Node parentId must be a non-empty string");
5
7
  const node = {
6
8
  type: "node",
7
9
  id: config.id,
8
- parentId: config.parentId ?? null,
9
- initialNodeId: config.initialNodeId ?? null,
10
+ ...config.parentId !== void 0 && { parentId: config.parentId ?? null },
11
+ ...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
10
12
  label: config.label ?? "",
11
13
  data: config.data
12
14
  };
@@ -20,6 +22,9 @@ function resolveNode(config) {
20
22
  return node;
21
23
  }
22
24
  function resolveEdge(config) {
25
+ if (!config.id) throw new Error("Edge id must be a non-empty string");
26
+ if (!config.sourceId) throw new Error("Edge sourceId must be a non-empty string");
27
+ if (!config.targetId) throw new Error("Edge targetId must be a non-empty string");
23
28
  const edge = {
24
29
  type: "edge",
25
30
  id: config.id,
@@ -251,10 +256,10 @@ function hasEdge(graph, id) {
251
256
  * ```
252
257
  */
253
258
  function addNode(graph, config) {
259
+ const node = resolveNode(config);
254
260
  const idx = getIndex(graph);
255
261
  if (idx.nodeById.has(config.id)) throw new Error(`Node "${config.id}" already exists`);
256
- if (config.parentId != null && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
257
- const node = resolveNode(config);
262
+ if (config.parentId && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
258
263
  indexAddNode(idx, node, graph.nodes.push(node) - 1);
259
264
  return node;
260
265
  }
@@ -270,11 +275,11 @@ function addNode(graph, config) {
270
275
  * ```
271
276
  */
272
277
  function addEdge(graph, config) {
278
+ const edge = resolveEdge(config);
273
279
  const idx = getIndex(graph);
274
280
  if (idx.edgeById.has(config.id)) throw new Error(`Edge "${config.id}" already exists`);
275
281
  if (!idx.nodeById.has(config.sourceId)) throw new Error(`Source node "${config.sourceId}" does not exist`);
276
282
  if (!idx.nodeById.has(config.targetId)) throw new Error(`Target node "${config.targetId}" does not exist`);
277
- const edge = resolveEdge(config);
278
283
  indexAddEdge(idx, edge, graph.edges.push(edge) - 1);
279
284
  return edge;
280
285
  }
@@ -1,4 +1,4 @@
1
- import { C as TraversalOptions, _ as MSTOptions, b as PathOptions, c as Graph, h as GraphPath, p as GraphNode, t as AllPairsShortestPathsOptions, x as SinglePathOptions } from "./types-BDXC1O5b.mjs";
1
+ import { C as TraversalOptions, _ as MSTOptions, b as PathOptions, c as Graph, h as GraphPath, p as GraphNode, t as AllPairsShortestPathsOptions, x as SinglePathOptions } from "./types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/algorithms.d.ts
4
4
 
@@ -1,3 +1,3 @@
1
- import { C as isAcyclic, E as joinPaths, S as hasPath, T as isTree, _ as getShortestPaths, a as genPreorders, b as getStronglyConnectedComponents, c as getAllPairsShortestPaths, d as getMinimumSpanningTree, f as getPostorder, g as getShortestPath, h as getPreorders, i as genPostorders, l as getConnectedComponents, m as getPreorder, n as dfs, o as genShortestPaths, p as getPostorders, r as genCycles, s as genSimplePaths, t as bfs, u as getCycles, v as getSimplePath, w as isConnected, x as getTopologicalSort, y as getSimplePaths } from "./algorithms-NWSB2RWj.mjs";
1
+ import { C as isAcyclic, E as joinPaths, S as hasPath, T as isTree, _ as getShortestPaths, a as genPreorders, b as getStronglyConnectedComponents, c as getAllPairsShortestPaths, d as getMinimumSpanningTree, f as getPostorder, g as getShortestPath, h as getPreorders, i as genPostorders, l as getConnectedComponents, m as getPreorder, n as dfs, o as genShortestPaths, p as getPostorders, r as genCycles, s as genSimplePaths, t as bfs, u as getCycles, v as getSimplePath, w as isConnected, x as getTopologicalSort, y as getSimplePaths } from "./algorithms-DldwenLt.mjs";
2
2
 
3
3
  export { bfs, dfs, genCycles, genPostorders, genPreorders, genShortestPaths, genSimplePaths, getAllPairsShortestPaths, getConnectedComponents, getCycles, getMinimumSpanningTree, getPostorder, getPostorders, getPreorder, getPreorders, getShortestPath, getShortestPaths, getSimplePath, getSimplePaths, getStronglyConnectedComponents, getTopologicalSort, hasPath, isAcyclic, isConnected, isTree, joinPaths };
@@ -1,5 +1,5 @@
1
- import { n as toAdjacencyList, t as fromAdjacencyList } from "./adjacency-list-ITO40kmn.mjs";
2
- import { n as toEdgeList, t as fromEdgeList } from "./edge-list-CgX6bBIF.mjs";
1
+ import { n as toAdjacencyList, t as fromAdjacencyList } from "./adjacency-list-fldj-QAL.mjs";
2
+ import { n as toEdgeList, t as fromEdgeList } from "./edge-list-Br05wXMg.mjs";
3
3
 
4
4
  //#region src/formats/converter/index.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { c as Graph } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/adjacency-list/index.d.ts
4
4
 
@@ -1,3 +1,3 @@
1
- import { n as toAdjacencyList, t as fromAdjacencyList } from "../../adjacency-list-ITO40kmn.mjs";
1
+ import { n as toAdjacencyList, t as fromAdjacencyList } from "../../adjacency-list-fldj-QAL.mjs";
2
2
 
3
3
  export { fromAdjacencyList, toAdjacencyList };
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/converter/index.d.ts
4
4
 
@@ -18,7 +18,7 @@ import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs"
18
18
  * const graph = yamlConverter.from(yaml);
19
19
  * ```
20
20
  */
21
- declare function createFormatConverter<TSerial>(to: (graph: Graph) => TSerial, from: (input: TSerial) => Graph): GraphFormatConverter<TSerial>;
21
+ declare function createFormatConverter<TSerial, N = any, E = any, G = any>(to: (graph: Graph<N, E, G>) => TSerial, from: (input: TSerial) => Graph<N, E, G>): GraphFormatConverter<TSerial, N, E, G>;
22
22
  /**
23
23
  * Bidirectional converter for adjacency-list format (`Record<string, string[]>`).
24
24
  *
@@ -1,3 +1,3 @@
1
- import { n as createFormatConverter, r as edgeListConverter, t as adjacencyListConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter, r as edgeListConverter, t as adjacencyListConverter } from "../../converter-B5CUD0r9.mjs";
2
2
 
3
3
  export { adjacencyListConverter, createFormatConverter, edgeListConverter };
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/cytoscape/index.d.ts
4
4
  interface CytoscapeNode {
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
 
3
3
  //#region src/formats/cytoscape/index.ts
4
4
  /**
@@ -22,7 +22,7 @@ function toCytoscapeJSON(graph) {
22
22
  const graphData = {};
23
23
  if (graph.id) graphData.id = graph.id;
24
24
  graphData.type = graph.type;
25
- if (graph.initialNodeId !== null) graphData.initialNodeId = graph.initialNodeId;
25
+ if (graph.initialNodeId) graphData.initialNodeId = graph.initialNodeId;
26
26
  if (graph.data !== void 0) graphData.graphData = graph.data;
27
27
  if (graph.direction) graphData.direction = graph.direction;
28
28
  return {
@@ -30,9 +30,9 @@ function toCytoscapeJSON(graph) {
30
30
  elements: {
31
31
  nodes: graph.nodes.map((n) => {
32
32
  const data = { id: n.id };
33
- if (n.parentId !== null) data.parent = n.parentId;
33
+ if (n.parentId) data.parent = n.parentId;
34
34
  if (n.label) data.label = n.label;
35
- if (n.initialNodeId !== null) data.initialNodeId = n.initialNodeId;
35
+ if (n.initialNodeId) data.initialNodeId = n.initialNodeId;
36
36
  if (n.data !== void 0) data.nodeData = n.data;
37
37
  if (n.width !== void 0) data.width = n.width;
38
38
  if (n.height !== void 0) data.height = n.height;
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/d3/index.d.ts
4
4
  interface D3Node {
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
 
3
3
  //#region src/formats/d3/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/dot/index.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
  import parse from "dotparser";
3
3
 
4
4
  //#region src/formats/dot/index.ts
@@ -1,4 +1,4 @@
1
- import { c as Graph } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/edge-list/index.d.ts
4
4
 
@@ -1,3 +1,3 @@
1
- import { n as toEdgeList, t as fromEdgeList } from "../../edge-list-CgX6bBIF.mjs";
1
+ import { n as toEdgeList, t as fromEdgeList } from "../../edge-list-Br05wXMg.mjs";
2
2
 
3
3
  export { fromEdgeList, toEdgeList };
@@ -0,0 +1,61 @@
1
+ import { D as VisualGraphFormatConverter, T as VisualGraph } from "../../types-FBZCrmnG.mjs";
2
+ import { ElkEdge, ElkEdgeSection, ElkExtendedEdge, ElkGraphElement, ElkLabel, ElkNode, ElkNode as ElkNode$1, ElkPoint, ElkPort, ElkPrimitiveEdge, ElkShape, LayoutOptions } from "elkjs/lib/elk-api";
3
+
4
+ //#region src/formats/elk/index.d.ts
5
+
6
+ /**
7
+ * Converts a visual graph to ELK JSON format.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { createVisualGraph } from '@statelyai/graph';
12
+ * import { toELK } from '@statelyai/graph/elk';
13
+ *
14
+ * const graph = createVisualGraph({
15
+ * nodes: [
16
+ * { id: 'a', x: 0, y: 0, width: 100, height: 50 },
17
+ * { id: 'b', x: 200, y: 0, width: 100, height: 50 },
18
+ * ],
19
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
20
+ * });
21
+ *
22
+ * const elk = toELK(graph);
23
+ * // { id: '', children: [...], edges: [...] }
24
+ * ```
25
+ */
26
+ declare function toELK(graph: VisualGraph): ElkNode$1;
27
+ /**
28
+ * Parses an ELK JSON node into a visual graph.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { fromELK } from '@statelyai/graph/elk';
33
+ *
34
+ * const graph = fromELK({
35
+ * id: 'root',
36
+ * children: [
37
+ * { id: 'a', x: 0, y: 0, width: 100, height: 50 },
38
+ * { id: 'b', x: 200, y: 0, width: 100, height: 50 },
39
+ * ],
40
+ * edges: [{ id: 'e1', sources: ['a'], targets: ['b'] }],
41
+ * });
42
+ *
43
+ * graph.nodes; // [{id: 'a', x: 0, y: 0, ...}, {id: 'b', x: 200, ...}]
44
+ * graph.edges; // [{sourceId: 'a', targetId: 'b', ...}]
45
+ * ```
46
+ */
47
+ declare function fromELK(elkRoot: ElkNode$1): VisualGraph;
48
+ /**
49
+ * Bidirectional converter for ELK JSON format.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { elkConverter } from '@statelyai/graph/elk';
54
+ *
55
+ * const elk = elkConverter.to(graph);
56
+ * const roundTripped = elkConverter.from(elk);
57
+ * ```
58
+ */
59
+ declare const elkConverter: VisualGraphFormatConverter<ElkNode$1>;
60
+ //#endregion
61
+ export { type ElkEdge, type ElkEdgeSection, type ElkExtendedEdge, type ElkGraphElement, type ElkLabel, type ElkNode, type ElkPoint, type ElkPort, type ElkPrimitiveEdge, type ElkShape, type LayoutOptions, elkConverter, fromELK, toELK };
@@ -0,0 +1,176 @@
1
+ import { getChildren } from "../../queries.mjs";
2
+
3
+ //#region src/formats/elk/index.ts
4
+ const DIRECTION_TO_ELK = {
5
+ down: "DOWN",
6
+ up: "UP",
7
+ right: "RIGHT",
8
+ left: "LEFT"
9
+ };
10
+ const ELK_TO_DIRECTION = {
11
+ DOWN: "down",
12
+ UP: "up",
13
+ RIGHT: "right",
14
+ LEFT: "left"
15
+ };
16
+ function convertEdge(edge) {
17
+ const elkEdge = {
18
+ id: edge.id,
19
+ sources: [edge.sourceId],
20
+ targets: [edge.targetId]
21
+ };
22
+ if (edge.label) elkEdge.labels = [{ text: edge.label }];
23
+ return elkEdge;
24
+ }
25
+ function convertNode(graph, node) {
26
+ const elkNode = {
27
+ id: node.id,
28
+ x: node.x,
29
+ y: node.y,
30
+ width: node.width,
31
+ height: node.height
32
+ };
33
+ if (node.label) elkNode.labels = [{ text: node.label }];
34
+ const children = getChildren(graph, node.id);
35
+ if (children.length > 0) {
36
+ elkNode.children = children.map((child) => convertNode(graph, child));
37
+ const descendantIds = /* @__PURE__ */ new Set();
38
+ collectDescendants(graph, node.id, descendantIds);
39
+ const innerEdges = graph.edges.filter((e) => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId));
40
+ if (innerEdges.length > 0) elkNode.edges = innerEdges.map(convertEdge);
41
+ }
42
+ return elkNode;
43
+ }
44
+ function collectDescendants(graph, nodeId, set) {
45
+ const children = getChildren(graph, nodeId);
46
+ for (const child of children) {
47
+ set.add(child.id);
48
+ collectDescendants(graph, child.id, set);
49
+ }
50
+ }
51
+ /**
52
+ * Converts a visual graph to ELK JSON format.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { createVisualGraph } from '@statelyai/graph';
57
+ * import { toELK } from '@statelyai/graph/elk';
58
+ *
59
+ * const graph = createVisualGraph({
60
+ * nodes: [
61
+ * { id: 'a', x: 0, y: 0, width: 100, height: 50 },
62
+ * { id: 'b', x: 200, y: 0, width: 100, height: 50 },
63
+ * ],
64
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
65
+ * });
66
+ *
67
+ * const elk = toELK(graph);
68
+ * // { id: '', children: [...], edges: [...] }
69
+ * ```
70
+ */
71
+ function toELK(graph) {
72
+ const root = { id: graph.id };
73
+ const elkDir = DIRECTION_TO_ELK[graph.direction];
74
+ if (elkDir) root.layoutOptions = { "elk.direction": elkDir };
75
+ const roots = getChildren(graph, null);
76
+ if (roots.length > 0) root.children = roots.map((node) => convertNode(graph, node));
77
+ const allInnerEdgeIds = /* @__PURE__ */ new Set();
78
+ for (const node of graph.nodes) if (getChildren(graph, node.id).length > 0) {
79
+ const descendantIds = /* @__PURE__ */ new Set();
80
+ collectDescendants(graph, node.id, descendantIds);
81
+ for (const edge of graph.edges) if (descendantIds.has(edge.sourceId) && descendantIds.has(edge.targetId)) allInnerEdgeIds.add(edge.id);
82
+ }
83
+ const rootEdges = graph.edges.filter((e) => !allInnerEdgeIds.has(e.id));
84
+ if (rootEdges.length > 0) root.edges = rootEdges.map(convertEdge);
85
+ return root;
86
+ }
87
+ function flattenElkNodes(elkNode, parentId, nodes, edges, edgeIdx) {
88
+ if (elkNode.children) for (const child of elkNode.children) {
89
+ const label = child.labels?.[0]?.text ?? "";
90
+ const node = {
91
+ type: "node",
92
+ id: child.id,
93
+ parentId,
94
+ initialNodeId: null,
95
+ label,
96
+ data: void 0,
97
+ x: child.x ?? 0,
98
+ y: child.y ?? 0,
99
+ width: child.width ?? 0,
100
+ height: child.height ?? 0
101
+ };
102
+ nodes.push(node);
103
+ flattenElkNodes(child, child.id, nodes, edges, edgeIdx);
104
+ }
105
+ if (elkNode.edges) for (const elkEdge of elkNode.edges) for (const source of elkEdge.sources) for (const target of elkEdge.targets) {
106
+ const edge = {
107
+ type: "edge",
108
+ id: elkEdge.id ?? `e${edgeIdx.value++}`,
109
+ sourceId: source,
110
+ targetId: target,
111
+ label: elkEdge.labels?.[0]?.text ?? "",
112
+ data: void 0,
113
+ x: 0,
114
+ y: 0,
115
+ width: 0,
116
+ height: 0
117
+ };
118
+ edges.push(edge);
119
+ }
120
+ }
121
+ /**
122
+ * Parses an ELK JSON node into a visual graph.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { fromELK } from '@statelyai/graph/elk';
127
+ *
128
+ * const graph = fromELK({
129
+ * id: 'root',
130
+ * children: [
131
+ * { id: 'a', x: 0, y: 0, width: 100, height: 50 },
132
+ * { id: 'b', x: 200, y: 0, width: 100, height: 50 },
133
+ * ],
134
+ * edges: [{ id: 'e1', sources: ['a'], targets: ['b'] }],
135
+ * });
136
+ *
137
+ * graph.nodes; // [{id: 'a', x: 0, y: 0, ...}, {id: 'b', x: 200, ...}]
138
+ * graph.edges; // [{sourceId: 'a', targetId: 'b', ...}]
139
+ * ```
140
+ */
141
+ function fromELK(elkRoot) {
142
+ const nodes = [];
143
+ const edges = [];
144
+ flattenElkNodes(elkRoot, null, nodes, edges, { value: 0 });
145
+ const seenEdges = /* @__PURE__ */ new Map();
146
+ for (const edge of edges) if (!seenEdges.has(edge.id)) seenEdges.set(edge.id, edge);
147
+ const elkDir = elkRoot.layoutOptions?.["elk.direction"];
148
+ const direction = (elkDir ? ELK_TO_DIRECTION[elkDir] : void 0) ?? "down";
149
+ return {
150
+ id: elkRoot.id,
151
+ type: "directed",
152
+ initialNodeId: null,
153
+ nodes,
154
+ edges: [...seenEdges.values()],
155
+ data: void 0,
156
+ direction
157
+ };
158
+ }
159
+ /**
160
+ * Bidirectional converter for ELK JSON format.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { elkConverter } from '@statelyai/graph/elk';
165
+ *
166
+ * const elk = elkConverter.to(graph);
167
+ * const roundTripped = elkConverter.from(elk);
168
+ * ```
169
+ */
170
+ const elkConverter = {
171
+ to: toELK,
172
+ from: fromELK
173
+ };
174
+
175
+ //#endregion
176
+ export { elkConverter, fromELK, toELK };
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/gexf/index.d.ts
4
4
  declare function toGEXF(graph: Graph): string;
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
3
3
 
4
4
  //#region src/formats/gexf/index.ts
@@ -32,11 +32,11 @@ function toGEXF(graph) {
32
32
  }];
33
33
  const nodes = graph.nodes.map((n) => {
34
34
  const attvalues = [];
35
- if (n.parentId !== null) attvalues.push({
35
+ if (n.parentId) attvalues.push({
36
36
  "@_for": "a_parentId",
37
37
  "@_value": n.parentId
38
38
  });
39
- if (n.initialNodeId !== null) attvalues.push({
39
+ if (n.initialNodeId) attvalues.push({
40
40
  "@_for": "a_initialNodeId",
41
41
  "@_value": n.initialNodeId
42
42
  });
@@ -52,7 +52,7 @@ function toGEXF(graph) {
52
52
  "@_id": n.id,
53
53
  "@_label": n.label || n.id
54
54
  };
55
- if (n.parentId !== null) node["@_pid"] = n.parentId;
55
+ if (n.parentId) node["@_pid"] = n.parentId;
56
56
  if (attvalues.length > 0) node.attvalues = { attvalue: attvalues };
57
57
  if (n.color) {
58
58
  const hex$1 = n.color.replace("#", "");
@@ -91,7 +91,7 @@ function toGEXF(graph) {
91
91
  return edge;
92
92
  });
93
93
  const graphData = [];
94
- if (graph.initialNodeId !== null) graphData.push({
94
+ if (graph.initialNodeId) graphData.push({
95
95
  "@_for": "a_initialNodeId",
96
96
  "@_value": graph.initialNodeId
97
97
  });
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/gml/index.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
 
3
3
  //#region src/formats/gml/index.ts
4
4
  /**
@@ -30,7 +30,7 @@ function toGML(graph) {
30
30
  if (graph.id) lines.push(` id ${gmlString(graph.id)}`);
31
31
  const childrenMap = /* @__PURE__ */ new Map();
32
32
  for (const node of graph.nodes) {
33
- const pid = node.parentId;
33
+ const pid = node.parentId ?? null;
34
34
  if (!childrenMap.has(pid)) childrenMap.set(pid, []);
35
35
  childrenMap.get(pid).push(node);
36
36
  }
@@ -38,7 +38,7 @@ function toGML(graph) {
38
38
  lines.push(`${indent}node [`);
39
39
  lines.push(`${indent} id ${gmlString(node.id)}`);
40
40
  if (node.label) lines.push(`${indent} label ${gmlString(node.label)}`);
41
- if (node.initialNodeId !== null) lines.push(`${indent} initialNodeId ${gmlString(node.initialNodeId)}`);
41
+ if (node.initialNodeId) lines.push(`${indent} initialNodeId ${gmlString(node.initialNodeId)}`);
42
42
  if (node.data !== void 0) lines.push(`${indent} data ${gmlString(JSON.stringify(node.data))}`);
43
43
  if (node.shape) lines.push(`${indent} shape ${gmlString(node.shape)}`);
44
44
  if (node.color) lines.push(`${indent} color ${gmlString(node.color)}`);
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/graphml/index.d.ts
4
4
  declare function toGraphML(graph: Graph): string;
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
3
3
 
4
4
  //#region src/formats/graphml/index.ts
@@ -72,7 +72,7 @@ function toGraphML(graph) {
72
72
  "@_key": "label",
73
73
  "#text": n.label
74
74
  });
75
- if (n.parentId !== null) data.push({
75
+ if (n.parentId) data.push({
76
76
  "@_key": "parentId",
77
77
  "#text": n.parentId
78
78
  });
@@ -1,4 +1,4 @@
1
- import { c as Graph, f as GraphFormatConverter } from "../../types-BDXC1O5b.mjs";
1
+ import { c as Graph, f as GraphFormatConverter } from "../../types-FBZCrmnG.mjs";
2
2
 
3
3
  //#region src/formats/jgf/index.d.ts
4
4
  interface JGFNode {
@@ -1,4 +1,4 @@
1
- import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
1
+ import { n as createFormatConverter } from "../../converter-B5CUD0r9.mjs";
2
2
 
3
3
  //#region src/formats/jgf/index.ts
4
4
  /**
@@ -20,7 +20,7 @@ import { n as createFormatConverter } from "../../converter-CchokMDg.mjs";
20
20
  */
21
21
  function toJGF(graph) {
22
22
  const metadata = {};
23
- if (graph.initialNodeId !== null) metadata.initialNodeId = graph.initialNodeId;
23
+ if (graph.initialNodeId) metadata.initialNodeId = graph.initialNodeId;
24
24
  if (graph.data !== void 0) metadata.data = graph.data;
25
25
  if (graph.direction) metadata.direction = graph.direction;
26
26
  return { graph: {
@@ -29,8 +29,8 @@ function toJGF(graph) {
29
29
  ...Object.keys(metadata).length > 0 && { metadata },
30
30
  nodes: graph.nodes.map((n) => {
31
31
  const meta = {};
32
- if (n.parentId !== null) meta.parentId = n.parentId;
33
- if (n.initialNodeId !== null) meta.initialNodeId = n.initialNodeId;
32
+ if (n.parentId) meta.parentId = n.parentId;
33
+ if (n.initialNodeId) meta.initialNodeId = n.initialNodeId;
34
34
  if (n.data !== void 0) meta.data = n.data;
35
35
  if (n.x !== void 0) meta.x = n.x;
36
36
  if (n.y !== void 0) meta.y = n.y;