@statelyai/graph 0.1.0 → 0.3.1
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 +65 -15
- package/dist/{adjacency-list-CXpOCibq.mjs → adjacency-list-ITO40kmn.mjs} +34 -1
- package/dist/{algorithms-R35X6ro4.mjs → algorithms-NWSB2RWj.mjs} +753 -19
- package/dist/algorithms.d.mts +488 -11
- package/dist/algorithms.mjs +2 -2
- package/dist/converter-CchokMDg.mjs +67 -0
- package/dist/edge-list-CgX6bBIF.mjs +71 -0
- package/dist/formats/adjacency-list/index.d.mts +44 -0
- package/dist/formats/adjacency-list/index.mjs +3 -0
- package/dist/formats/converter/index.d.mts +61 -0
- package/dist/formats/converter/index.mjs +3 -0
- package/dist/formats/cytoscape/index.d.mts +83 -0
- package/dist/formats/cytoscape/index.mjs +135 -0
- package/dist/formats/d3/index.d.mts +68 -0
- package/dist/formats/d3/index.mjs +111 -0
- package/dist/formats/dot/index.d.mts +63 -0
- package/dist/formats/dot/index.mjs +288 -0
- package/dist/formats/edge-list/index.d.mts +43 -0
- package/dist/formats/edge-list/index.mjs +3 -0
- package/dist/formats/gexf/index.d.mts +9 -0
- package/dist/formats/gexf/index.mjs +249 -0
- package/dist/formats/gml/index.d.mts +65 -0
- package/dist/formats/gml/index.mjs +291 -0
- package/dist/formats/graphml/index.d.mts +9 -0
- package/dist/{graphml-CUTNRXqd.mjs → formats/graphml/index.mjs} +18 -4
- package/dist/formats/jgf/index.d.mts +79 -0
- package/dist/formats/jgf/index.mjs +134 -0
- package/dist/formats/mermaid/index.d.mts +381 -0
- package/dist/formats/mermaid/index.mjs +2237 -0
- package/dist/formats/tgf/index.d.mts +54 -0
- package/dist/formats/tgf/index.mjs +111 -0
- package/dist/index.d.mts +332 -21
- package/dist/index.mjs +117 -13
- package/dist/{indexing-BHg1VhqN.mjs → indexing-eNDrXdDA.mjs} +31 -2
- package/dist/queries.d.mts +430 -9
- package/dist/queries.mjs +472 -9
- package/dist/{types-XV3S5Jnh.d.mts → types-BDXC1O5b.d.mts} +37 -2
- package/package.json +43 -17
- package/dist/adjacency-list-DW-lAUe8.d.mts +0 -10
- package/dist/dot-BRtq3e3c.mjs +0 -59
- package/dist/dot-HmJeUMsj.d.mts +0 -6
- package/dist/edge-list-BRujEnnU.mjs +0 -39
- package/dist/edge-list-CJmfoNu2.d.mts +0 -10
- package/dist/formats/adjacency-list.d.mts +0 -2
- package/dist/formats/adjacency-list.mjs +0 -3
- package/dist/formats/dot.d.mts +0 -2
- package/dist/formats/dot.mjs +0 -3
- package/dist/formats/edge-list.d.mts +0 -2
- package/dist/formats/edge-list.mjs +0 -3
- package/dist/formats/graphml.d.mts +0 -2
- package/dist/formats/graphml.mjs +0 -3
- package/dist/graphml-CMjPzSfY.d.mts +0 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-
|
|
1
|
+
import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-eNDrXdDA.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/graph.ts
|
|
4
4
|
function resolveNode(config) {
|
|
@@ -36,7 +36,17 @@ function resolveEdge(config) {
|
|
|
36
36
|
if (config.style !== void 0) edge.style = config.style;
|
|
37
37
|
return edge;
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Create a graph from a config. Resolves defaults for all fields.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const graph = createGraph({
|
|
45
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
46
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
40
50
|
function createGraph(config) {
|
|
41
51
|
const graph = {
|
|
42
52
|
id: config?.id ?? "",
|
|
@@ -50,7 +60,18 @@ function createGraph(config) {
|
|
|
50
60
|
if (config?.style !== void 0) graph.style = config.style;
|
|
51
61
|
return graph;
|
|
52
62
|
}
|
|
53
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Create a visual graph with required position/size on all nodes and edges.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const graph = createVisualGraph({
|
|
69
|
+
* nodes: [{ id: 'a', x: 0, y: 0, width: 100, height: 50 }],
|
|
70
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', x: 0, y: 0, width: 0, height: 0 }],
|
|
71
|
+
* });
|
|
72
|
+
* // graph.nodes[0].x === 0
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
54
75
|
function createVisualGraph(config) {
|
|
55
76
|
const base = createGraph(config);
|
|
56
77
|
return {
|
|
@@ -62,7 +83,7 @@ function createVisualGraph(config) {
|
|
|
62
83
|
y: n.y ?? 0,
|
|
63
84
|
width: n.width ?? 0,
|
|
64
85
|
height: n.height ?? 0,
|
|
65
|
-
shape: n.shape
|
|
86
|
+
...n.shape !== void 0 && { shape: n.shape }
|
|
66
87
|
})),
|
|
67
88
|
edges: base.edges.map((e) => ({
|
|
68
89
|
...e,
|
|
@@ -73,27 +94,161 @@ function createVisualGraph(config) {
|
|
|
73
94
|
}))
|
|
74
95
|
};
|
|
75
96
|
}
|
|
76
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Create a graph by BFS exploration of a transition function.
|
|
99
|
+
* Each unique state becomes a node; each (state, event) -> nextState becomes an edge.
|
|
100
|
+
*
|
|
101
|
+
* - Node IDs are determined by `serializeState` (default: `JSON.stringify`).
|
|
102
|
+
* - Edge IDs use the format `sourceId|serializedEvent|targetId` for uniqueness
|
|
103
|
+
* and debuggability. Edge labels are just the serialized event string.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const graph = createGraphFromTransition(
|
|
108
|
+
* (state, event) => {
|
|
109
|
+
* if (state === 'green' && event === 'TIMER') return 'yellow';
|
|
110
|
+
* if (state === 'yellow' && event === 'TIMER') return 'red';
|
|
111
|
+
* if (state === 'red' && event === 'TIMER') return 'green';
|
|
112
|
+
* return state;
|
|
113
|
+
* },
|
|
114
|
+
* {
|
|
115
|
+
* initialState: 'green',
|
|
116
|
+
* events: ['TIMER'],
|
|
117
|
+
* serializeState: (s) => s,
|
|
118
|
+
* serializeEvent: (e) => e,
|
|
119
|
+
* },
|
|
120
|
+
* );
|
|
121
|
+
* // graph.nodes.length === 3
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
function createGraphFromTransition(transition, options) {
|
|
125
|
+
const serializeState = options.serializeState ?? JSON.stringify;
|
|
126
|
+
const serializeEvent = options.serializeEvent ?? JSON.stringify;
|
|
127
|
+
const limit = options.limit ?? Infinity;
|
|
128
|
+
const getEvents = typeof options.events === "function" ? options.events : () => options.events;
|
|
129
|
+
const nodes = [];
|
|
130
|
+
const edges = [];
|
|
131
|
+
const visited = /* @__PURE__ */ new Set();
|
|
132
|
+
const edgeSet = /* @__PURE__ */ new Set();
|
|
133
|
+
const queue = [options.initialState];
|
|
134
|
+
const initialStateId = serializeState(options.initialState);
|
|
135
|
+
visited.add(initialStateId);
|
|
136
|
+
nodes.push({
|
|
137
|
+
id: initialStateId,
|
|
138
|
+
label: initialStateId,
|
|
139
|
+
data: options.initialState
|
|
140
|
+
});
|
|
141
|
+
let iterations = 0;
|
|
142
|
+
while (queue.length > 0) {
|
|
143
|
+
const state = queue.shift();
|
|
144
|
+
const stateId = serializeState(state);
|
|
145
|
+
if (++iterations > limit) throw new Error("Traversal limit exceeded");
|
|
146
|
+
if (options.stopWhen?.(state)) continue;
|
|
147
|
+
const events = getEvents(state);
|
|
148
|
+
for (const event of events) {
|
|
149
|
+
const nextState = transition(state, event);
|
|
150
|
+
const nextStateId = serializeState(nextState);
|
|
151
|
+
const eventStr = serializeEvent(event);
|
|
152
|
+
if (!visited.has(nextStateId)) {
|
|
153
|
+
visited.add(nextStateId);
|
|
154
|
+
nodes.push({
|
|
155
|
+
id: nextStateId,
|
|
156
|
+
label: nextStateId,
|
|
157
|
+
data: nextState
|
|
158
|
+
});
|
|
159
|
+
queue.push(nextState);
|
|
160
|
+
}
|
|
161
|
+
const edgeKey = `${stateId}|${eventStr}|${nextStateId}`;
|
|
162
|
+
if (!edgeSet.has(edgeKey)) {
|
|
163
|
+
edgeSet.add(edgeKey);
|
|
164
|
+
edges.push({
|
|
165
|
+
id: edgeKey,
|
|
166
|
+
sourceId: stateId,
|
|
167
|
+
targetId: nextStateId,
|
|
168
|
+
label: eventStr,
|
|
169
|
+
data: event
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return createGraph({
|
|
175
|
+
id: options.id ?? "",
|
|
176
|
+
type: "directed",
|
|
177
|
+
initialNodeId: initialStateId,
|
|
178
|
+
nodes,
|
|
179
|
+
edges
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get a node by id, or `undefined` if not found.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
188
|
+
* const node = getNode(graph, 'a'); // GraphNode
|
|
189
|
+
* const missing = getNode(graph, 'z'); // undefined
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
77
192
|
function getNode(graph, id) {
|
|
78
193
|
const arrayIdx = getIndex(graph).nodeById.get(id);
|
|
79
194
|
return arrayIdx !== void 0 ? graph.nodes[arrayIdx] : void 0;
|
|
80
195
|
}
|
|
81
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Get an edge by id, or `undefined` if not found.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* const graph = createGraph({
|
|
202
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
203
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
204
|
+
* });
|
|
205
|
+
* const edge = getEdge(graph, 'e1'); // GraphEdge
|
|
206
|
+
* const missing = getEdge(graph, 'z'); // undefined
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
82
209
|
function getEdge(graph, id) {
|
|
83
210
|
const arrayIdx = getIndex(graph).edgeById.get(id);
|
|
84
211
|
return arrayIdx !== void 0 ? graph.edges[arrayIdx] : void 0;
|
|
85
212
|
}
|
|
86
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Check if a node exists in the graph.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
219
|
+
* hasNode(graph, 'a'); // true
|
|
220
|
+
* hasNode(graph, 'z'); // false
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
87
223
|
function hasNode(graph, id) {
|
|
88
224
|
return getIndex(graph).nodeById.has(id);
|
|
89
225
|
}
|
|
90
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* Check if an edge exists in the graph.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const graph = createGraph({
|
|
232
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
233
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
234
|
+
* });
|
|
235
|
+
* hasEdge(graph, 'e1'); // true
|
|
236
|
+
* hasEdge(graph, 'z'); // false
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
91
239
|
function hasEdge(graph, id) {
|
|
92
240
|
return getIndex(graph).edgeById.has(id);
|
|
93
241
|
}
|
|
94
242
|
/**
|
|
95
243
|
* **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place.
|
|
96
244
|
* @returns The resolved node that was added.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* const graph = createGraph();
|
|
249
|
+
* const node = addNode(graph, { id: 'a', label: 'Node A' });
|
|
250
|
+
* // graph.nodes.length === 1
|
|
251
|
+
* ```
|
|
97
252
|
*/
|
|
98
253
|
function addNode(graph, config) {
|
|
99
254
|
const idx = getIndex(graph);
|
|
@@ -106,6 +261,13 @@ function addNode(graph, config) {
|
|
|
106
261
|
/**
|
|
107
262
|
* **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place.
|
|
108
263
|
* @returns The resolved edge that was added.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }, { id: 'b' }] });
|
|
268
|
+
* const edge = addEdge(graph, { id: 'e1', sourceId: 'a', targetId: 'b' });
|
|
269
|
+
* // graph.edges.length === 1
|
|
270
|
+
* ```
|
|
109
271
|
*/
|
|
110
272
|
function addEdge(graph, config) {
|
|
111
273
|
const idx = getIndex(graph);
|
|
@@ -122,6 +284,16 @@ function addEdge(graph, config) {
|
|
|
122
284
|
*
|
|
123
285
|
* By default, children are deleted recursively.
|
|
124
286
|
* With `{ reparent: true }`, children are re-parented to the deleted node's parent.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* const graph = createGraph({
|
|
291
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
292
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
293
|
+
* });
|
|
294
|
+
* deleteNode(graph, 'a');
|
|
295
|
+
* // graph.nodes.length === 1, edge e1 also removed
|
|
296
|
+
* ```
|
|
125
297
|
*/
|
|
126
298
|
function deleteNode(graph, id, opts) {
|
|
127
299
|
const node = getNode(graph, id);
|
|
@@ -139,6 +311,16 @@ function deleteNode(graph, id, opts) {
|
|
|
139
311
|
}
|
|
140
312
|
/**
|
|
141
313
|
* **Mutable.** Delete an edge. Mutates `graph.edges` in place.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* const graph = createGraph({
|
|
318
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
319
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
320
|
+
* });
|
|
321
|
+
* deleteEdge(graph, 'e1');
|
|
322
|
+
* // graph.edges.length === 0
|
|
323
|
+
* ```
|
|
142
324
|
*/
|
|
143
325
|
function deleteEdge(graph, id) {
|
|
144
326
|
if (!hasEdge(graph, id)) throw new Error(`Edge "${id}" does not exist`);
|
|
@@ -148,6 +330,13 @@ function deleteEdge(graph, id) {
|
|
|
148
330
|
/**
|
|
149
331
|
* **Mutable.** Update a node in place.
|
|
150
332
|
* @returns The updated node.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
|
|
337
|
+
* const updated = updateNode(graph, 'a', { label: 'new' });
|
|
338
|
+
* // updated.label === 'new'
|
|
339
|
+
* ```
|
|
151
340
|
*/
|
|
152
341
|
function updateNode(graph, id, update) {
|
|
153
342
|
const idx = getIndex(graph);
|
|
@@ -172,6 +361,16 @@ function updateNode(graph, id, update) {
|
|
|
172
361
|
/**
|
|
173
362
|
* **Mutable.** Update an edge in place.
|
|
174
363
|
* @returns The updated edge.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* const graph = createGraph({
|
|
368
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
369
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
|
|
370
|
+
* });
|
|
371
|
+
* const updated = updateEdge(graph, 'e1', { label: 'new' });
|
|
372
|
+
* // updated.label === 'new'
|
|
373
|
+
* ```
|
|
175
374
|
*/
|
|
176
375
|
function updateEdge(graph, id, update) {
|
|
177
376
|
const idx = getIndex(graph);
|
|
@@ -196,6 +395,16 @@ function updateEdge(graph, id, update) {
|
|
|
196
395
|
/**
|
|
197
396
|
* **Mutable.** Add multiple nodes and edges to the graph.
|
|
198
397
|
* Nodes are added first, then edges (so edges can reference new nodes).
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* const graph = createGraph();
|
|
402
|
+
* addEntities(graph, {
|
|
403
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
404
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
405
|
+
* });
|
|
406
|
+
* // graph.nodes.length === 2, graph.edges.length === 1
|
|
407
|
+
* ```
|
|
199
408
|
*/
|
|
200
409
|
function addEntities(graph, entities) {
|
|
201
410
|
for (const nodeConfig of entities.nodes ?? []) addNode(graph, nodeConfig);
|
|
@@ -204,6 +413,16 @@ function addEntities(graph, entities) {
|
|
|
204
413
|
/**
|
|
205
414
|
* **Mutable.** Delete entities by id(s). Automatically detects whether each id
|
|
206
415
|
* is a node or edge. Node deletions cascade to children and connected edges.
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```ts
|
|
419
|
+
* const graph = createGraph({
|
|
420
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
421
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
422
|
+
* });
|
|
423
|
+
* deleteEntities(graph, ['a', 'e1']);
|
|
424
|
+
* // graph.nodes.length === 1, graph.edges.length === 0
|
|
425
|
+
* ```
|
|
207
426
|
*/
|
|
208
427
|
function deleteEntities(graph, ids, opts) {
|
|
209
428
|
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
@@ -213,6 +432,18 @@ function deleteEntities(graph, ids, opts) {
|
|
|
213
432
|
/**
|
|
214
433
|
* **Mutable.** Update multiple nodes and edges in place.
|
|
215
434
|
* Each entry must include an `id` to identify which entity to update.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```ts
|
|
438
|
+
* const graph = createGraph({
|
|
439
|
+
* nodes: [{ id: 'a', label: 'old' }],
|
|
440
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', label: 'old' }],
|
|
441
|
+
* });
|
|
442
|
+
* updateEntities(graph, {
|
|
443
|
+
* nodes: [{ id: 'a', label: 'new' }],
|
|
444
|
+
* edges: [{ id: 'e1', label: 'new' }],
|
|
445
|
+
* });
|
|
446
|
+
* ```
|
|
216
447
|
*/
|
|
217
448
|
function updateEntities(graph, updates) {
|
|
218
449
|
for (const nodeUpdate of updates.nodes ?? []) {
|
|
@@ -227,13 +458,33 @@ function updateEntities(graph, updates) {
|
|
|
227
458
|
/**
|
|
228
459
|
* OOP wrapper around a plain `Graph` object.
|
|
229
460
|
* Delegates to the standalone mutable functions.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```ts
|
|
464
|
+
* const instance = new GraphInstance({
|
|
465
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
466
|
+
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
467
|
+
* });
|
|
468
|
+
* instance.addNode({ id: 'c' });
|
|
469
|
+
* instance.hasNode('c'); // true
|
|
470
|
+
* instance.toJSON(); // plain Graph object
|
|
471
|
+
* ```
|
|
230
472
|
*/
|
|
231
473
|
var GraphInstance = class GraphInstance {
|
|
232
474
|
graph;
|
|
233
475
|
constructor(config) {
|
|
234
476
|
this.graph = createGraph(config);
|
|
235
477
|
}
|
|
236
|
-
/**
|
|
478
|
+
/**
|
|
479
|
+
* Wrap an existing plain graph object.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
484
|
+
* const instance = GraphInstance.from(graph);
|
|
485
|
+
* instance.hasNode('a'); // true
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
237
488
|
static from(graph) {
|
|
238
489
|
const instance = Object.create(GraphInstance.prototype);
|
|
239
490
|
instance.graph = graph;
|
|
@@ -311,6 +562,23 @@ function collectDescendants(graph, id) {
|
|
|
311
562
|
|
|
312
563
|
//#endregion
|
|
313
564
|
//#region src/algorithms.ts
|
|
565
|
+
/**
|
|
566
|
+
* Breadth-first traversal generator yielding nodes level by level.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* import { createGraph, bfs } from '@statelyai/graph';
|
|
571
|
+
*
|
|
572
|
+
* const graph = createGraph({
|
|
573
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
574
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
575
|
+
* });
|
|
576
|
+
*
|
|
577
|
+
* for (const node of bfs(graph, 'a')) {
|
|
578
|
+
* console.log(node.id); // 'a', 'b', 'c'
|
|
579
|
+
* }
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
314
582
|
function* bfs(graph, startId) {
|
|
315
583
|
const idx = getIndex(graph);
|
|
316
584
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -327,6 +595,23 @@ function* bfs(graph, startId) {
|
|
|
327
595
|
}
|
|
328
596
|
}
|
|
329
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Depth-first traversal generator yielding nodes as visited.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* import { createGraph, dfs } from '@statelyai/graph';
|
|
604
|
+
*
|
|
605
|
+
* const graph = createGraph({
|
|
606
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
607
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }],
|
|
608
|
+
* });
|
|
609
|
+
*
|
|
610
|
+
* for (const node of dfs(graph, 'a')) {
|
|
611
|
+
* console.log(node.id); // 'a', 'b', 'c'
|
|
612
|
+
* }
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
330
615
|
function* dfs(graph, startId) {
|
|
331
616
|
const idx = getIndex(graph);
|
|
332
617
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -358,6 +643,21 @@ function getSuccessorIds(graph, nodeId) {
|
|
|
358
643
|
const idx = getIndex(graph);
|
|
359
644
|
return (idx.outEdges.get(nodeId) ?? []).map((eid) => graph.edges[idx.edgeById.get(eid)].targetId);
|
|
360
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Checks whether the graph contains no cycles.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```ts
|
|
651
|
+
* import { createGraph, isAcyclic } from '@statelyai/graph';
|
|
652
|
+
*
|
|
653
|
+
* const dag = createGraph({
|
|
654
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
655
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
656
|
+
* });
|
|
657
|
+
*
|
|
658
|
+
* isAcyclic(dag); // true
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
361
661
|
function isAcyclic(graph) {
|
|
362
662
|
if (graph.type === "undirected") return isAcyclicUndirected(graph);
|
|
363
663
|
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
@@ -402,6 +702,23 @@ function isAcyclicUndirected(graph) {
|
|
|
402
702
|
for (const n of graph.nodes) if (!visited.has(n.id) && hasCycle(n.id, null)) return false;
|
|
403
703
|
return true;
|
|
404
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Returns connected components as arrays of nodes.
|
|
707
|
+
* Treats all edges as undirected for connectivity.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```ts
|
|
711
|
+
* import { createGraph, getConnectedComponents } from '@statelyai/graph';
|
|
712
|
+
*
|
|
713
|
+
* const graph = createGraph({
|
|
714
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
715
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
716
|
+
* });
|
|
717
|
+
*
|
|
718
|
+
* const components = getConnectedComponents(graph);
|
|
719
|
+
* // [[nodeA, nodeB], [nodeC]]
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
405
722
|
function getConnectedComponents(graph) {
|
|
406
723
|
const idx = getIndex(graph);
|
|
407
724
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -438,6 +755,25 @@ function getConnectedComponents(graph) {
|
|
|
438
755
|
}
|
|
439
756
|
return components;
|
|
440
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Returns a topological ordering of nodes, or `null` if the graph is cyclic.
|
|
760
|
+
*
|
|
761
|
+
* @example
|
|
762
|
+
* ```ts
|
|
763
|
+
* import { createGraph, getTopologicalSort } from '@statelyai/graph';
|
|
764
|
+
*
|
|
765
|
+
* const graph = createGraph({
|
|
766
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
767
|
+
* edges: [
|
|
768
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
769
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
770
|
+
* ],
|
|
771
|
+
* });
|
|
772
|
+
*
|
|
773
|
+
* const sorted = getTopologicalSort(graph);
|
|
774
|
+
* // [nodeA, nodeB, nodeC]
|
|
775
|
+
* ```
|
|
776
|
+
*/
|
|
441
777
|
function getTopologicalSort(graph) {
|
|
442
778
|
const idx = getIndex(graph);
|
|
443
779
|
const inDeg = /* @__PURE__ */ new Map();
|
|
@@ -462,16 +798,65 @@ function getTopologicalSort(graph) {
|
|
|
462
798
|
if (result.length !== graph.nodes.length) return null;
|
|
463
799
|
return result;
|
|
464
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Checks whether a path exists between two nodes.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```ts
|
|
806
|
+
* import { createGraph, hasPath } from '@statelyai/graph';
|
|
807
|
+
*
|
|
808
|
+
* const graph = createGraph({
|
|
809
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
810
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
811
|
+
* });
|
|
812
|
+
*
|
|
813
|
+
* hasPath(graph, 'a', 'b'); // true
|
|
814
|
+
* hasPath(graph, 'a', 'c'); // false
|
|
815
|
+
* ```
|
|
816
|
+
*/
|
|
465
817
|
function hasPath(graph, sourceId, targetId) {
|
|
466
818
|
return getShortestPaths(graph, {
|
|
467
819
|
from: sourceId,
|
|
468
820
|
to: targetId
|
|
469
821
|
}).length > 0;
|
|
470
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Checks whether the graph is connected (all nodes reachable from any node).
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```ts
|
|
828
|
+
* import { createGraph, isConnected } from '@statelyai/graph';
|
|
829
|
+
*
|
|
830
|
+
* const graph = createGraph({
|
|
831
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
832
|
+
* edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }],
|
|
833
|
+
* });
|
|
834
|
+
*
|
|
835
|
+
* isConnected(graph); // true
|
|
836
|
+
* ```
|
|
837
|
+
*/
|
|
471
838
|
function isConnected(graph) {
|
|
472
839
|
if (graph.nodes.length === 0) return true;
|
|
473
840
|
return getConnectedComponents(graph).length <= 1;
|
|
474
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Checks whether the graph is a tree (connected and acyclic).
|
|
844
|
+
*
|
|
845
|
+
* @example
|
|
846
|
+
* ```ts
|
|
847
|
+
* import { createGraph, isTree } from '@statelyai/graph';
|
|
848
|
+
*
|
|
849
|
+
* const tree = createGraph({
|
|
850
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
851
|
+
* edges: [
|
|
852
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
853
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
854
|
+
* ],
|
|
855
|
+
* });
|
|
856
|
+
*
|
|
857
|
+
* isTree(tree); // true
|
|
858
|
+
* ```
|
|
859
|
+
*/
|
|
475
860
|
function isTree(graph) {
|
|
476
861
|
return isConnected(graph) && isAcyclic(graph);
|
|
477
862
|
}
|
|
@@ -605,6 +990,24 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
|
|
|
605
990
|
/**
|
|
606
991
|
* Lazily yields all shortest paths from a source node.
|
|
607
992
|
* Use `getShortestPaths` for the full array.
|
|
993
|
+
*
|
|
994
|
+
* @example
|
|
995
|
+
* ```ts
|
|
996
|
+
* import { createGraph, genShortestPaths } from '@statelyai/graph';
|
|
997
|
+
*
|
|
998
|
+
* const graph = createGraph({
|
|
999
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1000
|
+
* edges: [
|
|
1001
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1002
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1003
|
+
* ],
|
|
1004
|
+
* initialNodeId: 'a',
|
|
1005
|
+
* });
|
|
1006
|
+
*
|
|
1007
|
+
* for (const path of genShortestPaths(graph)) {
|
|
1008
|
+
* console.log(path.steps.map(s => s.node.id));
|
|
1009
|
+
* }
|
|
1010
|
+
* ```
|
|
608
1011
|
*/
|
|
609
1012
|
function* genShortestPaths(graph, opts) {
|
|
610
1013
|
const idx = getIndex(graph);
|
|
@@ -615,25 +1018,101 @@ function* genShortestPaths(graph, opts) {
|
|
|
615
1018
|
const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((n) => n.id === sourceId);
|
|
616
1019
|
for (const targetId of targets) yield* reconstructPaths(graph, prev, sourceNode, targetId);
|
|
617
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Returns all shortest paths from a source node as an array.
|
|
1023
|
+
* Delegates to `genShortestPaths` internally.
|
|
1024
|
+
*
|
|
1025
|
+
* @example
|
|
1026
|
+
* ```ts
|
|
1027
|
+
* import { createGraph, getShortestPaths } from '@statelyai/graph';
|
|
1028
|
+
*
|
|
1029
|
+
* const graph = createGraph({
|
|
1030
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1031
|
+
* edges: [
|
|
1032
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1033
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1034
|
+
* ],
|
|
1035
|
+
* initialNodeId: 'a',
|
|
1036
|
+
* });
|
|
1037
|
+
*
|
|
1038
|
+
* const paths = getShortestPaths(graph);
|
|
1039
|
+
* // paths to 'b' and 'c' from 'a'
|
|
1040
|
+
* ```
|
|
1041
|
+
*/
|
|
618
1042
|
function getShortestPaths(graph, opts) {
|
|
619
1043
|
return [...genShortestPaths(graph, opts)];
|
|
620
1044
|
}
|
|
621
1045
|
/**
|
|
622
|
-
* Returns a single shortest path from source to target, or undefined if unreachable.
|
|
1046
|
+
* Returns a single shortest path from source to target, or `undefined` if unreachable.
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```ts
|
|
1050
|
+
* import { createGraph, getShortestPath } from '@statelyai/graph';
|
|
1051
|
+
*
|
|
1052
|
+
* const graph = createGraph({
|
|
1053
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1054
|
+
* edges: [
|
|
1055
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1056
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1057
|
+
* ],
|
|
1058
|
+
* initialNodeId: 'a',
|
|
1059
|
+
* });
|
|
1060
|
+
*
|
|
1061
|
+
* const path = getShortestPath(graph, { to: 'c' });
|
|
1062
|
+
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
1063
|
+
* ```
|
|
623
1064
|
*/
|
|
624
1065
|
function getShortestPath(graph, opts) {
|
|
625
1066
|
for (const path of genShortestPaths(graph, opts)) return path;
|
|
626
1067
|
}
|
|
627
1068
|
/**
|
|
628
|
-
* Returns all simple (acyclic) paths from a source node.
|
|
629
|
-
*
|
|
1069
|
+
* Returns all simple (acyclic) paths from a source node as an array.
|
|
1070
|
+
* Delegates to `genSimplePaths` internally.
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* import { createGraph, getSimplePaths } from '@statelyai/graph';
|
|
1075
|
+
*
|
|
1076
|
+
* const graph = createGraph({
|
|
1077
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1078
|
+
* edges: [
|
|
1079
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1080
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1081
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1082
|
+
* ],
|
|
1083
|
+
* initialNodeId: 'a',
|
|
1084
|
+
* });
|
|
1085
|
+
*
|
|
1086
|
+
* const paths = getSimplePaths(graph, { to: 'c' });
|
|
1087
|
+
* // two paths: a->b->c and a->c
|
|
1088
|
+
* ```
|
|
630
1089
|
*/
|
|
631
1090
|
function getSimplePaths(graph, opts) {
|
|
632
1091
|
return [...genSimplePaths(graph, opts)];
|
|
633
1092
|
}
|
|
634
1093
|
/**
|
|
635
|
-
* Lazily yields all simple (acyclic) paths from a source node.
|
|
1094
|
+
* Lazily yields all simple (acyclic) paths from a source node via DFS backtracking.
|
|
636
1095
|
* Use `getSimplePaths` for the full array.
|
|
1096
|
+
*
|
|
1097
|
+
* @example
|
|
1098
|
+
* ```ts
|
|
1099
|
+
* import { createGraph, genSimplePaths } from '@statelyai/graph';
|
|
1100
|
+
*
|
|
1101
|
+
* const graph = createGraph({
|
|
1102
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1103
|
+
* edges: [
|
|
1104
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1105
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1106
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1107
|
+
* ],
|
|
1108
|
+
* initialNodeId: 'a',
|
|
1109
|
+
* });
|
|
1110
|
+
*
|
|
1111
|
+
* for (const path of genSimplePaths(graph, { to: 'c' })) {
|
|
1112
|
+
* console.log(path.steps.map(s => s.node.id));
|
|
1113
|
+
* // ['b', 'c'] or ['c']
|
|
1114
|
+
* }
|
|
1115
|
+
* ```
|
|
637
1116
|
*/
|
|
638
1117
|
function* genSimplePaths(graph, opts) {
|
|
639
1118
|
const idx = getIndex(graph);
|
|
@@ -675,11 +1154,49 @@ function* genSimplePaths(graph, opts) {
|
|
|
675
1154
|
yield* found;
|
|
676
1155
|
}
|
|
677
1156
|
/**
|
|
678
|
-
* Returns a single simple (acyclic) path from source to target, or undefined if unreachable.
|
|
1157
|
+
* Returns a single simple (acyclic) path from source to target, or `undefined` if unreachable.
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* ```ts
|
|
1161
|
+
* import { createGraph, getSimplePath } from '@statelyai/graph';
|
|
1162
|
+
*
|
|
1163
|
+
* const graph = createGraph({
|
|
1164
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1165
|
+
* edges: [
|
|
1166
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1167
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1168
|
+
* ],
|
|
1169
|
+
* initialNodeId: 'a',
|
|
1170
|
+
* });
|
|
1171
|
+
*
|
|
1172
|
+
* const path = getSimplePath(graph, { to: 'c' });
|
|
1173
|
+
* // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}]
|
|
1174
|
+
* ```
|
|
679
1175
|
*/
|
|
680
1176
|
function getSimplePath(graph, opts) {
|
|
681
1177
|
for (const path of genSimplePaths(graph, opts)) return path;
|
|
682
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Returns strongly connected components using Tarjan's algorithm.
|
|
1181
|
+
* Only meaningful for directed graphs.
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* ```ts
|
|
1185
|
+
* import { createGraph, getStronglyConnectedComponents } from '@statelyai/graph';
|
|
1186
|
+
*
|
|
1187
|
+
* const graph = createGraph({
|
|
1188
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1189
|
+
* edges: [
|
|
1190
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1191
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1192
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1193
|
+
* ],
|
|
1194
|
+
* });
|
|
1195
|
+
*
|
|
1196
|
+
* const sccs = getStronglyConnectedComponents(graph);
|
|
1197
|
+
* // [[nodeA, nodeB], [nodeC]]
|
|
1198
|
+
* ```
|
|
1199
|
+
*/
|
|
683
1200
|
function getStronglyConnectedComponents(graph) {
|
|
684
1201
|
const idx = getIndex(graph);
|
|
685
1202
|
let indexCounter = 0;
|
|
@@ -718,12 +1235,49 @@ function getStronglyConnectedComponents(graph) {
|
|
|
718
1235
|
for (const n of graph.nodes) if (!nodeIndex.has(n.id)) strongconnect(n.id);
|
|
719
1236
|
return result;
|
|
720
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Returns all elementary cycles as an array of paths.
|
|
1240
|
+
* Delegates to `genCycles` internally.
|
|
1241
|
+
*
|
|
1242
|
+
* @example
|
|
1243
|
+
* ```ts
|
|
1244
|
+
* import { createGraph, getCycles } from '@statelyai/graph';
|
|
1245
|
+
*
|
|
1246
|
+
* const graph = createGraph({
|
|
1247
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1248
|
+
* edges: [
|
|
1249
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1250
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1251
|
+
* ],
|
|
1252
|
+
* });
|
|
1253
|
+
*
|
|
1254
|
+
* const cycles = getCycles(graph);
|
|
1255
|
+
* // one cycle: a -> b -> a
|
|
1256
|
+
* ```
|
|
1257
|
+
*/
|
|
721
1258
|
function getCycles(graph) {
|
|
722
1259
|
return [...genCycles(graph)];
|
|
723
1260
|
}
|
|
724
1261
|
/**
|
|
725
|
-
* Lazily yields cycles one at a time.
|
|
1262
|
+
* Lazily yields elementary cycles one at a time.
|
|
726
1263
|
* Use `getCycles` for the full array.
|
|
1264
|
+
*
|
|
1265
|
+
* @example
|
|
1266
|
+
* ```ts
|
|
1267
|
+
* import { createGraph, genCycles } from '@statelyai/graph';
|
|
1268
|
+
*
|
|
1269
|
+
* const graph = createGraph({
|
|
1270
|
+
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
1271
|
+
* edges: [
|
|
1272
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1273
|
+
* { id: 'ba', sourceId: 'b', targetId: 'a' },
|
|
1274
|
+
* ],
|
|
1275
|
+
* });
|
|
1276
|
+
*
|
|
1277
|
+
* for (const cycle of genCycles(graph)) {
|
|
1278
|
+
* console.log(cycle.steps.map(s => s.node.id)); // ['b', 'a']
|
|
1279
|
+
* }
|
|
1280
|
+
* ```
|
|
727
1281
|
*/
|
|
728
1282
|
function* genCycles(graph) {
|
|
729
1283
|
if (graph.type === "undirected") yield* genCyclesUndirected(graph);
|
|
@@ -846,6 +1400,23 @@ function getNeighborEdgesAll(graph, nodeId) {
|
|
|
846
1400
|
/**
|
|
847
1401
|
* Returns a single canonical preorder (DFS visit-order) sequence.
|
|
848
1402
|
* Visits neighbors in the order they appear in the adjacency list.
|
|
1403
|
+
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* ```ts
|
|
1406
|
+
* import { createGraph, getPreorder } from '@statelyai/graph';
|
|
1407
|
+
*
|
|
1408
|
+
* const graph = createGraph({
|
|
1409
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1410
|
+
* edges: [
|
|
1411
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1412
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1413
|
+
* ],
|
|
1414
|
+
* initialNodeId: 'a',
|
|
1415
|
+
* });
|
|
1416
|
+
*
|
|
1417
|
+
* const order = getPreorder(graph);
|
|
1418
|
+
* // [nodeA, nodeB, nodeC]
|
|
1419
|
+
* ```
|
|
849
1420
|
*/
|
|
850
1421
|
function getPreorder(graph, opts) {
|
|
851
1422
|
const idx = getIndex(graph);
|
|
@@ -872,6 +1443,23 @@ function getPreorder(graph, opts) {
|
|
|
872
1443
|
/**
|
|
873
1444
|
* Returns a single canonical postorder (DFS finish-order) sequence.
|
|
874
1445
|
* Visits neighbors in the order they appear in the adjacency list.
|
|
1446
|
+
*
|
|
1447
|
+
* @example
|
|
1448
|
+
* ```ts
|
|
1449
|
+
* import { createGraph, getPostorder } from '@statelyai/graph';
|
|
1450
|
+
*
|
|
1451
|
+
* const graph = createGraph({
|
|
1452
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1453
|
+
* edges: [
|
|
1454
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1455
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1456
|
+
* ],
|
|
1457
|
+
* initialNodeId: 'a',
|
|
1458
|
+
* });
|
|
1459
|
+
*
|
|
1460
|
+
* const order = getPostorder(graph);
|
|
1461
|
+
* // [nodeC, nodeB, nodeA]
|
|
1462
|
+
* ```
|
|
875
1463
|
*/
|
|
876
1464
|
function getPostorder(graph, opts) {
|
|
877
1465
|
const idx = getIndex(graph);
|
|
@@ -894,11 +1482,49 @@ function getPostorder(graph, opts) {
|
|
|
894
1482
|
}
|
|
895
1483
|
return result;
|
|
896
1484
|
}
|
|
897
|
-
/**
|
|
1485
|
+
/**
|
|
1486
|
+
* Returns all possible preorder sequences as an array. Can be exponential -- prefer `genPreorders`.
|
|
1487
|
+
*
|
|
1488
|
+
* @example
|
|
1489
|
+
* ```ts
|
|
1490
|
+
* import { createGraph, getPreorders } from '@statelyai/graph';
|
|
1491
|
+
*
|
|
1492
|
+
* const graph = createGraph({
|
|
1493
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1494
|
+
* edges: [
|
|
1495
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1496
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1497
|
+
* ],
|
|
1498
|
+
* initialNodeId: 'a',
|
|
1499
|
+
* });
|
|
1500
|
+
*
|
|
1501
|
+
* const allOrders = getPreorders(graph);
|
|
1502
|
+
* // [[nodeA, nodeB, nodeC], [nodeA, nodeC, nodeB]]
|
|
1503
|
+
* ```
|
|
1504
|
+
*/
|
|
898
1505
|
function getPreorders(graph, opts) {
|
|
899
1506
|
return [...genPreorders(graph, opts)];
|
|
900
1507
|
}
|
|
901
|
-
/**
|
|
1508
|
+
/**
|
|
1509
|
+
* Returns all possible postorder sequences as an array. Can be exponential -- prefer `genPostorders`.
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* ```ts
|
|
1513
|
+
* import { createGraph, getPostorders } from '@statelyai/graph';
|
|
1514
|
+
*
|
|
1515
|
+
* const graph = createGraph({
|
|
1516
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1517
|
+
* edges: [
|
|
1518
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1519
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1520
|
+
* ],
|
|
1521
|
+
* initialNodeId: 'a',
|
|
1522
|
+
* });
|
|
1523
|
+
*
|
|
1524
|
+
* const allOrders = getPostorders(graph);
|
|
1525
|
+
* // [[nodeB, nodeC, nodeA], [nodeC, nodeB, nodeA]]
|
|
1526
|
+
* ```
|
|
1527
|
+
*/
|
|
902
1528
|
function getPostorders(graph, opts) {
|
|
903
1529
|
return [...genPostorders(graph, opts)];
|
|
904
1530
|
}
|
|
@@ -906,6 +1532,25 @@ function getPostorders(graph, opts) {
|
|
|
906
1532
|
* Lazily yields all possible preorder (DFS visit-order) sequences.
|
|
907
1533
|
* Different neighbor exploration orders yield different sequences.
|
|
908
1534
|
* Use `getPreorder()` for a single canonical ordering.
|
|
1535
|
+
*
|
|
1536
|
+
* @example
|
|
1537
|
+
* ```ts
|
|
1538
|
+
* import { createGraph, genPreorders } from '@statelyai/graph';
|
|
1539
|
+
*
|
|
1540
|
+
* const graph = createGraph({
|
|
1541
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1542
|
+
* edges: [
|
|
1543
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1544
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1545
|
+
* ],
|
|
1546
|
+
* initialNodeId: 'a',
|
|
1547
|
+
* });
|
|
1548
|
+
*
|
|
1549
|
+
* for (const order of genPreorders(graph)) {
|
|
1550
|
+
* console.log(order.map(n => n.id));
|
|
1551
|
+
* // ['a', 'b', 'c'] or ['a', 'c', 'b']
|
|
1552
|
+
* }
|
|
1553
|
+
* ```
|
|
909
1554
|
*/
|
|
910
1555
|
function* genPreorders(graph, opts) {
|
|
911
1556
|
const idx = getIndex(graph);
|
|
@@ -951,6 +1596,25 @@ function* genPreorders(graph, opts) {
|
|
|
951
1596
|
* Lazily yields all possible postorder (DFS finish-order) sequences.
|
|
952
1597
|
* Different neighbor exploration orders yield different sequences.
|
|
953
1598
|
* Use `getPostorder()` for a single canonical ordering.
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* ```ts
|
|
1602
|
+
* import { createGraph, genPostorders } from '@statelyai/graph';
|
|
1603
|
+
*
|
|
1604
|
+
* const graph = createGraph({
|
|
1605
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1606
|
+
* edges: [
|
|
1607
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1608
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c' },
|
|
1609
|
+
* ],
|
|
1610
|
+
* initialNodeId: 'a',
|
|
1611
|
+
* });
|
|
1612
|
+
*
|
|
1613
|
+
* for (const order of genPostorders(graph)) {
|
|
1614
|
+
* console.log(order.map(n => n.id));
|
|
1615
|
+
* // ['b', 'c', 'a'] or ['c', 'b', 'a']
|
|
1616
|
+
* }
|
|
1617
|
+
* ```
|
|
954
1618
|
*/
|
|
955
1619
|
function* genPostorders(graph, opts) {
|
|
956
1620
|
const idx = getIndex(graph);
|
|
@@ -995,6 +1659,26 @@ function* genPostorders(graph, opts) {
|
|
|
995
1659
|
* Returns a minimum spanning tree of the graph.
|
|
996
1660
|
* Only meaningful for connected undirected graphs (or the component reachable
|
|
997
1661
|
* from an arbitrary start node in directed graphs).
|
|
1662
|
+
*
|
|
1663
|
+
* @example
|
|
1664
|
+
* ```ts
|
|
1665
|
+
* import { createGraph, getMinimumSpanningTree } from '@statelyai/graph';
|
|
1666
|
+
*
|
|
1667
|
+
* const graph = createGraph({
|
|
1668
|
+
* type: 'undirected',
|
|
1669
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1670
|
+
* edges: [
|
|
1671
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b', data: { weight: 1 } },
|
|
1672
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c', data: { weight: 2 } },
|
|
1673
|
+
* { id: 'ac', sourceId: 'a', targetId: 'c', data: { weight: 3 } },
|
|
1674
|
+
* ],
|
|
1675
|
+
* });
|
|
1676
|
+
*
|
|
1677
|
+
* const mst = getMinimumSpanningTree(graph, {
|
|
1678
|
+
* getWeight: (e) => e.data.weight,
|
|
1679
|
+
* });
|
|
1680
|
+
* // mst has edges 'ab' and 'bc' (total weight 3)
|
|
1681
|
+
* ```
|
|
998
1682
|
*/
|
|
999
1683
|
function getMinimumSpanningTree(graph, opts) {
|
|
1000
1684
|
const algorithm = opts?.algorithm ?? "prim";
|
|
@@ -1091,7 +1775,23 @@ function kruskalMST(graph, getWeight) {
|
|
|
1091
1775
|
/**
|
|
1092
1776
|
* Returns shortest paths between all pairs of nodes.
|
|
1093
1777
|
* Algorithm 'dijkstra' (default): runs getShortestPaths per source node.
|
|
1094
|
-
* Algorithm 'floyd-warshall': classic O(V
|
|
1778
|
+
* Algorithm 'floyd-warshall': classic O(V^3) dynamic programming.
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* ```ts
|
|
1782
|
+
* import { createGraph, getAllPairsShortestPaths } from '@statelyai/graph';
|
|
1783
|
+
*
|
|
1784
|
+
* const graph = createGraph({
|
|
1785
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1786
|
+
* edges: [
|
|
1787
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1788
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1789
|
+
* ],
|
|
1790
|
+
* });
|
|
1791
|
+
*
|
|
1792
|
+
* const allPaths = getAllPairsShortestPaths(graph);
|
|
1793
|
+
* // paths for every reachable (source, target) pair
|
|
1794
|
+
* ```
|
|
1095
1795
|
*/
|
|
1096
1796
|
function getAllPairsShortestPaths(graph, opts) {
|
|
1097
1797
|
if ((opts?.algorithm ?? "dijkstra") === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
|
|
@@ -1192,6 +1892,40 @@ function fwReconstruct(graph, prev, idxOf, nodeIds, sourceNode, sourceIdx, targe
|
|
|
1192
1892
|
}
|
|
1193
1893
|
return results;
|
|
1194
1894
|
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Joins two paths end-to-end. The last node of the head path must equal
|
|
1897
|
+
* the source of the tail path (the overlap node).
|
|
1898
|
+
*
|
|
1899
|
+
* Steps are concatenated: head.steps ++ tail.steps (tail already starts
|
|
1900
|
+
* from the overlap node, so no slicing is needed).
|
|
1901
|
+
*
|
|
1902
|
+
* @example
|
|
1903
|
+
* ```ts
|
|
1904
|
+
* import { createGraph, getShortestPath, joinPaths } from '@statelyai/graph';
|
|
1905
|
+
*
|
|
1906
|
+
* const graph = createGraph({
|
|
1907
|
+
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
1908
|
+
* edges: [
|
|
1909
|
+
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
1910
|
+
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
1911
|
+
* ],
|
|
1912
|
+
* initialNodeId: 'a',
|
|
1913
|
+
* });
|
|
1914
|
+
*
|
|
1915
|
+
* const ab = getShortestPath(graph, { to: 'b' })!;
|
|
1916
|
+
* const bc = getShortestPath(graph, { from: 'b', to: 'c' })!;
|
|
1917
|
+
* const ac = joinPaths(ab, bc);
|
|
1918
|
+
* // ac: a -> b -> c
|
|
1919
|
+
* ```
|
|
1920
|
+
*/
|
|
1921
|
+
function joinPaths(headPath, tailPath) {
|
|
1922
|
+
const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
|
|
1923
|
+
if (headEnd.id !== tailPath.source.id) throw new Error(`Paths cannot be joined: head path ends at "${headEnd.id}" but tail path starts at "${tailPath.source.id}"`);
|
|
1924
|
+
return {
|
|
1925
|
+
source: headPath.source,
|
|
1926
|
+
steps: [...headPath.steps, ...tailPath.steps]
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1195
1929
|
|
|
1196
1930
|
//#endregion
|
|
1197
|
-
export {
|
|
1931
|
+
export { addNode as A, hasNode as B, isAcyclic as C, GraphInstance as D, joinPaths as E, deleteEntities as F, updateEntities as H, deleteNode as I, getEdge as L, createGraphFromTransition as M, createVisualGraph as N, addEdge as O, deleteEdge as P, getNode as R, hasPath as S, isTree as T, updateNode as U, updateEdge as V, getShortestPaths as _, genPreorders as a, getStronglyConnectedComponents as b, getAllPairsShortestPaths as c, getMinimumSpanningTree as d, getPostorder as f, getShortestPath as g, getPreorders as h, genPostorders as i, createGraph as j, addEntities as k, getConnectedComponents as l, getPreorder as m, dfs as n, genShortestPaths as o, getPostorders as p, genCycles as r, genSimplePaths as s, bfs as t, getCycles as u, getSimplePath as v, isConnected as w, getTopologicalSort as x, getSimplePaths as y, hasEdge as z };
|