@statelyai/graph 0.11.0 → 0.12.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 +130 -44
- package/dist/format-support.mjs +33 -18
- package/dist/formats/cytoscape/index.mjs +8 -2
- package/dist/formats/d3/index.mjs +8 -2
- package/dist/formats/dot/index.mjs +18 -6
- package/dist/formats/gexf/index.mjs +46 -17
- package/dist/formats/gml/index.mjs +6 -0
- package/dist/formats/graphml/index.mjs +33 -0
- package/dist/formats/jgf/index.mjs +8 -2
- package/dist/formats/mermaid/index.d.mts +46 -2
- package/dist/formats/mermaid/index.mjs +233 -28
- package/dist/schemas.d.mts +17 -3
- package/dist/schemas.mjs +35 -2
- package/package.json +2 -1
- package/schemas/graph.schema.json +8 -1
- package/schemas/node.schema.json +8 -1
package/README.md
CHANGED
|
@@ -12,14 +12,16 @@ npm install @statelyai/graph
|
|
|
12
12
|
|
|
13
13
|
Optional peers are only needed for specific adapters:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
<!-- optional peer dependencies derived from package.json#peerDependencies -->
|
|
16
|
+
|
|
17
|
+
| Package | Needed for |
|
|
18
|
+
| ----------------- | --------------------------------------------------- |
|
|
17
19
|
| `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
|
|
18
|
-
| `dotparser`
|
|
19
|
-
| `cytoscape`
|
|
20
|
-
| `d3-force`
|
|
21
|
-
| `elkjs`
|
|
22
|
-
| `zod`
|
|
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` |
|
|
23
25
|
|
|
24
26
|
## Highlights
|
|
25
27
|
|
|
@@ -37,7 +39,12 @@ Optional peers are only needed for specific adapters:
|
|
|
37
39
|
Graphs are plain JSON-serializable objects. All operations are standalone functions — no classes, no DOM, no rendering engine.
|
|
38
40
|
|
|
39
41
|
```ts
|
|
40
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
createGraph,
|
|
44
|
+
addNode,
|
|
45
|
+
addEdge,
|
|
46
|
+
getShortestPath,
|
|
47
|
+
} from '@statelyai/graph';
|
|
41
48
|
|
|
42
49
|
const graph = createGraph({
|
|
43
50
|
nodes: [
|
|
@@ -64,12 +71,17 @@ const path = getShortestPath(graph, { from: 'a', to: 'c' });
|
|
|
64
71
|
Look up, add, delete, and update nodes and edges. Query neighbors, predecessors, successors, degree, and more.
|
|
65
72
|
|
|
66
73
|
```ts
|
|
67
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
getNode,
|
|
76
|
+
deleteNode,
|
|
77
|
+
getNeighbors,
|
|
78
|
+
getSources,
|
|
79
|
+
} from '@statelyai/graph';
|
|
68
80
|
|
|
69
|
-
const node = getNode(graph, 'a');
|
|
70
|
-
deleteNode(graph, 'd');
|
|
81
|
+
const node = getNode(graph, 'a'); // lookup by id
|
|
82
|
+
deleteNode(graph, 'd'); // removes node + connected edges
|
|
71
83
|
const neighbors = getNeighbors(graph, 'a'); // adjacent nodes
|
|
72
|
-
const roots = getSources(graph);
|
|
84
|
+
const roots = getSources(graph); // nodes with no incoming edges
|
|
73
85
|
```
|
|
74
86
|
|
|
75
87
|
Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
|
|
@@ -90,14 +102,14 @@ const graph = createGraph({
|
|
|
90
102
|
{ id: 'c' },
|
|
91
103
|
],
|
|
92
104
|
edges: [
|
|
93
|
-
{ id: 'e1', sourceId: 'a', targetId: 'b' },
|
|
105
|
+
{ id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1
|
|
94
106
|
{ id: 'e2', sourceId: 'b1', targetId: 'b2' },
|
|
95
|
-
{ id: 'e3', sourceId: 'b', targetId: 'c' },
|
|
107
|
+
{ id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b
|
|
96
108
|
],
|
|
97
109
|
});
|
|
98
110
|
|
|
99
111
|
const children = getChildren(graph, 'b'); // [b1, b2]
|
|
100
|
-
const flat = flatten(graph);
|
|
112
|
+
const flat = flatten(graph); // only leaf nodes, edges resolved
|
|
101
113
|
```
|
|
102
114
|
|
|
103
115
|
## Ports
|
|
@@ -133,33 +145,68 @@ getPorts(graph, 'fetch'); // [{ name: 'result', ... }]
|
|
|
133
145
|
getEdgesByPort(graph, 'render', 'input'); // [e1]
|
|
134
146
|
```
|
|
135
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.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { GraphSchema, getGraphIssues, isGraph } 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(getGraphIssues(unknownValue));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const parsed = GraphSchema.parse(unknownValue);
|
|
166
|
+
```
|
|
167
|
+
|
|
136
168
|
## Algorithms
|
|
137
169
|
|
|
138
|
-
|
|
170
|
+
<!-- algorithm functions exported from src/algorithms.ts -->
|
|
171
|
+
|
|
172
|
+
Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, simple paths, all-pairs shortest paths, A*), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector), community detection (label propagation, Girvan-Newman, greedy modularity, modularity scoring), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, isomorphism, topological sort, minimum spanning tree, and more. Many algorithms have lazy generator variants (`gen*`) for early exit.
|
|
139
173
|
|
|
140
174
|
```ts
|
|
141
175
|
import {
|
|
142
|
-
bfs,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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,
|
|
147
190
|
} from '@statelyai/graph';
|
|
148
191
|
|
|
149
|
-
for (const node of bfs(graph, 'a')) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
163
210
|
```
|
|
164
211
|
|
|
165
212
|
## Diff & Walks
|
|
@@ -197,13 +244,15 @@ import { fromGEXF } from '@statelyai/graph/gexf';
|
|
|
197
244
|
import { toCytoscapeJSON } from '@statelyai/graph/cytoscape';
|
|
198
245
|
import { toD3Graph } from '@statelyai/graph/d3';
|
|
199
246
|
|
|
200
|
-
const dot = toDOT(graph);
|
|
201
|
-
const cytoData = toCytoscapeJSON(graph);
|
|
202
|
-
const d3Data = toD3Graph(graph);
|
|
203
|
-
const imported = fromGEXF(gexfXmlString);
|
|
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)
|
|
204
251
|
```
|
|
205
252
|
|
|
206
|
-
|
|
253
|
+
<!-- supported format adapters derived from src/formats/* subdirectories -->
|
|
254
|
+
|
|
255
|
+
**Supported formats:** Cytoscape.js JSON, D3.js JSON, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block, Ishikawa), ELK, xyflow, adjacency list, and edge list.
|
|
207
256
|
|
|
208
257
|
Each bidirectional format also has a converter object:
|
|
209
258
|
|
|
@@ -214,23 +263,58 @@ const cyto = cytoscapeConverter.to(graph);
|
|
|
214
263
|
const back = cytoscapeConverter.from(cyto);
|
|
215
264
|
```
|
|
216
265
|
|
|
266
|
+
## Format Support
|
|
267
|
+
|
|
268
|
+
<!-- format support matrix derived from src/formats/support.ts -->
|
|
269
|
+
|
|
270
|
+
| Format | Hierarchy | Ports | Visual | Round-trip | Notes |
|
|
271
|
+
| ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------- |
|
|
272
|
+
| `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
|
|
273
|
+
| `cytoscape` | full | full | partial | partial | Ports round-trip through element data. |
|
|
274
|
+
| `d3` | none | full | partial | partial | Ports round-trip through node/link objects. |
|
|
275
|
+
| `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
|
|
276
|
+
| `edge-list` | none | none | none | partial | Endpoints only. |
|
|
277
|
+
| `elk` | full | full | full | partial | Best for layout exchange. |
|
|
278
|
+
| `gexf` | full | full | partial | partial | Ports round-trip via custom attributes. |
|
|
279
|
+
| `gml` | full | full | partial | partial | Ports round-trip through JSON-stringified metadata. |
|
|
280
|
+
| `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
|
|
281
|
+
| `jgf` | full | full | none | partial | Ports round-trip through metadata. |
|
|
282
|
+
| `tgf` | none | none | none | partial | Minimal ids and labels only. |
|
|
283
|
+
| `xyflow` | none | full | full | partial | Ports map directly to handles. |
|
|
284
|
+
| `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
|
|
285
|
+
| `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
|
|
286
|
+
| `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
|
|
287
|
+
| `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
|
|
288
|
+
| `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
|
|
289
|
+
| `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
|
|
290
|
+
| `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
|
|
291
|
+
| `mermaid/state` | full | none | partial | partial | State notes are still lossy. |
|
|
292
|
+
|
|
217
293
|
Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
|
|
218
294
|
|
|
219
295
|
Format-specific docs live alongside the source:
|
|
220
296
|
|
|
297
|
+
<!-- format README files under src/formats/*/README.md -->
|
|
298
|
+
|
|
299
|
+
- [Adjacency list](./src/formats/adjacency-list/README.md)
|
|
300
|
+
- [Cytoscape](./src/formats/cytoscape/README.md)
|
|
301
|
+
- [D3](./src/formats/d3/README.md)
|
|
221
302
|
- [DOT](./src/formats/dot/README.md)
|
|
222
|
-
- [
|
|
303
|
+
- [Edge list](./src/formats/edge-list/README.md)
|
|
304
|
+
- [ELK](./src/formats/elk/README.md)
|
|
223
305
|
- [GEXF](./src/formats/gexf/README.md)
|
|
224
306
|
- [GML](./src/formats/gml/README.md)
|
|
307
|
+
- [GraphML](./src/formats/graphml/README.md)
|
|
225
308
|
- [JGF](./src/formats/jgf/README.md)
|
|
226
|
-
- [TGF](./src/formats/tgf/README.md)
|
|
227
|
-
- [Cytoscape](./src/formats/cytoscape/README.md)
|
|
228
|
-
- [D3](./src/formats/d3/README.md)
|
|
229
309
|
- [Mermaid](./src/formats/mermaid/README.md)
|
|
310
|
+
- [TGF](./src/formats/tgf/README.md)
|
|
311
|
+
- [xyflow](./src/formats/xyflow/README.md)
|
|
230
312
|
- [Converter helpers](./src/formats/converter/README.md)
|
|
231
313
|
|
|
232
314
|
## Examples
|
|
233
315
|
|
|
316
|
+
<!-- runnable example files under examples/ -->
|
|
317
|
+
|
|
234
318
|
The repo includes runnable examples under [`examples/`](./examples):
|
|
235
319
|
|
|
236
320
|
- [Flow-based math](./examples/flow-based-math.ts) shows ports, topological ordering, and value propagation.
|
|
@@ -238,6 +322,8 @@ The repo includes runnable examples under [`examples/`](./examples):
|
|
|
238
322
|
|
|
239
323
|
## Development
|
|
240
324
|
|
|
325
|
+
<!-- dev commands from package.json#scripts -->
|
|
326
|
+
|
|
241
327
|
```bash
|
|
242
328
|
pnpm install
|
|
243
329
|
pnpm verify
|
package/dist/format-support.mjs
CHANGED
|
@@ -21,14 +21,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
21
21
|
features: {
|
|
22
22
|
directed: "full",
|
|
23
23
|
undirected: "full",
|
|
24
|
-
hierarchy: "
|
|
25
|
-
ports: "
|
|
24
|
+
hierarchy: "full",
|
|
25
|
+
ports: "full",
|
|
26
26
|
visual: "partial",
|
|
27
27
|
style: "partial",
|
|
28
28
|
weight: "full",
|
|
29
29
|
roundTrip: "partial"
|
|
30
30
|
},
|
|
31
|
-
notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity."]
|
|
31
|
+
notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: "d3",
|
|
@@ -37,13 +37,13 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
37
37
|
directed: "full",
|
|
38
38
|
undirected: "full",
|
|
39
39
|
hierarchy: "none",
|
|
40
|
-
ports: "
|
|
40
|
+
ports: "full",
|
|
41
41
|
visual: "partial",
|
|
42
42
|
style: "none",
|
|
43
43
|
weight: "full",
|
|
44
44
|
roundTrip: "partial"
|
|
45
45
|
},
|
|
46
|
-
notes: ["Targets force-graph structures, not compound graph metadata."]
|
|
46
|
+
notes: ["Targets force-graph structures, not compound graph metadata.", "Ports round-trip through node/link objects."]
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
id: "dot",
|
|
@@ -58,7 +58,7 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
58
58
|
weight: "none",
|
|
59
59
|
roundTrip: "partial"
|
|
60
60
|
},
|
|
61
|
-
notes: ["
|
|
61
|
+
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
62
|
},
|
|
63
63
|
{
|
|
64
64
|
id: "edge-list",
|
|
@@ -96,14 +96,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
96
96
|
features: {
|
|
97
97
|
directed: "full",
|
|
98
98
|
undirected: "full",
|
|
99
|
-
hierarchy: "
|
|
100
|
-
ports: "
|
|
99
|
+
hierarchy: "full",
|
|
100
|
+
ports: "full",
|
|
101
101
|
visual: "partial",
|
|
102
102
|
style: "partial",
|
|
103
103
|
weight: "full",
|
|
104
104
|
roundTrip: "partial"
|
|
105
105
|
},
|
|
106
|
-
notes: ["Attribute and viz extensions round-trip partially."]
|
|
106
|
+
notes: ["Attribute and viz extensions round-trip partially.", "Ports round-trip via custom node/edge attributes."]
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
id: "gml",
|
|
@@ -111,14 +111,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
111
111
|
features: {
|
|
112
112
|
directed: "full",
|
|
113
113
|
undirected: "full",
|
|
114
|
-
hierarchy: "
|
|
115
|
-
ports: "
|
|
114
|
+
hierarchy: "full",
|
|
115
|
+
ports: "full",
|
|
116
116
|
visual: "partial",
|
|
117
117
|
style: "partial",
|
|
118
118
|
weight: "full",
|
|
119
119
|
roundTrip: "partial"
|
|
120
120
|
},
|
|
121
|
-
notes: ["GML support focuses on common graph/node/edge attributes."]
|
|
121
|
+
notes: ["GML support focuses on common graph/node/edge attributes.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
id: "graphml",
|
|
@@ -126,14 +126,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
126
126
|
features: {
|
|
127
127
|
directed: "full",
|
|
128
128
|
undirected: "full",
|
|
129
|
-
hierarchy: "
|
|
130
|
-
ports: "
|
|
129
|
+
hierarchy: "full",
|
|
130
|
+
ports: "full",
|
|
131
131
|
visual: "partial",
|
|
132
132
|
style: "partial",
|
|
133
133
|
weight: "full",
|
|
134
134
|
roundTrip: "partial"
|
|
135
135
|
},
|
|
136
|
-
notes: ["GraphML attribute fidelity is good, but not every extension is represented."]
|
|
136
|
+
notes: ["GraphML attribute fidelity is good, but not every extension is represented.", "Ports round-trip through node and edge `<data>` fields."]
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
id: "jgf",
|
|
@@ -141,14 +141,14 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
141
141
|
features: {
|
|
142
142
|
directed: "full",
|
|
143
143
|
undirected: "full",
|
|
144
|
-
hierarchy: "
|
|
145
|
-
ports: "
|
|
144
|
+
hierarchy: "full",
|
|
145
|
+
ports: "full",
|
|
146
146
|
visual: "none",
|
|
147
147
|
style: "none",
|
|
148
148
|
weight: "full",
|
|
149
149
|
roundTrip: "partial"
|
|
150
150
|
},
|
|
151
|
-
notes: ["JGF preserves core graph structure and data, but not layout primitives."]
|
|
151
|
+
notes: ["JGF preserves core graph structure and data, but not layout primitives.", "Ports round-trip through node and edge metadata."]
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
id: "tgf",
|
|
@@ -240,6 +240,21 @@ const FORMAT_SUPPORT_MATRIX = [
|
|
|
240
240
|
},
|
|
241
241
|
notes: ["Index-based `linkStyle` metadata is fragile after graph mutation.", "Mermaid init directives are not fully preserved."]
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
id: "mermaid/ishikawa",
|
|
245
|
+
importPath: "@statelyai/graph/mermaid",
|
|
246
|
+
features: {
|
|
247
|
+
directed: "full",
|
|
248
|
+
undirected: "none",
|
|
249
|
+
hierarchy: "full",
|
|
250
|
+
ports: "none",
|
|
251
|
+
visual: "none",
|
|
252
|
+
style: "none",
|
|
253
|
+
weight: "none",
|
|
254
|
+
roundTrip: "partial"
|
|
255
|
+
},
|
|
256
|
+
notes: ["Indentation is preserved as hierarchy; renderer-specific fishbone layout is not represented."]
|
|
257
|
+
},
|
|
243
258
|
{
|
|
244
259
|
id: "mermaid/mindmap",
|
|
245
260
|
importPath: "@statelyai/graph/mermaid",
|
|
@@ -38,6 +38,7 @@ function toCytoscapeJSON(graph) {
|
|
|
38
38
|
if (n.height !== void 0) data.height = n.height;
|
|
39
39
|
if (n.shape) data.shape = n.shape;
|
|
40
40
|
if (n.color) data.color = n.color;
|
|
41
|
+
if (n.ports !== void 0) data.ports = n.ports;
|
|
41
42
|
const node = { data };
|
|
42
43
|
if (n.x !== void 0 && n.y !== void 0) node.position = {
|
|
43
44
|
x: n.x,
|
|
@@ -54,6 +55,8 @@ function toCytoscapeJSON(graph) {
|
|
|
54
55
|
if (e.label) data.label = e.label;
|
|
55
56
|
if (e.data !== void 0) data.edgeData = e.data;
|
|
56
57
|
if (e.color) data.color = e.color;
|
|
58
|
+
if (e.sourcePort !== void 0) data.sourcePort = e.sourcePort;
|
|
59
|
+
if (e.targetPort !== void 0) data.targetPort = e.targetPort;
|
|
57
60
|
return { data };
|
|
58
61
|
})
|
|
59
62
|
}
|
|
@@ -99,7 +102,8 @@ function fromCytoscapeJSON(cyto) {
|
|
|
99
102
|
...n.data.width !== void 0 && { width: n.data.width },
|
|
100
103
|
...n.data.height !== void 0 && { height: n.data.height },
|
|
101
104
|
...n.data.shape && { shape: n.data.shape },
|
|
102
|
-
...n.data.color && { color: n.data.color }
|
|
105
|
+
...n.data.color && { color: n.data.color },
|
|
106
|
+
...n.data.ports !== void 0 && { ports: n.data.ports }
|
|
103
107
|
})),
|
|
104
108
|
edges: cyto.elements.edges.map((e, i) => ({
|
|
105
109
|
type: "edge",
|
|
@@ -108,7 +112,9 @@ function fromCytoscapeJSON(cyto) {
|
|
|
108
112
|
targetId: e.data.target,
|
|
109
113
|
label: e.data.label ?? "",
|
|
110
114
|
data: e.data.edgeData,
|
|
111
|
-
...e.data.color && { color: e.data.color }
|
|
115
|
+
...e.data.color && { color: e.data.color },
|
|
116
|
+
...e.data.sourcePort !== void 0 && { sourcePort: e.data.sourcePort },
|
|
117
|
+
...e.data.targetPort !== void 0 && { targetPort: e.data.targetPort }
|
|
112
118
|
}))
|
|
113
119
|
};
|
|
114
120
|
}
|
|
@@ -28,6 +28,7 @@ function toD3Graph(graph) {
|
|
|
28
28
|
if (n.y !== void 0) node.y = n.y;
|
|
29
29
|
if (n.color) node.color = n.color;
|
|
30
30
|
if (n.shape) node.shape = n.shape;
|
|
31
|
+
if (n.ports !== void 0) node.ports = n.ports;
|
|
31
32
|
return node;
|
|
32
33
|
}),
|
|
33
34
|
links: graph.edges.map((e) => {
|
|
@@ -39,6 +40,8 @@ function toD3Graph(graph) {
|
|
|
39
40
|
if (e.label) link.label = e.label;
|
|
40
41
|
if (e.data !== void 0) link.data = e.data;
|
|
41
42
|
if (e.color) link.color = e.color;
|
|
43
|
+
if (e.sourcePort !== void 0) link.sourcePort = e.sourcePort;
|
|
44
|
+
if (e.targetPort !== void 0) link.targetPort = e.targetPort;
|
|
42
45
|
return link;
|
|
43
46
|
})
|
|
44
47
|
};
|
|
@@ -75,7 +78,8 @@ function fromD3Graph(d3) {
|
|
|
75
78
|
...n.x !== void 0 && { x: n.x },
|
|
76
79
|
...n.y !== void 0 && { y: n.y },
|
|
77
80
|
...n.color && { color: n.color },
|
|
78
|
-
...n.shape && { shape: n.shape }
|
|
81
|
+
...n.shape && { shape: n.shape },
|
|
82
|
+
...n.ports !== void 0 && { ports: n.ports }
|
|
79
83
|
})),
|
|
80
84
|
edges: d3.links.map((l, i) => ({
|
|
81
85
|
type: "edge",
|
|
@@ -84,7 +88,9 @@ function fromD3Graph(d3) {
|
|
|
84
88
|
targetId: typeof l.target === "string" ? l.target : l.target.id,
|
|
85
89
|
label: l.label ?? "",
|
|
86
90
|
data: l.data,
|
|
87
|
-
...l.color && { color: l.color }
|
|
91
|
+
...l.color && { color: l.color },
|
|
92
|
+
...l.sourcePort !== void 0 && { sourcePort: l.sourcePort },
|
|
93
|
+
...l.targetPort !== void 0 && { targetPort: l.targetPort }
|
|
88
94
|
}))
|
|
89
95
|
};
|
|
90
96
|
}
|
|
@@ -11,6 +11,9 @@ function escapeId(id) {
|
|
|
11
11
|
function escapeLabel(label) {
|
|
12
12
|
return label.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
13
13
|
}
|
|
14
|
+
function formatEndpoint(id, port) {
|
|
15
|
+
return `${escapeId(id)}${port ? `:${escapeId(port)}` : ""}`;
|
|
16
|
+
}
|
|
14
17
|
const DIRECTION_TO_RANKDIR = {
|
|
15
18
|
down: "TB",
|
|
16
19
|
up: "BT",
|
|
@@ -72,7 +75,7 @@ function toDOT(graph) {
|
|
|
72
75
|
if (edge.label) attrs.push(`label="${escapeLabel(edge.label)}"`);
|
|
73
76
|
if (edge.color) attrs.push(`color="${escapeLabel(edge.color)}"`);
|
|
74
77
|
const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
|
|
75
|
-
lines.push(` ${
|
|
78
|
+
lines.push(` ${formatEndpoint(edge.sourceId, edge.sourcePort)} ${edgeOp} ${formatEndpoint(edge.targetId, edge.targetPort)}${attrStr};`);
|
|
76
79
|
}
|
|
77
80
|
lines.push("}");
|
|
78
81
|
return lines.join("\n");
|
|
@@ -95,6 +98,10 @@ const DOT_TO_SHAPE = {
|
|
|
95
98
|
cylinder: "cylinder",
|
|
96
99
|
parallelogram: "parallelogram"
|
|
97
100
|
};
|
|
101
|
+
function getPortId(nodeId) {
|
|
102
|
+
const port = nodeId.port;
|
|
103
|
+
return typeof port?.id === "string" ? port.id : void 0;
|
|
104
|
+
}
|
|
98
105
|
function attrsToMap(attrList) {
|
|
99
106
|
const map = {};
|
|
100
107
|
for (const a of attrList) map[a.id] = String(a.eq);
|
|
@@ -209,24 +216,29 @@ function fromDOT(dot) {
|
|
|
209
216
|
const endpointGroups = [];
|
|
210
217
|
for (const item of stmt.edge_list) if (item.type === "node_id") {
|
|
211
218
|
ensureNode(item.id, parentId, nd);
|
|
212
|
-
endpointGroups.push([
|
|
219
|
+
endpointGroups.push([{
|
|
220
|
+
id: item.id,
|
|
221
|
+
...getPortId(item) && { port: getPortId(item) }
|
|
222
|
+
}]);
|
|
213
223
|
} else if (item.type === "subgraph") {
|
|
214
224
|
walkChildren(item.children, parentId, nd, ed);
|
|
215
225
|
const subNodeIds = getNodeIdsFromSubgraph(item.children);
|
|
216
226
|
for (const subNodeId of subNodeIds) ensureNode(subNodeId, parentId, nd);
|
|
217
|
-
if (subNodeIds.length > 0) endpointGroups.push(subNodeIds);
|
|
227
|
+
if (subNodeIds.length > 0) endpointGroups.push(subNodeIds.map((id) => ({ id })));
|
|
218
228
|
}
|
|
219
229
|
for (let i = 0; i < endpointGroups.length - 1; i++) {
|
|
220
230
|
const left = endpointGroups[i];
|
|
221
231
|
const right = endpointGroups[i + 1];
|
|
222
|
-
for (const
|
|
232
|
+
for (const source of left) for (const target of right) {
|
|
223
233
|
const edge = {
|
|
224
234
|
type: "edge",
|
|
225
235
|
id: `e${edgeIdx++}`,
|
|
226
|
-
sourceId,
|
|
227
|
-
targetId,
|
|
236
|
+
sourceId: source.id,
|
|
237
|
+
targetId: target.id,
|
|
228
238
|
label: mergedEdgeAttrs["label"] ?? "",
|
|
229
239
|
data: void 0,
|
|
240
|
+
...source.port && { sourcePort: source.port },
|
|
241
|
+
...target.port && { targetPort: target.port },
|
|
230
242
|
...mergedEdgeAttrs["color"] && { color: mergedEdgeAttrs["color"] }
|
|
231
243
|
};
|
|
232
244
|
edges.push(edge);
|
|
@@ -23,13 +23,30 @@ function toGEXF(graph) {
|
|
|
23
23
|
"@_id": "a_shape",
|
|
24
24
|
"@_title": "shape",
|
|
25
25
|
"@_type": "string"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"@_id": "a_ports",
|
|
29
|
+
"@_title": "ports",
|
|
30
|
+
"@_type": "string"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
const edgeAttrs = [
|
|
34
|
+
{
|
|
35
|
+
"@_id": "a_edgeData",
|
|
36
|
+
"@_title": "data",
|
|
37
|
+
"@_type": "string"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"@_id": "a_sourcePort",
|
|
41
|
+
"@_title": "sourcePort",
|
|
42
|
+
"@_type": "string"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"@_id": "a_targetPort",
|
|
46
|
+
"@_title": "targetPort",
|
|
47
|
+
"@_type": "string"
|
|
26
48
|
}
|
|
27
49
|
];
|
|
28
|
-
const edgeAttrs = [{
|
|
29
|
-
"@_id": "a_edgeData",
|
|
30
|
-
"@_title": "data",
|
|
31
|
-
"@_type": "string"
|
|
32
|
-
}];
|
|
33
50
|
const nodes = graph.nodes.map((n) => {
|
|
34
51
|
const attvalues = [];
|
|
35
52
|
if (n.parentId) attvalues.push({
|
|
@@ -48,6 +65,10 @@ function toGEXF(graph) {
|
|
|
48
65
|
"@_for": "a_shape",
|
|
49
66
|
"@_value": n.shape
|
|
50
67
|
});
|
|
68
|
+
if (n.ports !== void 0) attvalues.push({
|
|
69
|
+
"@_for": "a_ports",
|
|
70
|
+
"@_value": JSON.stringify(n.ports)
|
|
71
|
+
});
|
|
51
72
|
const node = {
|
|
52
73
|
"@_id": n.id,
|
|
53
74
|
"@_label": n.label || n.id
|
|
@@ -80,6 +101,16 @@ function toGEXF(graph) {
|
|
|
80
101
|
"@_for": "a_edgeData",
|
|
81
102
|
"@_value": JSON.stringify(e.data)
|
|
82
103
|
}] };
|
|
104
|
+
const edgeAttvalues = edge.attvalues?.attvalue ?? [];
|
|
105
|
+
if (e.sourcePort !== void 0) edgeAttvalues.push({
|
|
106
|
+
"@_for": "a_sourcePort",
|
|
107
|
+
"@_value": e.sourcePort
|
|
108
|
+
});
|
|
109
|
+
if (e.targetPort !== void 0) edgeAttvalues.push({
|
|
110
|
+
"@_for": "a_targetPort",
|
|
111
|
+
"@_value": e.targetPort
|
|
112
|
+
});
|
|
113
|
+
if (edgeAttvalues.length > 0) edge.attvalues = { attvalue: edgeAttvalues };
|
|
83
114
|
if (e.color) {
|
|
84
115
|
const hex$1 = e.color.replace("#", "");
|
|
85
116
|
if (hex$1.length === 6) edge["viz:color"] = {
|
|
@@ -90,15 +121,6 @@ function toGEXF(graph) {
|
|
|
90
121
|
}
|
|
91
122
|
return edge;
|
|
92
123
|
});
|
|
93
|
-
const graphData = [];
|
|
94
|
-
if (graph.initialNodeId) graphData.push({
|
|
95
|
-
"@_for": "a_initialNodeId",
|
|
96
|
-
"@_value": graph.initialNodeId
|
|
97
|
-
});
|
|
98
|
-
if (graph.data !== void 0) graphData.push({
|
|
99
|
-
"@_for": "a_data",
|
|
100
|
-
"@_value": JSON.stringify(graph.data)
|
|
101
|
-
});
|
|
102
124
|
const obj = {
|
|
103
125
|
"?xml": {
|
|
104
126
|
"@_version": "1.0",
|
|
@@ -111,6 +133,9 @@ function toGEXF(graph) {
|
|
|
111
133
|
graph: {
|
|
112
134
|
"@_defaultedgetype": graph.type === "directed" ? "directed" : "undirected",
|
|
113
135
|
...graph.id && { "@_id": graph.id },
|
|
136
|
+
...graph.initialNodeId && { "@_initialNodeId": graph.initialNodeId },
|
|
137
|
+
...graph.direction && { "@_direction": graph.direction },
|
|
138
|
+
...graph.data !== void 0 && { "@_data": JSON.stringify(graph.data) },
|
|
114
139
|
attributes: [{
|
|
115
140
|
"@_class": "node",
|
|
116
141
|
attribute: nodeAttrs
|
|
@@ -168,6 +193,7 @@ function fromGEXF(xml) {
|
|
|
168
193
|
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
|
|
169
194
|
};
|
|
170
195
|
if (attvals["shape"]) node.shape = attvals["shape"];
|
|
196
|
+
if (attvals["ports"] !== void 0) node.ports = tryParseJSON(attvals["ports"]);
|
|
171
197
|
const pos = n["viz:position"];
|
|
172
198
|
if (pos) {
|
|
173
199
|
node.x = Number(pos["@_x"] ?? 0);
|
|
@@ -199,7 +225,9 @@ function fromGEXF(xml) {
|
|
|
199
225
|
sourceId: String(e["@_source"]),
|
|
200
226
|
targetId: String(e["@_target"]),
|
|
201
227
|
label: e["@_label"] ?? "",
|
|
202
|
-
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0
|
|
228
|
+
data: attvals["data"] !== void 0 ? tryParseJSON(attvals["data"]) : void 0,
|
|
229
|
+
...attvals["sourcePort"] !== void 0 && { sourcePort: attvals["sourcePort"] },
|
|
230
|
+
...attvals["targetPort"] !== void 0 && { targetPort: attvals["targetPort"] }
|
|
203
231
|
};
|
|
204
232
|
const color = e["viz:color"];
|
|
205
233
|
if (color) {
|
|
@@ -213,10 +241,11 @@ function fromGEXF(xml) {
|
|
|
213
241
|
return {
|
|
214
242
|
id: String(graphEl["@_id"] ?? ""),
|
|
215
243
|
type: graphType,
|
|
216
|
-
initialNodeId: null,
|
|
244
|
+
initialNodeId: graphEl["@_initialNodeId"] ?? null,
|
|
217
245
|
nodes,
|
|
218
246
|
edges,
|
|
219
|
-
data: void 0
|
|
247
|
+
data: graphEl["@_data"] !== void 0 ? tryParseJSON(String(graphEl["@_data"])) : void 0,
|
|
248
|
+
...graphEl["@_direction"] && { direction: graphEl["@_direction"] }
|
|
220
249
|
};
|
|
221
250
|
}
|
|
222
251
|
function asArray(val) {
|