@statelyai/graph 0.3.0 → 0.4.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 +12 -11
- package/dist/{adjacency-list-A4_Eiwj3.mjs → adjacency-list-Bv4tfiM3.mjs} +33 -0
- package/dist/{algorithms-DBU7nmIV.mjs → algorithms-CnTmuX9t.mjs} +682 -24
- package/dist/algorithms.d.mts +479 -10
- package/dist/algorithms.mjs +1 -1
- package/dist/converter-C5DlzzHs.mjs +67 -0
- package/dist/{edge-list-DuHMz8hf.mjs → edge-list-R1SUbHwe.mjs} +32 -0
- package/dist/formats/adjacency-list/index.d.mts +35 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +37 -3
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +50 -2
- package/dist/formats/cytoscape/index.mjs +53 -5
- package/dist/formats/d3/index.d.mts +48 -2
- package/dist/formats/d3/index.mjs +48 -2
- package/dist/formats/dot/index.d.mts +56 -2
- package/dist/formats/dot/index.mjs +55 -2
- package/dist/formats/edge-list/index.d.mts +34 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +5 -5
- package/dist/formats/gml/index.d.mts +58 -2
- package/dist/formats/gml/index.mjs +59 -4
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +2 -2
- package/dist/formats/jgf/index.d.mts +51 -2
- package/dist/formats/jgf/index.mjs +54 -5
- package/dist/formats/mermaid/index.d.mts +201 -8
- package/dist/formats/mermaid/index.mjs +365 -26
- package/dist/formats/tgf/index.d.mts +47 -2
- package/dist/formats/tgf/index.mjs +46 -2
- package/dist/formats/xyflow/index.d.mts +73 -0
- package/dist/formats/xyflow/index.mjs +133 -0
- package/dist/index.d.mts +320 -14
- package/dist/index.mjs +117 -9
- package/dist/{indexing-BFFVMnjF.mjs → indexing-DitHphT7.mjs} +37 -7
- package/dist/queries.d.mts +353 -8
- package/dist/queries.mjs +359 -15
- package/dist/{types-B6Tpeerk.d.mts → types-Bq_fmLwW.d.mts} +15 -4
- package/package.json +3 -1
- package/dist/converter-DnbeyE_p.mjs +0 -33
|
@@ -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-
|
|
1
|
+
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-DitHphT7.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,
|
|
@@ -36,7 +41,17 @@ function resolveEdge(config) {
|
|
|
36
41
|
if (config.style !== void 0) edge.style = config.style;
|
|
37
42
|
return edge;
|
|
38
43
|
}
|
|
39
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Create a graph from a config. Resolves defaults for all fields.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const graph = createGraph({
|
|
50
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
51
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
40
55
|
function createGraph(config) {
|
|
41
56
|
const graph = {
|
|
42
57
|
id: config?.id ?? "",
|
|
@@ -50,7 +65,18 @@ function createGraph(config) {
|
|
|
50
65
|
if (config?.style !== void 0) graph.style = config.style;
|
|
51
66
|
return graph;
|
|
52
67
|
}
|
|
53
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Create a visual graph with required position/size on all nodes and edges.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const graph = createVisualGraph({
|
|
74
|
+
* nodes: [{ id: 'a', x: 0, y: 0, width: 100, height: 50 }],
|
|
75
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', x: 0, y: 0, width: 0, height: 0 }],
|
|
76
|
+
* });
|
|
77
|
+
* // graph.nodes[0].x === 0
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
54
80
|
function createVisualGraph(config) {
|
|
55
81
|
const base = createGraph(config);
|
|
56
82
|
return {
|
|
@@ -62,7 +88,7 @@ function createVisualGraph(config) {
|
|
|
62
88
|
y: n.y ?? 0,
|
|
63
89
|
width: n.width ?? 0,
|
|
64
90
|
height: n.height ?? 0,
|
|
65
|
-
shape: n.shape
|
|
91
|
+
...n.shape !== void 0 && { shape: n.shape }
|
|
66
92
|
})),
|
|
67
93
|
edges: base.edges.map((e) => ({
|
|
68
94
|
...e,
|
|
@@ -75,11 +101,30 @@ function createVisualGraph(config) {
|
|
|
75
101
|
}
|
|
76
102
|
/**
|
|
77
103
|
* Create a graph by BFS exploration of a transition function.
|
|
78
|
-
* Each unique state becomes a node; each (state, event)
|
|
104
|
+
* Each unique state becomes a node; each (state, event) -> nextState becomes an edge.
|
|
79
105
|
*
|
|
80
106
|
* - Node IDs are determined by `serializeState` (default: `JSON.stringify`).
|
|
81
107
|
* - Edge IDs use the format `sourceId|serializedEvent|targetId` for uniqueness
|
|
82
108
|
* and debuggability. Edge labels are just the serialized event string.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const graph = createGraphFromTransition(
|
|
113
|
+
* (state, event) => {
|
|
114
|
+
* if (state === 'green' && event === 'TIMER') return 'yellow';
|
|
115
|
+
* if (state === 'yellow' && event === 'TIMER') return 'red';
|
|
116
|
+
* if (state === 'red' && event === 'TIMER') return 'green';
|
|
117
|
+
* return state;
|
|
118
|
+
* },
|
|
119
|
+
* {
|
|
120
|
+
* initialState: 'green',
|
|
121
|
+
* events: ['TIMER'],
|
|
122
|
+
* serializeState: (s) => s,
|
|
123
|
+
* serializeEvent: (e) => e,
|
|
124
|
+
* },
|
|
125
|
+
* );
|
|
126
|
+
* // graph.nodes.length === 3
|
|
127
|
+
* ```
|
|
83
128
|
*/
|
|
84
129
|
function createGraphFromTransition(transition, options) {
|
|
85
130
|
const serializeState = options.serializeState ?? JSON.stringify;
|
|
@@ -139,46 +184,102 @@ function createGraphFromTransition(transition, options) {
|
|
|
139
184
|
edges
|
|
140
185
|
});
|
|
141
186
|
}
|
|
142
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Get a node by id, or `undefined` if not found.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```ts
|
|
192
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
193
|
+
* const node = getNode(graph, 'a'); // GraphNode
|
|
194
|
+
* const missing = getNode(graph, 'z'); // undefined
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
143
197
|
function getNode(graph, id) {
|
|
144
198
|
const arrayIdx = getIndex(graph).nodeById.get(id);
|
|
145
199
|
return arrayIdx !== void 0 ? graph.nodes[arrayIdx] : void 0;
|
|
146
200
|
}
|
|
147
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Get an edge by id, or `undefined` if not found.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* const graph = createGraph({
|
|
207
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
208
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
209
|
+
* });
|
|
210
|
+
* const edge = getEdge(graph, 'e1'); // GraphEdge
|
|
211
|
+
* const missing = getEdge(graph, 'z'); // undefined
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
148
214
|
function getEdge(graph, id) {
|
|
149
215
|
const arrayIdx = getIndex(graph).edgeById.get(id);
|
|
150
216
|
return arrayIdx !== void 0 ? graph.edges[arrayIdx] : void 0;
|
|
151
217
|
}
|
|
152
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Check if a node exists in the graph.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
224
|
+
* hasNode(graph, 'a'); // true
|
|
225
|
+
* hasNode(graph, 'z'); // false
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
153
228
|
function hasNode(graph, id) {
|
|
154
229
|
return getIndex(graph).nodeById.has(id);
|
|
155
230
|
}
|
|
156
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Check if an edge exists in the graph.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* const graph = createGraph({
|
|
237
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
238
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
239
|
+
* });
|
|
240
|
+
* hasEdge(graph, 'e1'); // true
|
|
241
|
+
* hasEdge(graph, 'z'); // false
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
157
244
|
function hasEdge(graph, id) {
|
|
158
245
|
return getIndex(graph).edgeById.has(id);
|
|
159
246
|
}
|
|
160
247
|
/**
|
|
161
248
|
* **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place.
|
|
162
249
|
* @returns The resolved node that was added.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* const graph = createGraph();
|
|
254
|
+
* const node = addNode(graph, { id: 'a', label: 'Node A' });
|
|
255
|
+
* // graph.nodes.length === 1
|
|
256
|
+
* ```
|
|
163
257
|
*/
|
|
164
258
|
function addNode(graph, config) {
|
|
259
|
+
const node = resolveNode(config);
|
|
165
260
|
const idx = getIndex(graph);
|
|
166
261
|
if (idx.nodeById.has(config.id)) throw new Error(`Node "${config.id}" already exists`);
|
|
167
|
-
if (config.parentId
|
|
168
|
-
const node = resolveNode(config);
|
|
262
|
+
if (config.parentId && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
|
|
169
263
|
indexAddNode(idx, node, graph.nodes.push(node) - 1);
|
|
170
264
|
return node;
|
|
171
265
|
}
|
|
172
266
|
/**
|
|
173
267
|
* **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place.
|
|
174
268
|
* @returns The resolved edge that was added.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }, { id: 'b' }] });
|
|
273
|
+
* const edge = addEdge(graph, { id: 'e1', sourceId: 'a', targetId: 'b' });
|
|
274
|
+
* // graph.edges.length === 1
|
|
275
|
+
* ```
|
|
175
276
|
*/
|
|
176
277
|
function addEdge(graph, config) {
|
|
278
|
+
const edge = resolveEdge(config);
|
|
177
279
|
const idx = getIndex(graph);
|
|
178
280
|
if (idx.edgeById.has(config.id)) throw new Error(`Edge "${config.id}" already exists`);
|
|
179
281
|
if (!idx.nodeById.has(config.sourceId)) throw new Error(`Source node "${config.sourceId}" does not exist`);
|
|
180
282
|
if (!idx.nodeById.has(config.targetId)) throw new Error(`Target node "${config.targetId}" does not exist`);
|
|
181
|
-
const edge = resolveEdge(config);
|
|
182
283
|
indexAddEdge(idx, edge, graph.edges.push(edge) - 1);
|
|
183
284
|
return edge;
|
|
184
285
|
}
|
|
@@ -188,6 +289,16 @@ function addEdge(graph, config) {
|
|
|
188
289
|
*
|
|
189
290
|
* By default, children are deleted recursively.
|
|
190
291
|
* With `{ reparent: true }`, children are re-parented to the deleted node's parent.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* const graph = createGraph({
|
|
296
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
297
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
298
|
+
* });
|
|
299
|
+
* deleteNode(graph, 'a');
|
|
300
|
+
* // graph.nodes.length === 1, edge e1 also removed
|
|
301
|
+
* ```
|
|
191
302
|
*/
|
|
192
303
|
function deleteNode(graph, id, opts) {
|
|
193
304
|
const node = getNode(graph, id);
|
|
@@ -205,6 +316,16 @@ function deleteNode(graph, id, opts) {
|
|
|
205
316
|
}
|
|
206
317
|
/**
|
|
207
318
|
* **Mutable.** Delete an edge. Mutates `graph.edges` in place.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* const graph = createGraph({
|
|
323
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
324
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
325
|
+
* });
|
|
326
|
+
* deleteEdge(graph, 'e1');
|
|
327
|
+
* // graph.edges.length === 0
|
|
328
|
+
* ```
|
|
208
329
|
*/
|
|
209
330
|
function deleteEdge(graph, id) {
|
|
210
331
|
if (!hasEdge(graph, id)) throw new Error(`Edge "${id}" does not exist`);
|
|
@@ -214,6 +335,13 @@ function deleteEdge(graph, id) {
|
|
|
214
335
|
/**
|
|
215
336
|
* **Mutable.** Update a node in place.
|
|
216
337
|
* @returns The updated node.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
|
|
342
|
+
* const updated = updateNode(graph, 'a', { label: 'new' });
|
|
343
|
+
* // updated.label === 'new'
|
|
344
|
+
* ```
|
|
217
345
|
*/
|
|
218
346
|
function updateNode(graph, id, update) {
|
|
219
347
|
const idx = getIndex(graph);
|
|
@@ -238,6 +366,16 @@ function updateNode(graph, id, update) {
|
|
|
238
366
|
/**
|
|
239
367
|
* **Mutable.** Update an edge in place.
|
|
240
368
|
* @returns The updated edge.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```ts
|
|
372
|
+
* const graph = createGraph({
|
|
373
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
374
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
|
|
375
|
+
* });
|
|
376
|
+
* const updated = updateEdge(graph, 'e1', { label: 'new' });
|
|
377
|
+
* // updated.label === 'new'
|
|
378
|
+
* ```
|
|
241
379
|
*/
|
|
242
380
|
function updateEdge(graph, id, update) {
|
|
243
381
|
const idx = getIndex(graph);
|
|
@@ -262,6 +400,16 @@ function updateEdge(graph, id, update) {
|
|
|
262
400
|
/**
|
|
263
401
|
* **Mutable.** Add multiple nodes and edges to the graph.
|
|
264
402
|
* Nodes are added first, then edges (so edges can reference new nodes).
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* ```ts
|
|
406
|
+
* const graph = createGraph();
|
|
407
|
+
* addEntities(graph, {
|
|
408
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
409
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
410
|
+
* });
|
|
411
|
+
* // graph.nodes.length === 2, graph.edges.length === 1
|
|
412
|
+
* ```
|
|
265
413
|
*/
|
|
266
414
|
function addEntities(graph, entities) {
|
|
267
415
|
for (const nodeConfig of entities.nodes ?? []) addNode(graph, nodeConfig);
|
|
@@ -270,6 +418,16 @@ function addEntities(graph, entities) {
|
|
|
270
418
|
/**
|
|
271
419
|
* **Mutable.** Delete entities by id(s). Automatically detects whether each id
|
|
272
420
|
* is a node or edge. Node deletions cascade to children and connected edges.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```ts
|
|
424
|
+
* const graph = createGraph({
|
|
425
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
426
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
427
|
+
* });
|
|
428
|
+
* deleteEntities(graph, ['a', 'e1']);
|
|
429
|
+
* // graph.nodes.length === 1, graph.edges.length === 0
|
|
430
|
+
* ```
|
|
273
431
|
*/
|
|
274
432
|
function deleteEntities(graph, ids, opts) {
|
|
275
433
|
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
@@ -279,6 +437,18 @@ function deleteEntities(graph, ids, opts) {
|
|
|
279
437
|
/**
|
|
280
438
|
* **Mutable.** Update multiple nodes and edges in place.
|
|
281
439
|
* Each entry must include an `id` to identify which entity to update.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* const graph = createGraph({
|
|
444
|
+
* nodes: [{ id: 'a', label: 'old' }],
|
|
445
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', label: 'old' }],
|
|
446
|
+
* });
|
|
447
|
+
* updateEntities(graph, {
|
|
448
|
+
* nodes: [{ id: 'a', label: 'new' }],
|
|
449
|
+
* edges: [{ id: 'e1', label: 'new' }],
|
|
450
|
+
* });
|
|
451
|
+
* ```
|
|
282
452
|
*/
|
|
283
453
|
function updateEntities(graph, updates) {
|
|
284
454
|
for (const nodeUpdate of updates.nodes ?? []) {
|
|
@@ -293,13 +463,33 @@ function updateEntities(graph, updates) {
|
|
|
293
463
|
/**
|
|
294
464
|
* OOP wrapper around a plain `Graph` object.
|
|
295
465
|
* Delegates to the standalone mutable functions.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```ts
|
|
469
|
+
* const instance = new GraphInstance({
|
|
470
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
471
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
472
|
+
* });
|
|
473
|
+
* instance.addNode({ id: 'c' });
|
|
474
|
+
* instance.hasNode('c'); // true
|
|
475
|
+
* instance.toJSON(); // plain Graph object
|
|
476
|
+
* ```
|
|
296
477
|
*/
|
|
297
478
|
var GraphInstance = class GraphInstance {
|
|
298
479
|
graph;
|
|
299
480
|
constructor(config) {
|
|
300
481
|
this.graph = createGraph(config);
|
|
301
482
|
}
|
|
302
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Wrap an existing plain graph object.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
489
|
+
* const instance = GraphInstance.from(graph);
|
|
490
|
+
* instance.hasNode('a'); // true
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
303
493
|
static from(graph) {
|
|
304
494
|
const instance = Object.create(GraphInstance.prototype);
|
|
305
495
|
instance.graph = graph;
|
|
@@ -377,6 +567,23 @@ function collectDescendants(graph, id) {
|
|
|
377
567
|
|
|
378
568
|
//#endregion
|
|
379
569
|
//#region src/algorithms.ts
|
|
570
|
+
/**
|
|
571
|
+
* Breadth-first traversal generator yielding nodes level by level.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* import { createGraph, bfs } from '@statelyai/graph';
|
|
576
|
+
*
|
|
577
|
+
* const graph = createGraph({
|
|
578
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
579
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
580
|
+
* });
|
|
581
|
+
*
|
|
582
|
+
* for (const node of bfs(graph, 'a')) {
|
|
583
|
+
* console.log(node.id); // 'a', 'b', 'c'
|
|
584
|
+
* }
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
380
587
|
function* bfs(graph, startId) {
|
|
381
588
|
const idx = getIndex(graph);
|
|
382
589
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -393,6 +600,23 @@ function* bfs(graph, startId) {
|
|
|
393
600
|
}
|
|
394
601
|
}
|
|
395
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Depth-first traversal generator yielding nodes as visited.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* import { createGraph, dfs } from '@statelyai/graph';
|
|
609
|
+
*
|
|
610
|
+
* const graph = createGraph({
|
|
611
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
612
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
613
|
+
* });
|
|
614
|
+
*
|
|
615
|
+
* for (const node of dfs(graph, 'a')) {
|
|
616
|
+
* console.log(node.id); // 'a', 'b', 'c'
|
|
617
|
+
* }
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
396
620
|
function* dfs(graph, startId) {
|
|
397
621
|
const idx = getIndex(graph);
|
|
398
622
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -424,6 +648,21 @@ function getSuccessorIds(graph, nodeId) {
|
|
|
424
648
|
const idx = getIndex(graph);
|
|
425
649
|
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
426
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Checks whether the graph contains no cycles.
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```ts
|
|
656
|
+
* import { createGraph, isAcyclic } from '@statelyai/graph';
|
|
657
|
+
*
|
|
658
|
+
* const dag = createGraph({
|
|
659
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
660
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
661
|
+
* });
|
|
662
|
+
*
|
|
663
|
+
* isAcyclic(dag); // true
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
427
666
|
function isAcyclic(graph) {
|
|
428
667
|
if (graph.type === "undirected") return isAcyclicUndirected(graph);
|
|
429
668
|
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
@@ -468,6 +707,23 @@ function isAcyclicUndirected(graph) {
|
|
|
468
707
|
for (const n of graph.nodes) if (!visited.has(n.id) && hasCycle(n.id, null)) return false;
|
|
469
708
|
return true;
|
|
470
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Returns connected components as arrays of nodes.
|
|
712
|
+
* Treats all edges as undirected for connectivity.
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```ts
|
|
716
|
+
* import { createGraph, getConnectedComponents } from '@statelyai/graph';
|
|
717
|
+
*
|
|
718
|
+
* const graph = createGraph({
|
|
719
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
720
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
721
|
+
* });
|
|
722
|
+
*
|
|
723
|
+
* const components = getConnectedComponents(graph);
|
|
724
|
+
* // [[nodeA, nodeB], [nodeC]]
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
471
727
|
function getConnectedComponents(graph) {
|
|
472
728
|
const idx = getIndex(graph);
|
|
473
729
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -504,6 +760,25 @@ function getConnectedComponents(graph) {
|
|
|
504
760
|
}
|
|
505
761
|
return components;
|
|
506
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Returns a topological ordering of nodes, or `null` if the graph is cyclic.
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```ts
|
|
768
|
+
* import { createGraph, getTopologicalSort } from '@statelyai/graph';
|
|
769
|
+
*
|
|
770
|
+
* const graph = createGraph({
|
|
771
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
772
|
+
* edges: [
|
|
773
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
774
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
775
|
+
* ],
|
|
776
|
+
* });
|
|
777
|
+
*
|
|
778
|
+
* const sorted = getTopologicalSort(graph);
|
|
779
|
+
* // [nodeA, nodeB, nodeC]
|
|
780
|
+
* ```
|
|
781
|
+
*/
|
|
507
782
|
function getTopologicalSort(graph) {
|
|
508
783
|
const idx = getIndex(graph);
|
|
509
784
|
const inDeg = /* @__PURE__ */ new Map();
|
|
@@ -528,16 +803,65 @@ function getTopologicalSort(graph) {
|
|
|
528
803
|
if (result.length !== graph.nodes.length) return null;
|
|
529
804
|
return result;
|
|
530
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Checks whether a path exists between two nodes.
|
|
808
|
+
*
|
|
809
|
+
* @example
|
|
810
|
+
* ```ts
|
|
811
|
+
* import { createGraph, hasPath } from '@statelyai/graph';
|
|
812
|
+
*
|
|
813
|
+
* const graph = createGraph({
|
|
814
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
815
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
816
|
+
* });
|
|
817
|
+
*
|
|
818
|
+
* hasPath(graph, 'a', 'b'); // true
|
|
819
|
+
* hasPath(graph, 'a', 'c'); // false
|
|
820
|
+
* ```
|
|
821
|
+
*/
|
|
531
822
|
function hasPath(graph, sourceId, targetId) {
|
|
532
823
|
return getShortestPaths(graph, {
|
|
533
824
|
from: sourceId,
|
|
534
825
|
to: targetId
|
|
535
826
|
}).length > 0;
|
|
536
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Checks whether the graph is connected (all nodes reachable from any node).
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* ```ts
|
|
833
|
+
* import { createGraph, isConnected } from '@statelyai/graph';
|
|
834
|
+
*
|
|
835
|
+
* const graph = createGraph({
|
|
836
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
837
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
838
|
+
* });
|
|
839
|
+
*
|
|
840
|
+
* isConnected(graph); // true
|
|
841
|
+
* ```
|
|
842
|
+
*/
|
|
537
843
|
function isConnected(graph) {
|
|
538
844
|
if (graph.nodes.length === 0) return true;
|
|
539
845
|
return getConnectedComponents(graph).length <= 1;
|
|
540
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Checks whether the graph is a tree (connected and acyclic).
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```ts
|
|
852
|
+
* import { createGraph, isTree } from '@statelyai/graph';
|
|
853
|
+
*
|
|
854
|
+
* const tree = createGraph({
|
|
855
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
856
|
+
* edges: [
|
|
857
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
858
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
859
|
+
* ],
|
|
860
|
+
* });
|
|
861
|
+
*
|
|
862
|
+
* isTree(tree); // true
|
|
863
|
+
* ```
|
|
864
|
+
*/
|
|
541
865
|
function isTree(graph) {
|
|
542
866
|
return isConnected(graph) && isAcyclic(graph);
|
|
543
867
|
}
|
|
@@ -671,6 +995,24 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
|
671
995
|
/**
|
|
672
996
|
* Lazily yields all shortest paths from a source node.
|
|
673
997
|
* Use `getShortestPaths` for the full array.
|
|
998
|
+
*
|
|
999
|
+
* @example
|
|
1000
|
+
* ```ts
|
|
1001
|
+
* import { createGraph, genShortestPaths } from '@statelyai/graph';
|
|
1002
|
+
*
|
|
1003
|
+
* const graph = createGraph({
|
|
1004
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1005
|
+
* edges: [
|
|
1006
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1007
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1008
|
+
* ],
|
|
1009
|
+
* initialNodeId: 'a',
|
|
1010
|
+
* });
|
|
1011
|
+
*
|
|
1012
|
+
* for (const path of genShortestPaths(graph)) {
|
|
1013
|
+
* console.log(path.steps.map(s => s.node.id));
|
|
1014
|
+
* }
|
|
1015
|
+
* ```
|
|
674
1016
|
*/
|
|
675
1017
|
function* genShortestPaths(graph, opts) {
|
|
676
1018
|
const idx = getIndex(graph);
|
|
@@ -681,25 +1023,101 @@ function* genShortestPaths(graph, opts) {
|
|
|
681
1023
|
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
682
1024
|
for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
|
|
683
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Returns all shortest paths from a source node as an array.
|
|
1028
|
+
* Delegates to `genShortestPaths` internally.
|
|
1029
|
+
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```ts
|
|
1032
|
+
* import { createGraph, getShortestPaths } from '@statelyai/graph';
|
|
1033
|
+
*
|
|
1034
|
+
* const graph = createGraph({
|
|
1035
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1036
|
+
* edges: [
|
|
1037
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1038
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1039
|
+
* ],
|
|
1040
|
+
* initialNodeId: 'a',
|
|
1041
|
+
* });
|
|
1042
|
+
*
|
|
1043
|
+
* const paths = getShortestPaths(graph);
|
|
1044
|
+
* // paths to 'b' and 'c' from 'a'
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
684
1047
|
function getShortestPaths(graph, opts) {
|
|
685
1048
|
return [...genShortestPaths(graph, opts)];
|
|
686
1049
|
}
|
|
687
1050
|
/**
|
|
688
|
-
* Returns a single shortest path from source to target, or undefined if unreachable.
|
|
1051
|
+
* Returns a single shortest path from source to target, or `undefined` if unreachable.
|
|
1052
|
+
*
|
|
1053
|
+
* @example
|
|
1054
|
+
* ```ts
|
|
1055
|
+
* import { createGraph, getShortestPath } from '@statelyai/graph';
|
|
1056
|
+
*
|
|
1057
|
+
* const graph = createGraph({
|
|
1058
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1059
|
+
* edges: [
|
|
1060
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1061
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1062
|
+
* ],
|
|
1063
|
+
* initialNodeId: 'a',
|
|
1064
|
+
* });
|
|
1065
|
+
*
|
|
1066
|
+
* const path = getShortestPath(graph, { to: 'c' });
|
|
1067
|
+
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
1068
|
+
* ```
|
|
689
1069
|
*/
|
|
690
1070
|
function getShortestPath(graph, opts) {
|
|
691
1071
|
for (const path of genShortestPaths(graph, opts)) return path;
|
|
692
1072
|
}
|
|
693
1073
|
/**
|
|
694
|
-
* Returns all simple (acyclic) paths from a source node.
|
|
695
|
-
*
|
|
1074
|
+
* Returns all simple (acyclic) paths from a source node as an array.
|
|
1075
|
+
* Delegates to `genSimplePaths` internally.
|
|
1076
|
+
*
|
|
1077
|
+
* @example
|
|
1078
|
+
* ```ts
|
|
1079
|
+
* import { createGraph, getSimplePaths } from '@statelyai/graph';
|
|
1080
|
+
*
|
|
1081
|
+
* const graph = createGraph({
|
|
1082
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1083
|
+
* edges: [
|
|
1084
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1085
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1086
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1087
|
+
* ],
|
|
1088
|
+
* initialNodeId: 'a',
|
|
1089
|
+
* });
|
|
1090
|
+
*
|
|
1091
|
+
* const paths = getSimplePaths(graph, { to: 'c' });
|
|
1092
|
+
* // two paths: a->b->c and a->c
|
|
1093
|
+
* ```
|
|
696
1094
|
*/
|
|
697
1095
|
function getSimplePaths(graph, opts) {
|
|
698
1096
|
return [...genSimplePaths(graph, opts)];
|
|
699
1097
|
}
|
|
700
1098
|
/**
|
|
701
|
-
* Lazily yields all simple (acyclic) paths from a source node.
|
|
1099
|
+
* Lazily yields all simple (acyclic) paths from a source node via DFS backtracking.
|
|
702
1100
|
* Use `getSimplePaths` for the full array.
|
|
1101
|
+
*
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```ts
|
|
1104
|
+
* import { createGraph, genSimplePaths } from '@statelyai/graph';
|
|
1105
|
+
*
|
|
1106
|
+
* const graph = createGraph({
|
|
1107
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1108
|
+
* edges: [
|
|
1109
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1110
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1111
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1112
|
+
* ],
|
|
1113
|
+
* initialNodeId: 'a',
|
|
1114
|
+
* });
|
|
1115
|
+
*
|
|
1116
|
+
* for (const path of genSimplePaths(graph, { to: 'c' })) {
|
|
1117
|
+
* console.log(path.steps.map(s => s.node.id));
|
|
1118
|
+
* // ['b', 'c'] or ['c']
|
|
1119
|
+
* }
|
|
1120
|
+
* ```
|
|
703
1121
|
*/
|
|
704
1122
|
function* genSimplePaths(graph, opts) {
|
|
705
1123
|
const idx = getIndex(graph);
|
|
@@ -741,11 +1159,49 @@ function* genSimplePaths(graph, opts) {
|
|
|
741
1159
|
yield* found;
|
|
742
1160
|
}
|
|
743
1161
|
/**
|
|
744
|
-
* Returns a single simple (acyclic) path from source to target, or undefined if unreachable.
|
|
1162
|
+
* Returns a single simple (acyclic) path from source to target, or `undefined` if unreachable.
|
|
1163
|
+
*
|
|
1164
|
+
* @example
|
|
1165
|
+
* ```ts
|
|
1166
|
+
* import { createGraph, getSimplePath } from '@statelyai/graph';
|
|
1167
|
+
*
|
|
1168
|
+
* const graph = createGraph({
|
|
1169
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1170
|
+
* edges: [
|
|
1171
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1172
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1173
|
+
* ],
|
|
1174
|
+
* initialNodeId: 'a',
|
|
1175
|
+
* });
|
|
1176
|
+
*
|
|
1177
|
+
* const path = getSimplePath(graph, { to: 'c' });
|
|
1178
|
+
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
1179
|
+
* ```
|
|
745
1180
|
*/
|
|
746
1181
|
function getSimplePath(graph, opts) {
|
|
747
1182
|
for (const path of genSimplePaths(graph, opts)) return path;
|
|
748
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Returns strongly connected components using Tarjan's algorithm.
|
|
1186
|
+
* Only meaningful for directed graphs.
|
|
1187
|
+
*
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```ts
|
|
1190
|
+
* import { createGraph, getStronglyConnectedComponents } from '@statelyai/graph';
|
|
1191
|
+
*
|
|
1192
|
+
* const graph = createGraph({
|
|
1193
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1194
|
+
* edges: [
|
|
1195
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1196
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1197
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1198
|
+
* ],
|
|
1199
|
+
* });
|
|
1200
|
+
*
|
|
1201
|
+
* const sccs = getStronglyConnectedComponents(graph);
|
|
1202
|
+
* // [[nodeA, nodeB], [nodeC]]
|
|
1203
|
+
* ```
|
|
1204
|
+
*/
|
|
749
1205
|
function getStronglyConnectedComponents(graph) {
|
|
750
1206
|
const idx = getIndex(graph);
|
|
751
1207
|
let indexCounter = 0;
|
|
@@ -784,12 +1240,49 @@ function getStronglyConnectedComponents(graph) {
|
|
|
784
1240
|
for (const n of graph.nodes) if (!nodeIndex.has(n.id)) strongconnect(n.id);
|
|
785
1241
|
return result;
|
|
786
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Returns all elementary cycles as an array of paths.
|
|
1245
|
+
* Delegates to `genCycles` internally.
|
|
1246
|
+
*
|
|
1247
|
+
* @example
|
|
1248
|
+
* ```ts
|
|
1249
|
+
* import { createGraph, getCycles } from '@statelyai/graph';
|
|
1250
|
+
*
|
|
1251
|
+
* const graph = createGraph({
|
|
1252
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1253
|
+
* edges: [
|
|
1254
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1255
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1256
|
+
* ],
|
|
1257
|
+
* });
|
|
1258
|
+
*
|
|
1259
|
+
* const cycles = getCycles(graph);
|
|
1260
|
+
* // one cycle: a -> b -> a
|
|
1261
|
+
* ```
|
|
1262
|
+
*/
|
|
787
1263
|
function getCycles(graph) {
|
|
788
1264
|
return [...genCycles(graph)];
|
|
789
1265
|
}
|
|
790
1266
|
/**
|
|
791
|
-
* Lazily yields cycles one at a time.
|
|
1267
|
+
* Lazily yields elementary cycles one at a time.
|
|
792
1268
|
* Use `getCycles` for the full array.
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```ts
|
|
1272
|
+
* import { createGraph, genCycles } from '@statelyai/graph';
|
|
1273
|
+
*
|
|
1274
|
+
* const graph = createGraph({
|
|
1275
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1276
|
+
* edges: [
|
|
1277
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1278
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1279
|
+
* ],
|
|
1280
|
+
* });
|
|
1281
|
+
*
|
|
1282
|
+
* for (const cycle of genCycles(graph)) {
|
|
1283
|
+
* console.log(cycle.steps.map(s => s.node.id)); // ['b', 'a']
|
|
1284
|
+
* }
|
|
1285
|
+
* ```
|
|
793
1286
|
*/
|
|
794
1287
|
function* genCycles(graph) {
|
|
795
1288
|
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
@@ -912,6 +1405,23 @@ function getNeighborEdgesAll(graph, nodeId) {
|
|
|
912
1405
|
/**
|
|
913
1406
|
* Returns a single canonical preorder (DFS visit-order) sequence.
|
|
914
1407
|
* Visits neighbors in the order they appear in the adjacency list.
|
|
1408
|
+
*
|
|
1409
|
+
* @example
|
|
1410
|
+
* ```ts
|
|
1411
|
+
* import { createGraph, getPreorder } from '@statelyai/graph';
|
|
1412
|
+
*
|
|
1413
|
+
* const graph = createGraph({
|
|
1414
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1415
|
+
* edges: [
|
|
1416
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1417
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1418
|
+
* ],
|
|
1419
|
+
* initialNodeId: 'a',
|
|
1420
|
+
* });
|
|
1421
|
+
*
|
|
1422
|
+
* const order = getPreorder(graph);
|
|
1423
|
+
* // [nodeA, nodeB, nodeC]
|
|
1424
|
+
* ```
|
|
915
1425
|
*/
|
|
916
1426
|
function getPreorder(graph, opts) {
|
|
917
1427
|
const idx = getIndex(graph);
|
|
@@ -938,6 +1448,23 @@ function getPreorder(graph, opts) {
|
|
|
938
1448
|
/**
|
|
939
1449
|
* Returns a single canonical postorder (DFS finish-order) sequence.
|
|
940
1450
|
* Visits neighbors in the order they appear in the adjacency list.
|
|
1451
|
+
*
|
|
1452
|
+
* @example
|
|
1453
|
+
* ```ts
|
|
1454
|
+
* import { createGraph, getPostorder } from '@statelyai/graph';
|
|
1455
|
+
*
|
|
1456
|
+
* const graph = createGraph({
|
|
1457
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1458
|
+
* edges: [
|
|
1459
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1460
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1461
|
+
* ],
|
|
1462
|
+
* initialNodeId: 'a',
|
|
1463
|
+
* });
|
|
1464
|
+
*
|
|
1465
|
+
* const order = getPostorder(graph);
|
|
1466
|
+
* // [nodeC, nodeB, nodeA]
|
|
1467
|
+
* ```
|
|
941
1468
|
*/
|
|
942
1469
|
function getPostorder(graph, opts) {
|
|
943
1470
|
const idx = getIndex(graph);
|
|
@@ -960,11 +1487,49 @@ function getPostorder(graph, opts) {
|
|
|
960
1487
|
}
|
|
961
1488
|
return result;
|
|
962
1489
|
}
|
|
963
|
-
/**
|
|
1490
|
+
/**
|
|
1491
|
+
* Returns all possible preorder sequences as an array. Can be exponential -- prefer `genPreorders`.
|
|
1492
|
+
*
|
|
1493
|
+
* @example
|
|
1494
|
+
* ```ts
|
|
1495
|
+
* import { createGraph, getPreorders } from '@statelyai/graph';
|
|
1496
|
+
*
|
|
1497
|
+
* const graph = createGraph({
|
|
1498
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1499
|
+
* edges: [
|
|
1500
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1501
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1502
|
+
* ],
|
|
1503
|
+
* initialNodeId: 'a',
|
|
1504
|
+
* });
|
|
1505
|
+
*
|
|
1506
|
+
* const allOrders = getPreorders(graph);
|
|
1507
|
+
* // [[nodeA, nodeB, nodeC], [nodeA, nodeC, nodeB]]
|
|
1508
|
+
* ```
|
|
1509
|
+
*/
|
|
964
1510
|
function getPreorders(graph, opts) {
|
|
965
1511
|
return [...genPreorders(graph, opts)];
|
|
966
1512
|
}
|
|
967
|
-
/**
|
|
1513
|
+
/**
|
|
1514
|
+
* Returns all possible postorder sequences as an array. Can be exponential -- prefer `genPostorders`.
|
|
1515
|
+
*
|
|
1516
|
+
* @example
|
|
1517
|
+
* ```ts
|
|
1518
|
+
* import { createGraph, getPostorders } from '@statelyai/graph';
|
|
1519
|
+
*
|
|
1520
|
+
* const graph = createGraph({
|
|
1521
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1522
|
+
* edges: [
|
|
1523
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1524
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1525
|
+
* ],
|
|
1526
|
+
* initialNodeId: 'a',
|
|
1527
|
+
* });
|
|
1528
|
+
*
|
|
1529
|
+
* const allOrders = getPostorders(graph);
|
|
1530
|
+
* // [[nodeB, nodeC, nodeA], [nodeC, nodeB, nodeA]]
|
|
1531
|
+
* ```
|
|
1532
|
+
*/
|
|
968
1533
|
function getPostorders(graph, opts) {
|
|
969
1534
|
return [...genPostorders(graph, opts)];
|
|
970
1535
|
}
|
|
@@ -972,6 +1537,25 @@ function getPostorders(graph, opts) {
|
|
|
972
1537
|
* Lazily yields all possible preorder (DFS visit-order) sequences.
|
|
973
1538
|
* Different neighbor exploration orders yield different sequences.
|
|
974
1539
|
* Use `getPreorder()` for a single canonical ordering.
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* ```ts
|
|
1543
|
+
* import { createGraph, genPreorders } from '@statelyai/graph';
|
|
1544
|
+
*
|
|
1545
|
+
* const graph = createGraph({
|
|
1546
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1547
|
+
* edges: [
|
|
1548
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1549
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1550
|
+
* ],
|
|
1551
|
+
* initialNodeId: 'a',
|
|
1552
|
+
* });
|
|
1553
|
+
*
|
|
1554
|
+
* for (const order of genPreorders(graph)) {
|
|
1555
|
+
* console.log(order.map(n => n.id));
|
|
1556
|
+
* // ['a', 'b', 'c'] or ['a', 'c', 'b']
|
|
1557
|
+
* }
|
|
1558
|
+
* ```
|
|
975
1559
|
*/
|
|
976
1560
|
function* genPreorders(graph, opts) {
|
|
977
1561
|
const idx = getIndex(graph);
|
|
@@ -1017,6 +1601,25 @@ function* genPreorders(graph, opts) {
|
|
|
1017
1601
|
* Lazily yields all possible postorder (DFS finish-order) sequences.
|
|
1018
1602
|
* Different neighbor exploration orders yield different sequences.
|
|
1019
1603
|
* Use `getPostorder()` for a single canonical ordering.
|
|
1604
|
+
*
|
|
1605
|
+
* @example
|
|
1606
|
+
* ```ts
|
|
1607
|
+
* import { createGraph, genPostorders } from '@statelyai/graph';
|
|
1608
|
+
*
|
|
1609
|
+
* const graph = createGraph({
|
|
1610
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1611
|
+
* edges: [
|
|
1612
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1613
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1614
|
+
* ],
|
|
1615
|
+
* initialNodeId: 'a',
|
|
1616
|
+
* });
|
|
1617
|
+
*
|
|
1618
|
+
* for (const order of genPostorders(graph)) {
|
|
1619
|
+
* console.log(order.map(n => n.id));
|
|
1620
|
+
* // ['b', 'c', 'a'] or ['c', 'b', 'a']
|
|
1621
|
+
* }
|
|
1622
|
+
* ```
|
|
1020
1623
|
*/
|
|
1021
1624
|
function* genPostorders(graph, opts) {
|
|
1022
1625
|
const idx = getIndex(graph);
|
|
@@ -1061,6 +1664,26 @@ function* genPostorders(graph, opts) {
|
|
|
1061
1664
|
* Returns a minimum spanning tree of the graph.
|
|
1062
1665
|
* Only meaningful for connected undirected graphs (or the component reachable
|
|
1063
1666
|
* from an arbitrary start node in directed graphs).
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* import { createGraph, getMinimumSpanningTree } from '@statelyai/graph';
|
|
1671
|
+
*
|
|
1672
|
+
* const graph = createGraph({
|
|
1673
|
+
* type: 'undirected',
|
|
1674
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1675
|
+
* edges: [
|
|
1676
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b', data: { weight: 1 } },
|
|
1677
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c', data: { weight: 2 } },
|
|
1678
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c', data: { weight: 3 } },
|
|
1679
|
+
* ],
|
|
1680
|
+
* });
|
|
1681
|
+
*
|
|
1682
|
+
* const mst = getMinimumSpanningTree(graph, {
|
|
1683
|
+
* getWeight: (e) => e.data.weight,
|
|
1684
|
+
* });
|
|
1685
|
+
* // mst has edges 'ab' and 'bc' (total weight 3)
|
|
1686
|
+
* ```
|
|
1064
1687
|
*/
|
|
1065
1688
|
function getMinimumSpanningTree(graph, opts) {
|
|
1066
1689
|
const algorithm = opts?.algorithm ?? "prim";
|
|
@@ -1157,7 +1780,23 @@ function kruskalMST(graph, getWeight) {
|
|
|
1157
1780
|
/**
|
|
1158
1781
|
* Returns shortest paths between all pairs of nodes.
|
|
1159
1782
|
* Algorithm 'dijkstra' (default): runs getShortestPaths per source node.
|
|
1160
|
-
* Algorithm 'floyd-warshall': classic O(V
|
|
1783
|
+
* Algorithm 'floyd-warshall': classic O(V^3) dynamic programming.
|
|
1784
|
+
*
|
|
1785
|
+
* @example
|
|
1786
|
+
* ```ts
|
|
1787
|
+
* import { createGraph, getAllPairsShortestPaths } from '@statelyai/graph';
|
|
1788
|
+
*
|
|
1789
|
+
* const graph = createGraph({
|
|
1790
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1791
|
+
* edges: [
|
|
1792
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1793
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1794
|
+
* ],
|
|
1795
|
+
* });
|
|
1796
|
+
*
|
|
1797
|
+
* const allPaths = getAllPairsShortestPaths(graph);
|
|
1798
|
+
* // paths for every reachable (source, target) pair
|
|
1799
|
+
* ```
|
|
1161
1800
|
*/
|
|
1162
1801
|
function getAllPairsShortestPaths(graph, opts) {
|
|
1163
1802
|
if ((opts?.algorithm ?? "dijkstra") === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
@@ -1264,6 +1903,25 @@ function fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, targe
|
|
|
1264
1903
|
*
|
|
1265
1904
|
* Steps are concatenated: head.steps ++ tail.steps (tail already starts
|
|
1266
1905
|
* from the overlap node, so no slicing is needed).
|
|
1906
|
+
*
|
|
1907
|
+
* @example
|
|
1908
|
+
* ```ts
|
|
1909
|
+
* import { createGraph, getShortestPath, joinPaths } from '@statelyai/graph';
|
|
1910
|
+
*
|
|
1911
|
+
* const graph = createGraph({
|
|
1912
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1913
|
+
* edges: [
|
|
1914
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1915
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1916
|
+
* ],
|
|
1917
|
+
* initialNodeId: 'a',
|
|
1918
|
+
* });
|
|
1919
|
+
*
|
|
1920
|
+
* const ab = getShortestPath(graph, { to: 'b' })!;
|
|
1921
|
+
* const bc = getShortestPath(graph, { from: 'b', to: 'c' })!;
|
|
1922
|
+
* const ac = joinPaths(ab, bc);
|
|
1923
|
+
* // ac: a -> b -> c
|
|
1924
|
+
* ```
|
|
1267
1925
|
*/
|
|
1268
1926
|
function joinPaths(headPath, tailPath) {
|
|
1269
1927
|
const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
|