@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.
Files changed (77) hide show
  1. package/README.md +121 -44
  2. package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
  3. package/dist/algorithms-D1cgly0g.d.mts +452 -0
  4. package/dist/algorithms-DBpH74hR.mjs +3309 -0
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/config-Dt5u1gSf.mjs +793 -0
  8. package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
  9. package/dist/format-support.mjs +38 -11
  10. package/dist/formats/adjacency-list/index.d.mts +1 -1
  11. package/dist/formats/adjacency-list/index.mjs +1 -1
  12. package/dist/formats/converter/index.d.mts +1 -1
  13. package/dist/formats/converter/index.mjs +1 -1
  14. package/dist/formats/cytoscape/index.d.mts +4 -4
  15. package/dist/formats/cytoscape/index.mjs +10 -4
  16. package/dist/formats/d2/index.d.mts +1 -1
  17. package/dist/formats/d2/index.mjs +26 -12
  18. package/dist/formats/d3/index.d.mts +4 -4
  19. package/dist/formats/d3/index.mjs +10 -4
  20. package/dist/formats/dot/index.d.mts +1 -1
  21. package/dist/formats/dot/index.mjs +22 -6
  22. package/dist/formats/edge-list/index.d.mts +1 -1
  23. package/dist/formats/edge-list/index.mjs +1 -1
  24. package/dist/formats/elk/index.d.mts +1 -1
  25. package/dist/formats/elk/index.mjs +63 -24
  26. package/dist/formats/gexf/index.d.mts +1 -1
  27. package/dist/formats/gexf/index.mjs +43 -16
  28. package/dist/formats/gml/index.d.mts +4 -4
  29. package/dist/formats/gml/index.mjs +28 -15
  30. package/dist/formats/graphml/index.d.mts +1 -1
  31. package/dist/formats/graphml/index.mjs +96 -23
  32. package/dist/formats/jgf/index.d.mts +4 -4
  33. package/dist/formats/jgf/index.mjs +12 -5
  34. package/dist/formats/mermaid/index.d.mts +1 -1
  35. package/dist/formats/mermaid/index.mjs +49 -12
  36. package/dist/formats/tgf/index.d.mts +4 -4
  37. package/dist/formats/tgf/index.mjs +4 -4
  38. package/dist/formats/xyflow/index.d.mts +12 -6
  39. package/dist/formats/xyflow/index.mjs +42 -10
  40. package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
  41. package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
  42. package/dist/index.d.mts +6 -6
  43. package/dist/index.mjs +290 -307
  44. package/dist/layout/cytoscape.d.mts +66 -0
  45. package/dist/layout/cytoscape.mjs +114 -0
  46. package/dist/layout/d3-force.d.mts +52 -0
  47. package/dist/layout/d3-force.mjs +127 -0
  48. package/dist/layout/d3-hierarchy.d.mts +39 -0
  49. package/dist/layout/d3-hierarchy.mjs +135 -0
  50. package/dist/layout/dagre.d.mts +32 -0
  51. package/dist/layout/dagre.mjs +99 -0
  52. package/dist/layout/elk.d.mts +47 -0
  53. package/dist/layout/elk.mjs +73 -0
  54. package/dist/layout/forceatlas2.d.mts +48 -0
  55. package/dist/layout/forceatlas2.mjs +100 -0
  56. package/dist/layout/graphviz.d.mts +50 -0
  57. package/dist/layout/graphviz.mjs +179 -0
  58. package/dist/layout/index.d.mts +185 -0
  59. package/dist/layout/index.mjs +181 -0
  60. package/dist/layout/webcola.d.mts +40 -0
  61. package/dist/layout/webcola.mjs +104 -0
  62. package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
  63. package/dist/queries-BMM0XAv_.mjs +986 -0
  64. package/dist/queries.d.mts +1 -1
  65. package/dist/queries.mjs +1 -768
  66. package/dist/schemas.d.mts +19 -1
  67. package/dist/schemas.mjs +32 -84
  68. package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
  69. package/dist/validate-BsfSOv0S.mjs +190 -0
  70. package/package.json +59 -7
  71. package/schemas/edge.schema.json +27 -0
  72. package/schemas/graph.schema.json +27 -0
  73. package/dist/algorithms-Ba7o7niK.mjs +0 -2394
  74. package/dist/algorithms-fTqmvhzP.d.mts +0 -178
  75. package/dist/indexing-DR8M1vBy.mjs +0 -137
  76. /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
  77. /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
package/README.md CHANGED
@@ -14,14 +14,19 @@ Optional peers are only needed for specific adapters:
14
14
 
15
15
  <!-- optional peer dependencies derived from package.json#peerDependencies -->
16
16
 
17
- | Package | Needed for |
18
- | ----------------- | --------------------------------------------------- |
19
- | `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
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` |
17
+ | Package | Needed for |
18
+ | ------------------------------------------- | --------------------------------------------------- |
19
+ | `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
20
+ | `dotparser` | `@statelyai/graph/dot` parsing |
21
+ | `zod` | `@statelyai/graph/schemas` |
22
+ | `elkjs` | `@statelyai/graph/elk`, `@statelyai/graph/layout/elk` |
23
+ | `@dagrejs/dagre` | `@statelyai/graph/layout/dagre` |
24
+ | `@hpcc-js/wasm-graphviz` | `@statelyai/graph/layout/graphviz` |
25
+ | `d3-force` | `@statelyai/graph/layout/d3-force` |
26
+ | `graphology`, `graphology-layout-forceatlas2` | `@statelyai/graph/layout/forceatlas2` |
27
+ | `d3-hierarchy` | `@statelyai/graph/layout/d3-hierarchy` |
28
+ | `webcola` | `@statelyai/graph/layout/webcola` |
29
+ | `cytoscape` | `@statelyai/graph/layout/cytoscape`, Cytoscape format typing |
25
30
 
26
31
  ## Highlights
27
32
 
@@ -29,7 +34,8 @@ Optional peers are only needed for specific adapters:
29
34
  - Standalone functions with a consistent `get*`/`gen*`/`is*`/`add*` naming model
30
35
  - Directed, undirected, hierarchical, and visual graph support
31
36
  - Ports for node-editor and dataflow-style graphs
32
- - Algorithms for traversal, paths, centrality, communities, connectivity, isomorphism, ordering, MST, and walks
37
+ - Algorithms for traversal, paths, centrality, communities, connectivity, flow/cuts, matching, cores, isomorphism, ordering, MST, and walks
38
+ - Pluggable layout over eight external engines (ELK, Graphviz, dagre, d3-force, ForceAtlas2, tidy tree, WebCola, cytoscape) — pure functions, optional peers
33
39
  - Diff/patch utilities for graph state changes
34
40
  - Multi-format conversion via package subpaths, with fidelity claims tested against fixtures
35
41
  - Small, fast test suite with broad format coverage
@@ -86,12 +92,20 @@ const roots = getSources(graph); // nodes with no incoming edges
86
92
 
87
93
  Batch operations (`addEntities`, `deleteEntities`, `updateEntities`) let you apply multiple changes at once.
88
94
 
95
+ `updateNode`/`updateEdge` accept any config field. Optional fields (position, size, `shape`, `color`, `style`, edge `weight`/`mode`/ports) can be **unset** by passing `null`; `undefined` leaves them unchanged:
96
+
97
+ ```ts
98
+ updateNode(graph, 'a', { x: 100, color: 'red' }); // set
99
+ updateEdge(graph, 'e1', { weight: 2, mode: 'undirected' });
100
+ updateNode(graph, 'a', { color: null }); // unset
101
+ ```
102
+
89
103
  ## Hierarchy
90
104
 
91
- Nodes support parent-child relationships for compound/nested graphs. Query children, ancestors, descendants, depth, and least common ancestor. Use `flatten()` to decompose into a flat leaf-node graph.
105
+ Nodes support parent-child relationships for compound/nested graphs. Query children, ancestors, descendants, depth, and least common ancestor. Use `getFlattenedGraph()` to decompose into a flat leaf-node graph.
92
106
 
93
107
  ```ts
94
- import { createGraph, getChildren, getLCA, flatten } from '@statelyai/graph';
108
+ import { createGraph, getChildren, getLCA, getFlattenedGraph } from '@statelyai/graph';
95
109
 
96
110
  const graph = createGraph({
97
111
  nodes: [
@@ -109,7 +123,7 @@ const graph = createGraph({
109
123
  });
110
124
 
111
125
  const children = getChildren(graph, 'b'); // [b1, b2]
112
- const flat = flatten(graph); // only leaf nodes, edges resolved
126
+ const flat = getFlattenedGraph(graph); // only leaf nodes, edges resolved
113
127
  ```
114
128
 
115
129
  ## Ports
@@ -149,7 +163,18 @@ getEdgesByPort(graph, 'render', 'input'); // [e1]
149
163
 
150
164
  <!-- validation helpers exported from src/schemas.ts -->
151
165
 
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.
166
+ For structural invariant checking without zod, the core export `getGraphIssues(graph)` returns machine-readable issues (duplicate ids, dangling edge endpoints, missing parents, parent cycles, missing initial nodes, duplicate or invalid port references) the recommended gate for untrusted or imported graphs:
167
+
168
+ ```ts
169
+ import { getGraphIssues } from '@statelyai/graph';
170
+
171
+ const issues = getGraphIssues(importedGraph);
172
+ if (issues.length > 0) {
173
+ console.error(issues.map((issue) => issue.message));
174
+ }
175
+ ```
176
+
177
+ Use the `@statelyai/graph/schemas` subpath when you want full runtime shape validation or JSON Schema generation. `validateGraph()` combines zod shape checks with the same graph invariants.
153
178
 
154
179
  ```ts
155
180
  import { GraphSchema, isGraph, validateGraph } from '@statelyai/graph/schemas';
@@ -169,12 +194,14 @@ const parsed = GraphSchema.parse(unknownValue);
169
194
 
170
195
  <!-- algorithm functions exported from src/algorithms.ts -->
171
196
 
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.
197
+ Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, simple paths, all-pairs shortest paths, A*, bidirectional Dijkstra), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector, Katz), community detection (Louvain, label propagation, Girvan-Newman, greedy modularity, modularity scoring), flow & cuts (`getMaxFlow`, `getMinCut`), bipartite analysis (`isBipartite`, Hopcroft–Karp `getMaximumBipartiteMatching`), k-cores (`getCoreNumbers`, `getKCore`), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, dominator trees, transitive reduction, isomorphism, topological sort, minimum spanning tree, and seeded graph generators (`createCompleteGraph`, `createGridGraph`, `createRandomGraph`). Many algorithms have lazy generator variants (`gen*`) for early exit. See [docs/algorithms.md](./docs/algorithms.md) for the full reference.
198
+
199
+ Hot algorithm loops (centrality, components) run on an internal compressed-sparse-row snapshot — cached and invalidated transparently like the rest of the index — so they stay fast on large graphs without changing the plain-JSON model. Algorithm results are differential-tested against graphology on seeded random graphs.
173
200
 
174
201
  ```ts
175
202
  import {
176
- bfs,
177
- dfs,
203
+ genBFS,
204
+ genDFS,
178
205
  hasPath,
179
206
  isAcyclic,
180
207
  getShortestPath,
@@ -183,16 +210,20 @@ import {
183
210
  getConnectedComponents,
184
211
  getMinimumSpanningTree,
185
212
  getPageRank,
213
+ getLouvainCommunities,
186
214
  getLabelPropagationCommunities,
187
215
  genGirvanNewmanCommunities,
188
216
  getBridges,
217
+ getMaxFlow,
218
+ getDominatorTree,
219
+ getTransitiveReduction,
189
220
  isIsomorphic,
190
221
  } from '@statelyai/graph';
191
222
 
192
- for (const node of bfs(graph, 'a')) {
223
+ for (const node of genBFS(graph, 'a')) {
193
224
  /* breadth-first */
194
225
  }
195
- for (const node of dfs(graph, 'a')) {
226
+ for (const node of genDFS(graph, 'a')) {
196
227
  /* depth-first */
197
228
  }
198
229
 
@@ -201,21 +232,56 @@ isAcyclic(graph); // cycle check
201
232
  getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path
202
233
  getTopologicalSort(graph); // topological order (or null)
203
234
  getConnectedComponents(graph); // connected components
204
- getMinimumSpanningTree(graph, { weight: (e) => e.data?.weight ?? 1 }); // MST
235
+ getMinimumSpanningTree(graph, { getWeight: (e) => e.weight ?? 1 }); // MST
205
236
  getPageRank(graph); // link analysis scores
237
+ getLouvainCommunities(graph); // community detection (Louvain)
206
238
  getLabelPropagationCommunities(graph); // community detection
207
239
  [...genGirvanNewmanCommunities(graph)]; // lazy community splits
208
240
  getBridges(graph); // bridge edges
241
+ getMaxFlow(graph, { from: 'a', to: 'c' }); // max flow + min cut
242
+ getDominatorTree(graph, { from: 'a' }); // immediate dominators
243
+ getTransitiveReduction(graph); // minimal equivalent DAG
209
244
  isIsomorphic(graph, otherGraph); // structural equivalence
210
245
  ```
211
246
 
247
+ ## Layout
248
+
249
+ <!-- layout adapters under src/layout/*.ts and helpers exported from src/layout/index.ts -->
250
+
251
+ Plug-and-play layout over external engines — pure functions in, positioned `VisualGraph` out. No layout algorithms of our own; each adapter is a subpath with an optional peer dependency. The hierarchical engines (ELK, dagre, Graphviz) also produce routed edge `points` and computed edge-label rects; the physics/tree/cytoscape engines position nodes only.
252
+
253
+ ```ts
254
+ import { getElkLayout } from '@statelyai/graph/layout/elk'; // elkjs
255
+ import { getDagreLayout } from '@statelyai/graph/layout/dagre'; // @dagrejs/dagre
256
+ import { getGraphvizLayout } from '@statelyai/graph/layout/graphviz'; // @hpcc-js/wasm-graphviz (8 engines)
257
+ import { genForceLayout } from '@statelyai/graph/layout/d3-force'; // d3-force
258
+ import { getForceAtlas2Layout } from '@statelyai/graph/layout/forceatlas2'; // graphology FA2
259
+ import { getTidyTreeLayout } from '@statelyai/graph/layout/d3-hierarchy'; // d3-hierarchy
260
+ import { getColaLayout } from '@statelyai/graph/layout/webcola'; // webcola (constraints)
261
+ import { getCytoscapeLayout } from '@statelyai/graph/layout/cytoscape'; // cytoscape ecosystem
262
+ import { applyLayoutFrame, getLayoutBounds, centerGraph } from '@statelyai/graph/layout';
263
+
264
+ const laidOut = await getElkLayout(graph, {
265
+ measure: (node) => measureText(node.label), // text measurement stays yours
266
+ constraints: { layer: (node) => node.data?.tier }, // portable layer constraint
267
+ });
268
+
269
+ // Physics layouts are generators — one tick per frame, cancel by stopping
270
+ for (const frame of genForceLayout(graph, { seed: 42 })) {
271
+ applyLayoutFrame(graph, frame);
272
+ render(graph);
273
+ }
274
+ ```
275
+
276
+ Edge `x`/`y`/`width`/`height` are canonically the edge-label rect; routes live in `edge.points` (`routing` says how to interpret them). Layouts are plain JSON — tween between engines with `genLayoutTransition`, or diff them with `getPatches`. See [docs/layout.md](./docs/layout.md) and [docs/layout-transitions.md](./docs/layout-transitions.md).
277
+
212
278
  ## Diff & Walks
213
279
 
214
280
  Beyond classic graph algorithms, the library also includes utilities for evolving and exploring graph state:
215
281
 
216
- - `getDiff()`, `getPatches()`, `applyPatches()` for graph change tracking
282
+ - `getDiff()`, `getPatches()`, `updateGraphWithPatches()` for graph change tracking
217
283
  - `genRandomWalk()`, `genWeightedRandomWalk()`, and coverage helpers for model-based testing and simulation
218
- - `getSubgraph()` and `reverseGraph()` for structural transforms
284
+ - `getSubgraph()` and `getReversedGraph()` for structural transforms
219
285
 
220
286
  ## Visual Graphs
221
287
 
@@ -272,29 +338,29 @@ source information instead of preserving it as metadata.
272
338
 
273
339
  <!-- format support matrix derived from src/formats/support.ts -->
274
340
 
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
- | `d2` | full | full | full | full | D2 syntax, hierarchy, ports, styles, and connector modes round-trip. |
281
- | `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
282
- | `edge-list` | none | none | none | partial | Endpoints only. |
283
- | `elk` | full | full | full | full | Metadata round-trips through reserved layout options. |
284
- | `gexf` | full | full | full | full | Custom attributes preserve metadata beyond the standard viz module. |
285
- | `gml` | full | full | full | full | Graph, node, and edge metadata round-trip through direct and JSON fields. |
286
- | `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
287
- | `jgf` | full | full | full | full | Graph, node, and edge metadata round-trip through `metadata` objects. |
288
- | `tgf` | none | none | none | partial | Minimal ids and labels only. |
289
- | `xyflow` | full | full | full | full | Metadata round-trips through reserved data fields. |
290
- | `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
291
- | `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
292
- | `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
293
- | `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
294
- | `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
295
- | `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
296
- | `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
297
- | `mermaid/state` | full | none | partial | full | State syntax round-trips through graph and node data. |
341
+ | Format | Hierarchy | Ports | Visual | Round-trip | Notes |
342
+ | ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
343
+ | `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
344
+ | `cytoscape` | full | full | full | full | Graph/node/edge metadata (incl. per-edge `mode`) round-trips through element data. |
345
+ | `d3` | full | full | full | full | Graph/node/edge metadata (incl. per-edge `mode`) round-trips through the loose JSON shape. |
346
+ | `d2` | full | full | full | full | Hierarchy, ports, styles, and connector modes round-trip; nested `vars` sub-blocks are dropped. |
347
+ | `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
348
+ | `edge-list` | none | none | none | partial | Endpoints only. |
349
+ | `elk` | full | full | full | full | Metadata round-trips through reserved layout options; port ids are emitted as `nodeId__portName` (document-unique, as ELK requires). |
350
+ | `gexf` | full | full | full | full | Custom attributes preserve metadata; `bidirectional` maps to directed. |
351
+ | `gml` | full | full | full | full | Metadata round-trips through direct and JSON fields; per-edge/graph `mode` via a dialect key. |
352
+ | `graphml` | full | full | partial | full | Emit is own-dialect (`<data>` fields, flat); import handles both dialects incl. standard nested `<graph>`, native `<port>` elements, and `sourceport`/`targetport` attributes. Multi-graph files import the first graph. |
353
+ | `jgf` | full | full | full | full | Metadata (incl. per-edge/graph `mode`) round-trips through `metadata` objects. |
354
+ | `tgf` | none | none | none | partial | Minimal ids and labels only. |
355
+ | `xyflow` | full | full | full | full | Metadata (incl. weight, ports, per-edge `mode`) round-trips through reserved data fields; parents are ordered before children for React Flow. |
356
+ | `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
357
+ | `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
358
+ | `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
359
+ | `mermaid/flowchart` | partial | none | partial | partial | `linkStyle` indices are fragile. |
360
+ | `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
361
+ | `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
362
+ | `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
363
+ | `mermaid/state` | full | none | partial | partial | Isolated states and labels now emit (labels via the description form); `initialNodeId` round-trips as `[*] -->`. |
298
364
 
299
365
  Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
300
366
 
@@ -318,6 +384,17 @@ Format-specific docs live alongside the source:
318
384
  - [xyflow](./src/formats/xyflow/README.md)
319
385
  - [Converter helpers](./src/formats/converter/README.md)
320
386
 
387
+ ## Guides
388
+
389
+ <!-- guide documents under docs/*.md -->
390
+
391
+ - [Layout guide](./docs/layout.md) — the adapter contract, all eight engines, constraints, sizing, web workers
392
+ - [Layout transitions](./docs/layout-transitions.md) — tween between engines; layouts are just data
393
+ - [Algorithms reference](./docs/algorithms.md) — every algorithm with complexity and semantics notes
394
+ - [Benchmarks](./docs/benchmarks.md) — measured against graphology, ngraph, graphlib, and cytoscape
395
+ - [Migrating from graphlib](./docs/migrating-from-graphlib.md)
396
+ - [React Flow + ELK pipeline](./docs/react-flow-elk-pipeline.md) — measured nodes, worker layout, live re-layout
397
+
321
398
  ## Examples
322
399
 
323
400
  <!-- runnable example files under examples/ -->
@@ -44,7 +44,9 @@ function toAdjacencyList(graph) {
44
44
  function fromAdjacencyList(adj, options) {
45
45
  const directed = options?.directed ?? true;
46
46
  const seen = /* @__PURE__ */ new Set();
47
- const nodes = Object.keys(adj).map((id) => ({
47
+ const nodeIds = new Set(Object.keys(adj));
48
+ for (const targets of Object.values(adj)) for (const targetId of targets) nodeIds.add(targetId);
49
+ const nodes = [...nodeIds].map((id) => ({
48
50
  type: "node",
49
51
  id,
50
52
  parentId: null,