@statelyai/graph 0.11.1 → 0.13.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 CHANGED
@@ -1,10 +1,6 @@
1
- ---
2
- title: '@statelyai/graph'
3
- ---
4
-
5
1
  # @statelyai/graph
6
2
 
7
- A TypeScript graph library built on plain JSON objects. Supports directed/undirected graphs, hierarchical nodes, graph algorithms, visual properties, and serialization to DOT, GraphML, Mermaid, and more.
3
+ A TypeScript graph library for JSON-serializable graph IR. Use it to validate, analyze, transform, and round-trip directed, undirected, hierarchical, port-aware, and visual graphs across tools.
8
4
 
9
5
  Made from our experience at [stately.ai](https://stately.ai), where we build visual tools for complex systems.
10
6
 
@@ -18,24 +14,24 @@ Optional peers are only needed for specific adapters:
18
14
 
19
15
  <!-- optional peer dependencies derived from package.json#peerDependencies -->
20
16
 
21
- | Package | Needed for |
22
- | --- | --- |
17
+ | Package | Needed for |
18
+ | ----------------- | --------------------------------------------------- |
23
19
  | `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
24
- | `dotparser` | `@statelyai/graph/dot` parsing |
25
- | `cytoscape` | Cytoscape integration tests and consumer typing |
26
- | `d3-force` | D3 force integration tests and consumer typing |
27
- | `elkjs` | `@statelyai/graph/elk` |
28
- | `zod` | `@statelyai/graph/schemas` |
20
+ | `dotparser` | `@statelyai/graph/dot` parsing |
21
+ | `cytoscape` | Cytoscape integration tests and consumer typing |
22
+ | `d3-force` | D3 force integration tests and consumer typing |
23
+ | `elkjs` | `@statelyai/graph/elk` |
24
+ | `zod` | `@statelyai/graph/schemas` |
29
25
 
30
26
  ## Highlights
31
27
 
32
- - Plain JSON graphs with no runtime wrappers required
28
+ - Plain JSON graphs with no runtime wrappers required; omitted `data` defaults to `null`
33
29
  - Standalone functions with a consistent `get*`/`gen*`/`is*`/`add*` naming model
34
30
  - Directed, undirected, hierarchical, and visual graph support
35
31
  - Ports for node-editor and dataflow-style graphs
36
32
  - Algorithms for traversal, paths, centrality, communities, connectivity, isomorphism, ordering, MST, and walks
37
33
  - Diff/patch utilities for graph state changes
38
- - Multi-format conversion via package subpaths
34
+ - Multi-format conversion via package subpaths, with fidelity claims tested against fixtures
39
35
  - Small, fast test suite with broad format coverage
40
36
 
41
37
  ## Quick Start
@@ -43,7 +39,12 @@ Optional peers are only needed for specific adapters:
43
39
  Graphs are plain JSON-serializable objects. All operations are standalone functions — no classes, no DOM, no rendering engine.
44
40
 
45
41
  ```ts
46
- import { createGraph, addNode, addEdge, getShortestPath } from '@statelyai/graph';
42
+ import {
43
+ createGraph,
44
+ addNode,
45
+ addEdge,
46
+ getShortestPath,
47
+ } from '@statelyai/graph';
47
48
 
48
49
  const graph = createGraph({
49
50
  nodes: [
@@ -70,12 +71,17 @@ const path = getShortestPath(graph, { from: 'a', to: 'c' });
70
71
  Look up, add, delete, and update nodes and edges. Query neighbors, predecessors, successors, degree, and more.
71
72
 
72
73
  ```ts
73
- import { getNode, deleteNode, getNeighbors, getSources } from '@statelyai/graph';
74
+ import {
75
+ getNode,
76
+ deleteNode,
77
+ getNeighbors,
78
+ getSources,
79
+ } from '@statelyai/graph';
74
80
 
75
- const node = getNode(graph, 'a'); // lookup by id
76
- deleteNode(graph, 'd'); // removes node + connected edges
81
+ const node = getNode(graph, 'a'); // lookup by id
82
+ deleteNode(graph, 'd'); // removes node + connected edges
77
83
  const neighbors = getNeighbors(graph, 'a'); // adjacent nodes
78
- const roots = getSources(graph); // nodes with no incoming edges
84
+ const roots = getSources(graph); // nodes with no incoming edges
79
85
  ```
80
86
 
81
87
  Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
@@ -96,14 +102,14 @@ const graph = createGraph({
96
102
  { id: 'c' },
97
103
  ],
98
104
  edges: [
99
- { id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
105
+ { id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
100
106
  { id: 'e2', sourceId: 'b1', targetId: 'b2' },
101
- { id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
107
+ { id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
102
108
  ],
103
109
  });
104
110
 
105
111
  const children = getChildren(graph, 'b'); // [b1, b2]
106
- const flat = flatten(graph); // only leaf nodes, edges resolved
112
+ const flat = flatten(graph); // only leaf nodes, edges resolved
107
113
  ```
108
114
 
109
115
  ## Ports
@@ -139,6 +145,26 @@ getPorts(graph, 'fetch'); // [{ name: 'result', ... }]
139
145
  getEdgesByPort(graph, 'render', 'input'); // [e1]
140
146
  ```
141
147
 
148
+ ## Schema Validation
149
+
150
+ <!-- validation helpers exported from src/schemas.ts -->
151
+
152
+ Use the `@statelyai/graph/schemas` subpath when you want runtime validation or JSON Schema generation. `validateGraph()` combines shape checks with graph invariants such as duplicate ids, dangling edges, missing parents, missing initial nodes, duplicate ports, invalid port references, and parent cycles.
153
+
154
+ ```ts
155
+ import { GraphSchema, isGraph, validateGraph } from '@statelyai/graph/schemas';
156
+
157
+ const unknownValue: unknown = JSON.parse(input);
158
+
159
+ if (isGraph(unknownValue)) {
160
+ // fully typed Graph
161
+ } else {
162
+ console.error(validateGraph(unknownValue));
163
+ }
164
+
165
+ const parsed = GraphSchema.parse(unknownValue);
166
+ ```
167
+
142
168
  ## Algorithms
143
169
 
144
170
  <!-- algorithm functions exported from src/algorithms.ts -->
@@ -147,27 +173,40 @@ Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, s
147
173
 
148
174
  ```ts
149
175
  import {
150
- bfs, dfs, hasPath, isAcyclic,
151
- getShortestPath, getCycles, getTopologicalSort,
152
- getConnectedComponents, getMinimumSpanningTree,
153
- getPageRank, getLabelPropagationCommunities,
154
- genGirvanNewmanCommunities, getBridges, isIsomorphic,
176
+ bfs,
177
+ dfs,
178
+ hasPath,
179
+ isAcyclic,
180
+ getShortestPath,
181
+ getCycles,
182
+ getTopologicalSort,
183
+ getConnectedComponents,
184
+ getMinimumSpanningTree,
185
+ getPageRank,
186
+ getLabelPropagationCommunities,
187
+ genGirvanNewmanCommunities,
188
+ getBridges,
189
+ isIsomorphic,
155
190
  } from '@statelyai/graph';
156
191
 
157
- for (const node of bfs(graph, 'a')) { /* breadth-first */ }
158
- for (const node of dfs(graph, 'a')) { /* depth-first */ }
159
-
160
- hasPath(graph, 'a', 'c'); // reachability
161
- isAcyclic(graph); // cycle check
162
- getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
163
- getTopologicalSort(graph); // topological order (or null)
164
- getConnectedComponents(graph); // connected components
165
- getMinimumSpanningTree(graph, { weight: e => e.data?.weight ?? 1 }); // MST
166
- getPageRank(graph); // link analysis scores
167
- getLabelPropagationCommunities(graph); // community detection
168
- [...genGirvanNewmanCommunities(graph)]; // lazy community splits
169
- getBridges(graph); // bridge edges
170
- isIsomorphic(graph, otherGraph); // structural equivalence
192
+ for (const node of bfs(graph, 'a')) {
193
+ /* breadth-first */
194
+ }
195
+ for (const node of dfs(graph, 'a')) {
196
+ /* depth-first */
197
+ }
198
+
199
+ hasPath(graph, 'a', 'c'); // reachability
200
+ isAcyclic(graph); // cycle check
201
+ getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
202
+ getTopologicalSort(graph); // topological order (or null)
203
+ getConnectedComponents(graph); // connected components
204
+ getMinimumSpanningTree(graph, { weight: (e) => e.data?.weight ?? 1 }); // MST
205
+ getPageRank(graph); // link analysis scores
206
+ getLabelPropagationCommunities(graph); // community detection
207
+ [...genGirvanNewmanCommunities(graph)]; // lazy community splits
208
+ getBridges(graph); // bridge edges
209
+ isIsomorphic(graph, otherGraph); // structural equivalence
171
210
  ```
172
211
 
173
212
  ## Diff & Walks
@@ -205,10 +244,10 @@ import { fromGEXF } from '@statelyai/graph/gexf';
205
244
  import { toCytoscapeJSON } from '@statelyai/graph/cytoscape';
206
245
  import { toD3Graph } from '@statelyai/graph/d3';
207
246
 
208
- const dot = toDOT(graph); // Graphviz DOT
209
- const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON
210
- const d3Data = toD3Graph(graph); // D3.js { nodes, links }
211
- const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
247
+ const dot = toDOT(graph); // Graphviz DOT
248
+ const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON
249
+ const d3Data = toD3Graph(graph); // D3.js { nodes, links }
250
+ const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
212
251
  ```
213
252
 
214
253
  <!-- supported format adapters derived from src/formats/* subdirectories -->
@@ -224,6 +263,38 @@ const cyto = cytoscapeConverter.to(graph);
224
263
  const back = cytoscapeConverter.from(cyto);
225
264
  ```
226
265
 
266
+ Round-trip fidelity may use adapter-specific graph, node, and edge `data`
267
+ metadata when the target format does not have a native field for a source
268
+ concept. A `partial` round-trip entry means the adapter still drops meaningful
269
+ source information instead of preserving it as metadata.
270
+
271
+ ## Format Support
272
+
273
+ <!-- format support matrix derived from src/formats/support.ts -->
274
+
275
+ | Format | Hierarchy | Ports | Visual | Round-trip | Notes |
276
+ | ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------- |
277
+ | `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
278
+ | `cytoscape` | full | full | full | full | Graph, node, and edge metadata round-trip through element data. |
279
+ | `d3` | full | full | full | full | Graph, node, and edge metadata round-trip through the loose JSON shape. |
280
+ | `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
281
+ | `edge-list` | none | none | none | partial | Endpoints only. |
282
+ | `elk` | full | full | full | full | Metadata round-trips through reserved layout options. |
283
+ | `gexf` | full | full | full | full | Custom attributes preserve metadata beyond the standard viz module. |
284
+ | `gml` | full | full | full | full | Graph, node, and edge metadata round-trip through direct and JSON fields. |
285
+ | `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
286
+ | `jgf` | full | full | full | full | Graph, node, and edge metadata round-trip through `metadata` objects. |
287
+ | `tgf` | none | none | none | partial | Minimal ids and labels only. |
288
+ | `xyflow` | full | full | full | full | Metadata round-trips through reserved data fields. |
289
+ | `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
290
+ | `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
291
+ | `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
292
+ | `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
293
+ | `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
294
+ | `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
295
+ | `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
296
+ | `mermaid/state` | full | none | partial | full | State syntax round-trips through graph and node data. |
297
+
227
298
  Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
228
299
 
229
300
  Format-specific docs live alongside the source:
@@ -261,13 +332,14 @@ The repo includes runnable examples under [`examples/`](./examples):
261
332
  ```bash
262
333
  pnpm install
263
334
  pnpm verify
335
+ pnpm bench
264
336
  ```
265
337
 
266
338
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for contributor conventions, format-module checklist, and release notes guidance.
267
339
 
268
340
  ## Why this library?
269
341
 
270
- Graph file formats define how to _store_ graphs. Visualization libraries define how to _render_ them. This library is the computational layer in between: plain JSON objects in, algorithms and mutations, plain JSON objects out.
342
+ Graph file formats define how to _store_ graphs. Visualization libraries define how to _render_ them. This library is the trusted interchange and analysis layer in between: plain JSON objects in, validation, algorithms, transforms, diffing, and format-preserving conversion out.
271
343
 
272
344
  ```
273
345
  GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSON() → render
@@ -275,6 +347,8 @@ GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSO
275
347
 
276
348
  Your `Graph` is a plain object that survives `JSON.stringify`, `structuredClone`, `postMessage`, and `localStorage` without adapters.
277
349
 
350
+ A canonical graph is a deterministic projection of a graph for comparison, hashing, snapshots, or caches. A future pure helper would return a new graph with stable node/edge ordering and normalized optional fields. A hash would be a digest of that canonical JSON. A summary would be a small structural report, for example node count, edge count, roots, sinks, component count, compound depth, port count, and whether the graph is acyclic. A pure `sortGraph()` would return a sorted copy and never mutate the input.
351
+
278
352
  ## License
279
353
 
280
354
  MIT
@@ -1,4 +1,4 @@
1
- import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-CJc-ul8e.mjs";
1
+ import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-DUl3kTqm.mjs";
2
2
 
3
3
  //#region src/graph.ts
4
4
  /**
@@ -7,7 +7,7 @@ import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdg
7
7
  * @example
8
8
  * ```ts
9
9
  * const port = createGraphPort({ name: 'output', direction: 'out' });
10
- * // { name: 'output', direction: 'out', data: undefined }
10
+ * // { name: 'output', direction: 'out', data: null }
11
11
  * ```
12
12
  */
13
13
  function createGraphPort(config) {
@@ -15,7 +15,7 @@ function createGraphPort(config) {
15
15
  const port = {
16
16
  name: config.name,
17
17
  direction: config.direction ?? "inout",
18
- data: config.data
18
+ data: config.data ?? null
19
19
  };
20
20
  if (config.label !== void 0) port.label = config.label;
21
21
  if (config.x !== void 0) port.x = config.x;
@@ -50,7 +50,7 @@ function createGraphNode(config) {
50
50
  ...config.parentId !== void 0 && { parentId: config.parentId ?? null },
51
51
  ...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
52
52
  label: config.label ?? null,
53
- data: config.data
53
+ data: config.data ?? null
54
54
  };
55
55
  if (config.ports !== void 0 && config.ports.length > 0) {
56
56
  validatePortNames(config.ports);
@@ -71,7 +71,7 @@ function createGraphNode(config) {
71
71
  * @example
72
72
  * ```ts
73
73
  * const edge = createGraphEdge({ id: 'e1', sourceId: 'a', targetId: 'b' });
74
- * // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: undefined }
74
+ * // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: null }
75
75
  * ```
76
76
  */
77
77
  function createGraphEdge(config) {
@@ -84,7 +84,7 @@ function createGraphEdge(config) {
84
84
  sourceId: config.sourceId,
85
85
  targetId: config.targetId,
86
86
  label: config.label ?? null,
87
- data: config.data
87
+ data: config.data ?? null
88
88
  };
89
89
  if (config.sourcePort !== void 0) edge.sourcePort = config.sourcePort;
90
90
  if (config.targetPort !== void 0) edge.targetPort = config.targetPort;
@@ -115,7 +115,7 @@ function createGraph(config) {
115
115
  initialNodeId: config?.initialNodeId ?? null,
116
116
  nodes: (config?.nodes ?? []).map(createGraphNode),
117
117
  edges: (config?.edges ?? []).map(createGraphEdge),
118
- data: config?.data ?? void 0
118
+ data: config?.data ?? null
119
119
  };
120
120
  if (config?.direction !== void 0) graph.direction = config.direction;
121
121
  if (config?.style !== void 0) graph.style = config.style;
@@ -1,3 +1,3 @@
1
- import { A as isAcyclic, B as getShortestPaths, C as getPreorder, D as getConnectedComponents, E as dfs, F as genSimplePaths, H as getSimplePaths, I as getAStarPath, L as getAllPairsShortestPaths, M as isTree, N as genCycles, O as getTopologicalSort, P as genShortestPaths, R as getCycles, S as getPostorders, T as bfs, U as getStronglyConnectedComponents, V as getSimplePath, W as joinPaths, _ as getPageRank, a as genGirvanNewmanCommunities, b as genPreorders, c as getLabelPropagationCommunities, d as getClosenessCentrality, f as getDegreeCentrality, g as getOutDegreeCentrality, h as getInDegreeCentrality, i as getBridges, j as isConnected, k as hasPath, l as getModularity, m as getHITS, n as getArticulationPoints, o as getGirvanNewmanCommunities, p as getEigenvectorCentrality, r as getBiconnectedComponents, s as getGreedyModularityCommunities, t as isIsomorphic, u as getBetweennessCentrality, v as getMinimumSpanningTree, w as getPreorders, x as getPostorder, y as genPostorders, z as getShortestPath } from "./algorithms-BHHg7lGq.mjs";
1
+ import { A as isAcyclic, B as getShortestPaths, C as getPreorder, D as getConnectedComponents, E as dfs, F as genSimplePaths, H as getSimplePaths, I as getAStarPath, L as getAllPairsShortestPaths, M as isTree, N as genCycles, O as getTopologicalSort, P as genShortestPaths, R as getCycles, S as getPostorders, T as bfs, U as getStronglyConnectedComponents, V as getSimplePath, W as joinPaths, _ as getPageRank, a as genGirvanNewmanCommunities, b as genPreorders, c as getLabelPropagationCommunities, d as getClosenessCentrality, f as getDegreeCentrality, g as getOutDegreeCentrality, h as getInDegreeCentrality, i as getBridges, j as isConnected, k as hasPath, l as getModularity, m as getHITS, n as getArticulationPoints, o as getGirvanNewmanCommunities, p as getEigenvectorCentrality, r as getBiconnectedComponents, s as getGreedyModularityCommunities, t as isIsomorphic, u as getBetweennessCentrality, v as getMinimumSpanningTree, w as getPreorders, x as getPostorder, y as genPostorders, z as getShortestPath } from "./algorithms-BNDQcHU3.mjs";
2
2
 
3
3
  export { bfs, dfs, genCycles, genGirvanNewmanCommunities, genPostorders, genPreorders, genShortestPaths, genSimplePaths, getAStarPath, getAllPairsShortestPaths, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getClosenessCentrality, getConnectedComponents, getCycles, getDegreeCentrality, getEigenvectorCentrality, getGirvanNewmanCommunities, getGreedyModularityCommunities, getHITS, getInDegreeCentrality, getLabelPropagationCommunities, getMinimumSpanningTree, getModularity, getOutDegreeCentrality, getPageRank, getPostorder, getPostorders, getPreorder, getPreorders, getShortestPath, getShortestPaths, getSimplePath, getSimplePaths, getStronglyConnectedComponents, getTopologicalSort, hasPath, isAcyclic, isConnected, isIsomorphic, isTree, joinPaths };
@@ -16,6 +16,12 @@ interface FormatSupportEntry {
16
16
  features: FormatSupportFeatures;
17
17
  notes: string[];
18
18
  }
19
+ /**
20
+ * Round-trip support is allowed to use adapter-specific graph, node, and edge
21
+ * `data` metadata when the target format has no native field for a source
22
+ * concept. A `partial` value means the adapter still drops meaningful source
23
+ * information instead of preserving it as metadata.
24
+ */
19
25
  declare const FORMAT_SUPPORT_MATRIX: FormatSupportEntry[];
20
26
  declare function getFormatSupportEntry(id: string): FormatSupportEntry | undefined;
21
27
  //#endregion
@@ -1,4 +1,10 @@
1
1
  //#region src/formats/support.ts
2
+ /**
3
+ * Round-trip support is allowed to use adapter-specific graph, node, and edge
4
+ * `data` metadata when the target format has no native field for a source
5
+ * concept. A `partial` value means the adapter still drops meaningful source
6
+ * information instead of preserving it as metadata.
7
+ */
2
8
  const FORMAT_SUPPORT_MATRIX = [
3
9
  {
4
10
  id: "adjacency-list",
@@ -21,14 +27,14 @@ const FORMAT_SUPPORT_MATRIX = [
21
27
  features: {
22
28
  directed: "full",
23
29
  undirected: "full",
24
- hierarchy: "partial",
25
- ports: "none",
26
- visual: "partial",
27
- style: "partial",
30
+ hierarchy: "full",
31
+ ports: "full",
32
+ visual: "full",
33
+ style: "full",
28
34
  weight: "full",
29
- roundTrip: "partial"
35
+ roundTrip: "full"
30
36
  },
31
- notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity."]
37
+ notes: ["Uses Cytoscape JSON element data with graph, node, and edge metadata stored in element data.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
32
38
  },
33
39
  {
34
40
  id: "d3",
@@ -36,14 +42,14 @@ const FORMAT_SUPPORT_MATRIX = [
36
42
  features: {
37
43
  directed: "full",
38
44
  undirected: "full",
39
- hierarchy: "none",
40
- ports: "none",
41
- visual: "partial",
42
- style: "none",
45
+ hierarchy: "full",
46
+ ports: "full",
47
+ visual: "full",
48
+ style: "full",
43
49
  weight: "full",
44
- roundTrip: "partial"
50
+ roundTrip: "full"
45
51
  },
46
- notes: ["Targets force-graph structures, not compound graph metadata."]
52
+ notes: ["Targets force-graph structures, but graph, node, and edge metadata can be preserved on the loose JSON shape.", "Ports round-trip through node/link objects."]
47
53
  },
48
54
  {
49
55
  id: "dot",
@@ -58,7 +64,7 @@ const FORMAT_SUPPORT_MATRIX = [
58
64
  weight: "none",
59
65
  roundTrip: "partial"
60
66
  },
61
- notes: ["Port syntax (`:port:compass`) is not fully mapped.", "HTML labels and layout hints beyond `rankdir` are lossy."]
67
+ notes: ["Edge port ids round-trip, but compass points and node port definitions are not mapped.", "HTML labels and layout hints beyond `rankdir` are lossy."]
62
68
  },
63
69
  {
64
70
  id: "edge-list",
@@ -84,11 +90,11 @@ const FORMAT_SUPPORT_MATRIX = [
84
90
  hierarchy: "full",
85
91
  ports: "full",
86
92
  visual: "full",
87
- style: "partial",
88
- weight: "none",
89
- roundTrip: "partial"
93
+ style: "full",
94
+ weight: "full",
95
+ roundTrip: "full"
90
96
  },
91
- notes: ["Optimized for layout exchange with ELK rather than exact styling parity."]
97
+ notes: ["ELK-native layout fields are preserved directly; graph, node, port, and edge metadata round-trip through reserved layout options."]
92
98
  },
93
99
  {
94
100
  id: "gexf",
@@ -96,14 +102,14 @@ const FORMAT_SUPPORT_MATRIX = [
96
102
  features: {
97
103
  directed: "full",
98
104
  undirected: "full",
99
- hierarchy: "none",
100
- ports: "none",
101
- visual: "partial",
102
- style: "partial",
105
+ hierarchy: "full",
106
+ ports: "full",
107
+ visual: "full",
108
+ style: "full",
103
109
  weight: "full",
104
- roundTrip: "partial"
110
+ roundTrip: "full"
105
111
  },
106
- notes: ["Attribute and viz extensions round-trip partially."]
112
+ notes: ["Custom attributes preserve graph, node, and edge metadata beyond the standard viz module.", "Ports round-trip via custom node/edge attributes."]
107
113
  },
108
114
  {
109
115
  id: "gml",
@@ -111,14 +117,14 @@ const FORMAT_SUPPORT_MATRIX = [
111
117
  features: {
112
118
  directed: "full",
113
119
  undirected: "full",
114
- hierarchy: "none",
115
- ports: "none",
116
- visual: "partial",
117
- style: "partial",
120
+ hierarchy: "full",
121
+ ports: "full",
122
+ visual: "full",
123
+ style: "full",
118
124
  weight: "full",
119
- roundTrip: "partial"
125
+ roundTrip: "full"
120
126
  },
121
- notes: ["GML support focuses on common graph/node/edge attributes."]
127
+ notes: ["GML stores graph, node, and edge metadata directly or as JSON-stringified fields.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
122
128
  },
123
129
  {
124
130
  id: "graphml",
@@ -126,14 +132,14 @@ const FORMAT_SUPPORT_MATRIX = [
126
132
  features: {
127
133
  directed: "full",
128
134
  undirected: "full",
129
- hierarchy: "none",
130
- ports: "none",
135
+ hierarchy: "full",
136
+ ports: "full",
131
137
  visual: "partial",
132
138
  style: "partial",
133
139
  weight: "full",
134
140
  roundTrip: "partial"
135
141
  },
136
- notes: ["GraphML attribute fidelity is good, but not every extension is represented."]
142
+ notes: ["GraphML attribute fidelity is good, but not every extension is represented.", "Ports round-trip through node and edge `<data>` fields."]
137
143
  },
138
144
  {
139
145
  id: "jgf",
@@ -141,14 +147,14 @@ const FORMAT_SUPPORT_MATRIX = [
141
147
  features: {
142
148
  directed: "full",
143
149
  undirected: "full",
144
- hierarchy: "none",
145
- ports: "none",
146
- visual: "none",
147
- style: "none",
150
+ hierarchy: "full",
151
+ ports: "full",
152
+ visual: "full",
153
+ style: "full",
148
154
  weight: "full",
149
- roundTrip: "partial"
155
+ roundTrip: "full"
150
156
  },
151
- notes: ["JGF preserves core graph structure and data, but not layout primitives."]
157
+ notes: ["JGF preserves graph, node, and edge metadata via `metadata` objects.", "Ports round-trip through node and edge metadata."]
152
158
  },
153
159
  {
154
160
  id: "tgf",
@@ -171,14 +177,14 @@ const FORMAT_SUPPORT_MATRIX = [
171
177
  features: {
172
178
  directed: "full",
173
179
  undirected: "partial",
174
- hierarchy: "none",
180
+ hierarchy: "full",
175
181
  ports: "full",
176
182
  visual: "full",
177
- style: "partial",
178
- weight: "none",
179
- roundTrip: "partial"
183
+ style: "full",
184
+ weight: "full",
185
+ roundTrip: "full"
180
186
  },
181
- notes: ["Ports map cleanly to handles, but styling remains adapter-specific."]
187
+ notes: ["xyflow-native fields are preserved directly; graph, node, edge, style, weight, and port metadata round-trip through reserved data fields."]
182
188
  },
183
189
  {
184
190
  id: "mermaid/block",
@@ -296,9 +302,9 @@ const FORMAT_SUPPORT_MATRIX = [
296
302
  visual: "partial",
297
303
  style: "partial",
298
304
  weight: "none",
299
- roundTrip: "partial"
305
+ roundTrip: "full"
300
306
  },
301
- notes: ["State notes are stored, but not fully round-trippable as separate graph entities."]
307
+ notes: ["State-specific syntax such as notes, classes, descriptions, directions, hierarchy, and parallel regions round-trips through node and graph data."]
302
308
  }
303
309
  ];
304
310
  function getFormatSupportEntry(id) {
@@ -25,6 +25,7 @@ function toCytoscapeJSON(graph) {
25
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
+ if (graph.style !== void 0) graphData.style = graph.style;
28
29
  return {
29
30
  ...Object.keys(graphData).length > 0 && { data: graphData },
30
31
  elements: {
@@ -38,6 +39,8 @@ function toCytoscapeJSON(graph) {
38
39
  if (n.height !== void 0) data.height = n.height;
39
40
  if (n.shape) data.shape = n.shape;
40
41
  if (n.color) data.color = n.color;
42
+ if (n.style !== void 0) data.style = n.style;
43
+ if (n.ports !== void 0) data.ports = n.ports;
41
44
  const node = { data };
42
45
  if (n.x !== void 0 && n.y !== void 0) node.position = {
43
46
  x: n.x,
@@ -53,7 +56,15 @@ function toCytoscapeJSON(graph) {
53
56
  };
54
57
  if (e.label) data.label = e.label;
55
58
  if (e.data !== void 0) data.edgeData = e.data;
59
+ if (e.weight !== void 0) data.weight = e.weight;
60
+ if (e.x !== void 0) data.x = e.x;
61
+ if (e.y !== void 0) data.y = e.y;
62
+ if (e.width !== void 0) data.width = e.width;
63
+ if (e.height !== void 0) data.height = e.height;
56
64
  if (e.color) data.color = e.color;
65
+ if (e.style !== void 0) data.style = e.style;
66
+ if (e.sourcePort !== void 0) data.sourcePort = e.sourcePort;
67
+ if (e.targetPort !== void 0) data.targetPort = e.targetPort;
57
68
  return { data };
58
69
  })
59
70
  }
@@ -85,6 +96,7 @@ function fromCytoscapeJSON(cyto) {
85
96
  initialNodeId: cyto.data?.initialNodeId ?? null,
86
97
  data: cyto.data?.graphData,
87
98
  ...cyto.data?.direction && { direction: cyto.data.direction },
99
+ ...cyto.data?.style !== void 0 && { style: cyto.data.style },
88
100
  nodes: cyto.elements.nodes.map((n) => ({
89
101
  type: "node",
90
102
  id: n.data.id,
@@ -99,7 +111,9 @@ function fromCytoscapeJSON(cyto) {
99
111
  ...n.data.width !== void 0 && { width: n.data.width },
100
112
  ...n.data.height !== void 0 && { height: n.data.height },
101
113
  ...n.data.shape && { shape: n.data.shape },
102
- ...n.data.color && { color: n.data.color }
114
+ ...n.data.color && { color: n.data.color },
115
+ ...n.data.style !== void 0 && { style: n.data.style },
116
+ ...n.data.ports !== void 0 && { ports: n.data.ports }
103
117
  })),
104
118
  edges: cyto.elements.edges.map((e, i) => ({
105
119
  type: "edge",
@@ -108,7 +122,15 @@ function fromCytoscapeJSON(cyto) {
108
122
  targetId: e.data.target,
109
123
  label: e.data.label ?? "",
110
124
  data: e.data.edgeData,
111
- ...e.data.color && { color: e.data.color }
125
+ ...e.data.weight !== void 0 && { weight: e.data.weight },
126
+ ...e.data.x !== void 0 && { x: e.data.x },
127
+ ...e.data.y !== void 0 && { y: e.data.y },
128
+ ...e.data.width !== void 0 && { width: e.data.width },
129
+ ...e.data.height !== void 0 && { height: e.data.height },
130
+ ...e.data.color && { color: e.data.color },
131
+ ...e.data.style !== void 0 && { style: e.data.style },
132
+ ...e.data.sourcePort !== void 0 && { sourcePort: e.data.sourcePort },
133
+ ...e.data.targetPort !== void 0 && { targetPort: e.data.targetPort }
112
134
  }))
113
135
  };
114
136
  }
@@ -11,8 +11,15 @@ interface D3Link {
11
11
  [key: string]: any;
12
12
  }
13
13
  interface D3Graph {
14
+ id?: string;
15
+ type?: Graph['type'];
16
+ initialNodeId?: string | null;
17
+ data?: any;
18
+ direction?: Graph['direction'];
19
+ style?: Graph['style'];
14
20
  nodes: D3Node[];
15
21
  links: D3Link[];
22
+ [key: string]: any;
16
23
  }
17
24
  /**
18
25
  * Converts a graph to D3.js force-directed format.