@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.
- package/README.md +121 -44
- package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
- package/dist/algorithms-D1cgly0g.d.mts +452 -0
- package/dist/algorithms-DBpH74hR.mjs +3309 -0
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- package/dist/format-support.mjs +38 -11
- package/dist/formats/adjacency-list/index.d.mts +1 -1
- package/dist/formats/adjacency-list/index.mjs +1 -1
- package/dist/formats/converter/index.d.mts +1 -1
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +4 -4
- package/dist/formats/cytoscape/index.mjs +10 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +26 -12
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +10 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +22 -6
- package/dist/formats/edge-list/index.d.mts +1 -1
- package/dist/formats/edge-list/index.mjs +1 -1
- package/dist/formats/elk/index.d.mts +1 -1
- package/dist/formats/elk/index.mjs +63 -24
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +43 -16
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +28 -15
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +96 -23
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +12 -5
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +49 -12
- package/dist/formats/tgf/index.d.mts +4 -4
- package/dist/formats/tgf/index.mjs +4 -4
- package/dist/formats/xyflow/index.d.mts +12 -6
- package/dist/formats/xyflow/index.mjs +42 -10
- package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +290 -307
- package/dist/layout/cytoscape.d.mts +66 -0
- package/dist/layout/cytoscape.mjs +114 -0
- package/dist/layout/d3-force.d.mts +52 -0
- package/dist/layout/d3-force.mjs +127 -0
- package/dist/layout/d3-hierarchy.d.mts +39 -0
- package/dist/layout/d3-hierarchy.mjs +135 -0
- package/dist/layout/dagre.d.mts +32 -0
- package/dist/layout/dagre.mjs +99 -0
- package/dist/layout/elk.d.mts +47 -0
- package/dist/layout/elk.mjs +73 -0
- package/dist/layout/forceatlas2.d.mts +48 -0
- package/dist/layout/forceatlas2.mjs +100 -0
- package/dist/layout/graphviz.d.mts +50 -0
- package/dist/layout/graphviz.mjs +179 -0
- package/dist/layout/index.d.mts +185 -0
- package/dist/layout/index.mjs +181 -0
- package/dist/layout/webcola.d.mts +40 -0
- package/dist/layout/webcola.mjs +104 -0
- package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
- package/dist/queries-BMM0XAv_.mjs +986 -0
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -768
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +32 -84
- package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
- package/dist/validate-BsfSOv0S.mjs +190 -0
- package/package.json +59 -7
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- package/dist/algorithms-Ba7o7niK.mjs +0 -2394
- package/dist/algorithms-fTqmvhzP.d.mts +0 -178
- package/dist/indexing-DR8M1vBy.mjs +0 -137
- /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as isEdgeDirected, t as getEdgeMode } from "./mode-
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
1
|
+
import { C as getSinks, D as isLeaf, E as isCompound, N as invalidateIndex, S as getSiblings, T as getSuccessors, _ as getPorts, a as getDescendants, b as getRelativeDistanceMap, c as getEdgesOf, d as getLCA, f as getNeighbors, g as getPort, h as getParent, i as getDepth, l as getInDegree, m as getOutEdges, n as getChildren, o as getEdgesBetween, p as getOutDegree, r as getDegree, s as getEdgesByPort, t as getAncestors, u as getInEdges, v as getPredecessors, w as getSources, x as getRoots, y as getRelativeDistance } from "./queries-BMM0XAv_.mjs";
|
|
2
|
+
import { S as updateNode, _ as getNode, a as addEntities, b as updateEdge, c as createGraphEdge, d as createGraphPort, f as createVisualGraph, g as getEdge, h as deleteNode, i as addEdge, l as createGraphFromTransition, m as deleteEntities, n as toNodeConfig, o as addNode, p as deleteEdge, r as GraphInstance, s as createGraph, t as toEdgeConfig, u as createGraphNode, v as hasEdge, x as updateEntities, y as hasNode } from "./config-Dt5u1gSf.mjs";
|
|
3
|
+
import { n as isEdgeDirected, t as getEdgeMode } from "./mode-gu_mhKKs.mjs";
|
|
4
|
+
import { t as getGraphIssues } from "./validate-BsfSOv0S.mjs";
|
|
5
|
+
import { $ as getAStarPath, A as genPreorders, B as getTopologicalSort, C as getHITS, D as getPageRank, E as getOutDegreeCentrality, F as bfs, G as flatten, H as isAcyclic, I as dfs, J as getSubgraph, K as getFlattenedGraph, L as genBFS, M as getPostorders, N as getPreorder, O as getMinimumSpanningTree, P as getPreorders, Q as genSimplePaths, R as genDFS, S as getEigenvectorCentrality, T as getKatzCentrality, U as isConnected, V as hasPath, W as isTree, X as genCycles, Y as reverseGraph, Z as genShortestPaths, _ as getCoreNumbers, a as getLouvainCommunities, at as getSimplePath, b as getClosenessCentrality, c as getBiconnectedComponents, ct as joinPaths, d as getGirvanNewmanCommunities, et as getAllPairsShortestPaths, f as getGreedyModularityCommunities, g as isBipartite, h as getMaximumBipartiteMatching, i as getMinCut, it as getShortestPaths, j as getPostorder, k as genPostorders, l as getBridges, lt as mulberry32$1, m as getModularity, n as getDominatorTree, nt as getJoinedPath, o as isIsomorphic, ot as getSimplePaths, p as getLabelPropagationCommunities, q as getReversedGraph, r as getMaxFlow, rt as getShortestPath, s as getArticulationPoints, st as getStronglyConnectedComponents, t as getTransitiveReduction, tt as getCycles, u as genGirvanNewmanCommunities, v as getKCore, w as getInDegreeCentrality, x as getDegreeCentrality, y as getBetweennessCentrality, z as getConnectedComponents } from "./algorithms-DBpH74hR.mjs";
|
|
6
|
+
import { n as createFormatConverter } from "./converter-DB6Rg6Vd.mjs";
|
|
6
7
|
|
|
8
|
+
//#region src/generators.ts
|
|
9
|
+
function assertNonNegativeInteger(caller, name, value) {
|
|
10
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`${caller}: ${name} ${value} is invalid — pass a non-negative integer`);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create the complete graph K_n: every pair of distinct nodes connected by
|
|
14
|
+
* one undirected edge. Nodes are `n0..n{n-1}` (`options.idPrefix` overrides
|
|
15
|
+
* the `n` prefix); edges are `e0..`.
|
|
16
|
+
*/
|
|
17
|
+
function createCompleteGraph(n, options) {
|
|
18
|
+
assertNonNegativeInteger("createCompleteGraph", "node count", n);
|
|
19
|
+
const prefix = options?.idPrefix ?? "n";
|
|
20
|
+
const nodes = [];
|
|
21
|
+
for (let i = 0; i < n; i++) nodes.push({ id: `${prefix}${i}` });
|
|
22
|
+
const edges = [];
|
|
23
|
+
let edgeId = 0;
|
|
24
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) edges.push({
|
|
25
|
+
id: `e${edgeId++}`,
|
|
26
|
+
sourceId: `${prefix}${i}`,
|
|
27
|
+
targetId: `${prefix}${j}`
|
|
28
|
+
});
|
|
29
|
+
return createGraph({
|
|
30
|
+
mode: "undirected",
|
|
31
|
+
nodes,
|
|
32
|
+
edges
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a `rows × cols` grid graph: node `(r, c)` is connected to its right
|
|
37
|
+
* and down neighbors by undirected edges. Nodes are `n{r}_{c}`
|
|
38
|
+
* (`options.idPrefix` overrides the `n` prefix); edges are `e0..`.
|
|
39
|
+
*/
|
|
40
|
+
function createGridGraph(rows, cols, options) {
|
|
41
|
+
assertNonNegativeInteger("createGridGraph", "row count", rows);
|
|
42
|
+
assertNonNegativeInteger("createGridGraph", "column count", cols);
|
|
43
|
+
const prefix = options?.idPrefix ?? "n";
|
|
44
|
+
const nodeId = (r, c) => `${prefix}${r}_${c}`;
|
|
45
|
+
const nodes = [];
|
|
46
|
+
const edges = [];
|
|
47
|
+
let edgeId = 0;
|
|
48
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
|
49
|
+
nodes.push({ id: nodeId(r, c) });
|
|
50
|
+
if (c + 1 < cols) edges.push({
|
|
51
|
+
id: `e${edgeId++}`,
|
|
52
|
+
sourceId: nodeId(r, c),
|
|
53
|
+
targetId: nodeId(r, c + 1)
|
|
54
|
+
});
|
|
55
|
+
if (r + 1 < rows) edges.push({
|
|
56
|
+
id: `e${edgeId++}`,
|
|
57
|
+
sourceId: nodeId(r, c),
|
|
58
|
+
targetId: nodeId(r + 1, c)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return createGraph({
|
|
62
|
+
mode: "undirected",
|
|
63
|
+
nodes,
|
|
64
|
+
edges
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create an Erdős–Rényi G(n, p) random graph: each of the n·(n-1)/2 node
|
|
69
|
+
* pairs gets an undirected edge with probability `probability`. With
|
|
70
|
+
* `options.seed` the result is deterministic per seed (mulberry32);
|
|
71
|
+
* otherwise `Math.random` is used. Nodes are `n0..n{n-1}`
|
|
72
|
+
* (`options.idPrefix` overrides the `n` prefix); edges are `e0..`.
|
|
73
|
+
*/
|
|
74
|
+
function createRandomGraph(n, probability, options) {
|
|
75
|
+
assertNonNegativeInteger("createRandomGraph", "node count", n);
|
|
76
|
+
if (!(probability >= 0 && probability <= 1)) throw new Error(`createRandomGraph: probability ${probability} is invalid — pass a number between 0 and 1`);
|
|
77
|
+
const prefix = options?.idPrefix ?? "n";
|
|
78
|
+
const rng = options?.seed !== void 0 ? mulberry32$1(options.seed) : Math.random;
|
|
79
|
+
const nodes = [];
|
|
80
|
+
for (let i = 0; i < n; i++) nodes.push({ id: `${prefix}${i}` });
|
|
81
|
+
const edges = [];
|
|
82
|
+
let edgeId = 0;
|
|
83
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) if (rng() < probability) edges.push({
|
|
84
|
+
id: `e${edgeId++}`,
|
|
85
|
+
sourceId: `${prefix}${i}`,
|
|
86
|
+
targetId: `${prefix}${j}`
|
|
87
|
+
});
|
|
88
|
+
return createGraph({
|
|
89
|
+
mode: "undirected",
|
|
90
|
+
nodes,
|
|
91
|
+
edges
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
7
96
|
//#region src/equivalence.ts
|
|
8
97
|
/** Shallow-compare two values, returning true if they differ. */
|
|
9
98
|
function differs$1(a, b) {
|
|
@@ -27,6 +116,8 @@ const LAYOUT_KEYS = {
|
|
|
27
116
|
"y",
|
|
28
117
|
"width",
|
|
29
118
|
"height",
|
|
119
|
+
"points",
|
|
120
|
+
"routing",
|
|
30
121
|
"style",
|
|
31
122
|
"color"
|
|
32
123
|
]
|
|
@@ -52,7 +143,7 @@ const LAYOUT_KEY_SET = {
|
|
|
52
143
|
* ```
|
|
53
144
|
*/
|
|
54
145
|
function areEntitiesEqual(a, b, keys) {
|
|
55
|
-
const compareKeys = keys && keys.length > 0 ? keys : Object.keys(a);
|
|
146
|
+
const compareKeys = keys && keys.length > 0 ? keys : [...new Set([...Object.keys(a), ...Object.keys(b)])];
|
|
56
147
|
for (const key of compareKeys) if (differs$1(a[key], b[key])) return false;
|
|
57
148
|
return true;
|
|
58
149
|
}
|
|
@@ -87,47 +178,16 @@ function isLayoutEqual(a, b) {
|
|
|
87
178
|
*/
|
|
88
179
|
function isNonLayoutEqual(a, b) {
|
|
89
180
|
const skip = LAYOUT_KEY_SET[a.type];
|
|
90
|
-
const keys = Object.keys(a);
|
|
91
|
-
for (
|
|
92
|
-
if (skip.has(
|
|
93
|
-
if (differs$1(a[
|
|
181
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
182
|
+
for (const key of keys) {
|
|
183
|
+
if (skip.has(key)) continue;
|
|
184
|
+
if (differs$1(a[key], b[key])) return false;
|
|
94
185
|
}
|
|
95
186
|
return true;
|
|
96
187
|
}
|
|
97
188
|
|
|
98
189
|
//#endregion
|
|
99
190
|
//#region src/diff.ts
|
|
100
|
-
function nodeToConfig$1(node) {
|
|
101
|
-
const config = { id: node.id };
|
|
102
|
-
if (node.parentId) config.parentId = node.parentId;
|
|
103
|
-
if (node.initialNodeId) config.initialNodeId = node.initialNodeId;
|
|
104
|
-
if (node.label !== "") config.label = node.label;
|
|
105
|
-
if (node.data !== void 0) config.data = node.data;
|
|
106
|
-
if (node.x !== void 0) config.x = node.x;
|
|
107
|
-
if (node.y !== void 0) config.y = node.y;
|
|
108
|
-
if (node.width !== void 0) config.width = node.width;
|
|
109
|
-
if (node.height !== void 0) config.height = node.height;
|
|
110
|
-
if (node.shape !== void 0) config.shape = node.shape;
|
|
111
|
-
if (node.color !== void 0) config.color = node.color;
|
|
112
|
-
if (node.style !== void 0) config.style = node.style;
|
|
113
|
-
return config;
|
|
114
|
-
}
|
|
115
|
-
function edgeToConfig$1(edge) {
|
|
116
|
-
const config = {
|
|
117
|
-
id: edge.id,
|
|
118
|
-
sourceId: edge.sourceId,
|
|
119
|
-
targetId: edge.targetId
|
|
120
|
-
};
|
|
121
|
-
if (edge.label !== "") config.label = edge.label;
|
|
122
|
-
if (edge.data !== void 0) config.data = edge.data;
|
|
123
|
-
if (edge.x !== void 0) config.x = edge.x;
|
|
124
|
-
if (edge.y !== void 0) config.y = edge.y;
|
|
125
|
-
if (edge.width !== void 0) config.width = edge.width;
|
|
126
|
-
if (edge.height !== void 0) config.height = edge.height;
|
|
127
|
-
if (edge.color !== void 0) config.color = edge.color;
|
|
128
|
-
if (edge.style !== void 0) config.style = edge.style;
|
|
129
|
-
return config;
|
|
130
|
-
}
|
|
131
191
|
/** Shallow-compare two values, returning true if they differ. */
|
|
132
192
|
function differs(a, b) {
|
|
133
193
|
if (a === b) return false;
|
|
@@ -140,6 +200,7 @@ const NODE_COMPARE_KEYS = [
|
|
|
140
200
|
"initialNodeId",
|
|
141
201
|
"label",
|
|
142
202
|
"data",
|
|
203
|
+
"ports",
|
|
143
204
|
"x",
|
|
144
205
|
"y",
|
|
145
206
|
"width",
|
|
@@ -153,6 +214,12 @@ const EDGE_COMPARE_KEYS = [
|
|
|
153
214
|
"targetId",
|
|
154
215
|
"label",
|
|
155
216
|
"data",
|
|
217
|
+
"weight",
|
|
218
|
+
"mode",
|
|
219
|
+
"sourcePort",
|
|
220
|
+
"targetPort",
|
|
221
|
+
"points",
|
|
222
|
+
"routing",
|
|
156
223
|
"x",
|
|
157
224
|
"y",
|
|
158
225
|
"width",
|
|
@@ -194,13 +261,17 @@ function getDiff(a, b) {
|
|
|
194
261
|
};
|
|
195
262
|
for (const [id, nodeB] of bNodeMap) {
|
|
196
263
|
const nodeA = aNodeMap.get(id);
|
|
197
|
-
if (!nodeA) diff.nodes.added.push(
|
|
264
|
+
if (!nodeA) diff.nodes.added.push(toNodeConfig(nodeB));
|
|
198
265
|
else {
|
|
199
266
|
const oldPartial = {};
|
|
200
267
|
const newPartial = {};
|
|
201
|
-
for (const key of NODE_COMPARE_KEYS)
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
for (const key of NODE_COMPARE_KEYS) {
|
|
269
|
+
const oldValue = nodeA[key] ?? null;
|
|
270
|
+
const newValue = nodeB[key] ?? null;
|
|
271
|
+
if (differs(oldValue, newValue)) {
|
|
272
|
+
oldPartial[key] = oldValue;
|
|
273
|
+
newPartial[key] = newValue;
|
|
274
|
+
}
|
|
204
275
|
}
|
|
205
276
|
if (Object.keys(oldPartial).length > 0) diff.nodes.updated.push({
|
|
206
277
|
id,
|
|
@@ -209,16 +280,20 @@ function getDiff(a, b) {
|
|
|
209
280
|
});
|
|
210
281
|
}
|
|
211
282
|
}
|
|
212
|
-
for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(
|
|
283
|
+
for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(toNodeConfig(nodeA));
|
|
213
284
|
for (const [id, edgeB] of bEdgeMap) {
|
|
214
285
|
const edgeA = aEdgeMap.get(id);
|
|
215
|
-
if (!edgeA) diff.edges.added.push(
|
|
286
|
+
if (!edgeA) diff.edges.added.push(toEdgeConfig(edgeB));
|
|
216
287
|
else {
|
|
217
288
|
const oldPartial = {};
|
|
218
289
|
const newPartial = {};
|
|
219
|
-
for (const key of EDGE_COMPARE_KEYS)
|
|
220
|
-
|
|
221
|
-
|
|
290
|
+
for (const key of EDGE_COMPARE_KEYS) {
|
|
291
|
+
const oldValue = edgeA[key] ?? null;
|
|
292
|
+
const newValue = edgeB[key] ?? null;
|
|
293
|
+
if (differs(oldValue, newValue)) {
|
|
294
|
+
oldPartial[key] = oldValue;
|
|
295
|
+
newPartial[key] = newValue;
|
|
296
|
+
}
|
|
222
297
|
}
|
|
223
298
|
if (Object.keys(oldPartial).length > 0) diff.edges.updated.push({
|
|
224
299
|
id,
|
|
@@ -227,7 +302,7 @@ function getDiff(a, b) {
|
|
|
227
302
|
});
|
|
228
303
|
}
|
|
229
304
|
}
|
|
230
|
-
for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(
|
|
305
|
+
for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(toEdgeConfig(edgeA));
|
|
231
306
|
return diff;
|
|
232
307
|
}
|
|
233
308
|
/**
|
|
@@ -250,42 +325,49 @@ function isEmptyDiff(diff) {
|
|
|
250
325
|
*
|
|
251
326
|
* @example
|
|
252
327
|
* ```ts
|
|
253
|
-
* import { createGraph, getDiff,
|
|
328
|
+
* import { createGraph, getDiff, getInvertedDiff } from '@statelyai/graph';
|
|
254
329
|
*
|
|
255
330
|
* const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] });
|
|
256
331
|
* const b = createGraph({ nodes: [{ id: 'n2' }], edges: [] });
|
|
257
332
|
*
|
|
258
333
|
* const diff = getDiff(a, b);
|
|
259
|
-
* const inv =
|
|
334
|
+
* const inv = getInvertedDiff(diff);
|
|
260
335
|
* // inv.nodes.added contains n1 (was removed)
|
|
261
336
|
* // inv.nodes.removed contains n2 (was added)
|
|
262
337
|
* ```
|
|
263
338
|
*/
|
|
264
|
-
function
|
|
339
|
+
function getInvertedDiff(diff) {
|
|
265
340
|
return {
|
|
266
341
|
nodes: {
|
|
267
|
-
added: diff.nodes.removed,
|
|
268
|
-
removed: diff.nodes.added,
|
|
342
|
+
added: diff.nodes.removed.map((c) => structuredClone(c)),
|
|
343
|
+
removed: diff.nodes.added.map((c) => structuredClone(c)),
|
|
269
344
|
updated: diff.nodes.updated.map((c) => ({
|
|
270
345
|
id: c.id,
|
|
271
|
-
old: c.new,
|
|
272
|
-
new: c.old
|
|
346
|
+
old: structuredClone(c.new),
|
|
347
|
+
new: structuredClone(c.old)
|
|
273
348
|
}))
|
|
274
349
|
},
|
|
275
350
|
edges: {
|
|
276
|
-
added: diff.edges.removed,
|
|
277
|
-
removed: diff.edges.added,
|
|
351
|
+
added: diff.edges.removed.map((c) => structuredClone(c)),
|
|
352
|
+
removed: diff.edges.added.map((c) => structuredClone(c)),
|
|
278
353
|
updated: diff.edges.updated.map((c) => ({
|
|
279
354
|
id: c.id,
|
|
280
|
-
old: c.new,
|
|
281
|
-
new: c.old
|
|
355
|
+
old: structuredClone(c.new),
|
|
356
|
+
new: structuredClone(c.old)
|
|
282
357
|
}))
|
|
283
358
|
}
|
|
284
359
|
};
|
|
285
360
|
}
|
|
286
361
|
/**
|
|
362
|
+
* @deprecated Use {@link getInvertedDiff}.
|
|
363
|
+
*/
|
|
364
|
+
function invertDiff(diff) {
|
|
365
|
+
return getInvertedDiff(diff);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
287
368
|
* Compute an ordered patch list from graph `a` to graph `b`.
|
|
288
|
-
* Order
|
|
369
|
+
* Order (see {@link toPatches}): add nodes → update edges → delete edges →
|
|
370
|
+
* delete nodes → add edges → update nodes.
|
|
289
371
|
*
|
|
290
372
|
* @example
|
|
291
373
|
* ```ts
|
|
@@ -307,17 +389,17 @@ function getPatches(a, b) {
|
|
|
307
389
|
*
|
|
308
390
|
* @example
|
|
309
391
|
* ```ts
|
|
310
|
-
* import { createGraph, getPatches,
|
|
392
|
+
* import { createGraph, getPatches, updateGraphWithPatches } from '@statelyai/graph';
|
|
311
393
|
*
|
|
312
394
|
* const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] });
|
|
313
395
|
* const b = createGraph({ nodes: [{ id: 'n1' }, { id: 'n2' }], edges: [] });
|
|
314
396
|
*
|
|
315
397
|
* const patches = getPatches(a, b);
|
|
316
|
-
*
|
|
398
|
+
* updateGraphWithPatches(a, patches);
|
|
317
399
|
* // a now contains both n1 and n2
|
|
318
400
|
* ```
|
|
319
401
|
*/
|
|
320
|
-
function
|
|
402
|
+
function updateGraphWithPatches(graph, patches) {
|
|
321
403
|
for (const patch of patches) switch (patch.op) {
|
|
322
404
|
case "addNode":
|
|
323
405
|
addNode(graph, patch.node);
|
|
@@ -340,6 +422,12 @@ function applyPatches(graph, patches) {
|
|
|
340
422
|
}
|
|
341
423
|
}
|
|
342
424
|
/**
|
|
425
|
+
* @deprecated Use {@link updateGraphWithPatches}.
|
|
426
|
+
*/
|
|
427
|
+
function applyPatches(graph, patches) {
|
|
428
|
+
updateGraphWithPatches(graph, patches);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
343
431
|
* Flatten a structured diff into an ordered patch list.
|
|
344
432
|
* Order: add nodes → update edges → delete edges → delete nodes → add edges → update nodes.
|
|
345
433
|
* This avoids cascading deletes removing edges that are being updated,
|
|
@@ -365,7 +453,7 @@ function toPatches(diff) {
|
|
|
365
453
|
});
|
|
366
454
|
for (const change of diff.edges.updated) {
|
|
367
455
|
const data = {};
|
|
368
|
-
for (const [key, value] of Object.entries(change.new)) data[key] = value;
|
|
456
|
+
for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
|
|
369
457
|
patches.push({
|
|
370
458
|
op: "updateEdge",
|
|
371
459
|
id: change.id,
|
|
@@ -386,7 +474,7 @@ function toPatches(diff) {
|
|
|
386
474
|
});
|
|
387
475
|
for (const change of diff.nodes.updated) {
|
|
388
476
|
const data = {};
|
|
389
|
-
for (const [key, value] of Object.entries(change.new)) data[key] = value;
|
|
477
|
+
for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
|
|
390
478
|
patches.push({
|
|
391
479
|
op: "updateNode",
|
|
392
480
|
id: change.id,
|
|
@@ -460,201 +548,6 @@ function toDiff(patches) {
|
|
|
460
548
|
return diff;
|
|
461
549
|
}
|
|
462
550
|
|
|
463
|
-
//#endregion
|
|
464
|
-
//#region src/transforms.ts
|
|
465
|
-
/**
|
|
466
|
-
* Flattens a hierarchical graph into a flat graph with only leaf nodes.
|
|
467
|
-
*
|
|
468
|
-
* - Edges targeting a compound node resolve to its initial child (recursively).
|
|
469
|
-
* - Edges originating from a compound node expand to all leaf descendants.
|
|
470
|
-
* - Only leaf nodes (nodes with no children) appear in the result.
|
|
471
|
-
* - Duplicate edges (same source + target) are deduplicated.
|
|
472
|
-
*
|
|
473
|
-
* @example
|
|
474
|
-
* ```ts
|
|
475
|
-
* import { createGraph, flatten } from '@statelyai/graph';
|
|
476
|
-
*
|
|
477
|
-
* const graph = createGraph({
|
|
478
|
-
* nodes: [
|
|
479
|
-
* { id: 'parent', initialNodeId: 'child1' },
|
|
480
|
-
* { id: 'child1', parentId: 'parent' },
|
|
481
|
-
* { id: 'child2', parentId: 'parent' },
|
|
482
|
-
* { id: 'other' },
|
|
483
|
-
* ],
|
|
484
|
-
* edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
|
|
485
|
-
* });
|
|
486
|
-
*
|
|
487
|
-
* const flat = flatten(graph);
|
|
488
|
-
* // flat.nodes → [child1, child2, other] (leaf nodes only)
|
|
489
|
-
* // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
|
|
490
|
-
* ```
|
|
491
|
-
*/
|
|
492
|
-
function flatten(graph) {
|
|
493
|
-
const idx = getIndex(graph);
|
|
494
|
-
const leaves = /* @__PURE__ */ new Set();
|
|
495
|
-
for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
|
|
496
|
-
function resolveInitial(nodeId) {
|
|
497
|
-
if (leaves.has(nodeId)) return nodeId;
|
|
498
|
-
const ni = idx.nodeById.get(nodeId);
|
|
499
|
-
if (ni === void 0) return null;
|
|
500
|
-
const node = graph.nodes[ni];
|
|
501
|
-
if (node.initialNodeId) return resolveInitial(node.initialNodeId);
|
|
502
|
-
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
503
|
-
if (childIds.length > 0) return resolveInitial(childIds[0]);
|
|
504
|
-
return nodeId;
|
|
505
|
-
}
|
|
506
|
-
function getLeafDescendants(nodeId) {
|
|
507
|
-
if (leaves.has(nodeId)) return [nodeId];
|
|
508
|
-
const result = [];
|
|
509
|
-
const collect = (id) => {
|
|
510
|
-
const childIds = idx.childNodes.get(id) ?? [];
|
|
511
|
-
for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
|
|
512
|
-
else collect(childId);
|
|
513
|
-
};
|
|
514
|
-
collect(nodeId);
|
|
515
|
-
return result;
|
|
516
|
-
}
|
|
517
|
-
const edgeSeen = /* @__PURE__ */ new Set();
|
|
518
|
-
const flatEdges = [];
|
|
519
|
-
for (const edge of graph.edges) {
|
|
520
|
-
const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
|
|
521
|
-
const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
|
|
522
|
-
if (target === null) continue;
|
|
523
|
-
for (const source of sources) {
|
|
524
|
-
if (source === target) continue;
|
|
525
|
-
const key = `${source}->${target}`;
|
|
526
|
-
if (edgeSeen.has(key)) continue;
|
|
527
|
-
edgeSeen.add(key);
|
|
528
|
-
flatEdges.push({
|
|
529
|
-
type: "edge",
|
|
530
|
-
id: `${edge.id}:${source}->${target}`,
|
|
531
|
-
sourceId: source,
|
|
532
|
-
targetId: target,
|
|
533
|
-
label: edge.label,
|
|
534
|
-
data: edge.data
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => ({
|
|
539
|
-
id: n.id,
|
|
540
|
-
label: n.label,
|
|
541
|
-
data: n.data
|
|
542
|
-
}));
|
|
543
|
-
return createGraph({
|
|
544
|
-
id: graph.id,
|
|
545
|
-
mode: graph.mode,
|
|
546
|
-
nodes: leafNodes,
|
|
547
|
-
edges: flatEdges,
|
|
548
|
-
data: graph.data
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
function nodeToConfig(node, nodeIdSet) {
|
|
552
|
-
const config = {
|
|
553
|
-
id: node.id,
|
|
554
|
-
label: node.label,
|
|
555
|
-
data: node.data
|
|
556
|
-
};
|
|
557
|
-
if (node.parentId !== void 0 && node.parentId !== null) config.parentId = nodeIdSet && !nodeIdSet.has(node.parentId) ? void 0 : node.parentId;
|
|
558
|
-
if (node.initialNodeId !== void 0) config.initialNodeId = node.initialNodeId ?? void 0;
|
|
559
|
-
if (node.x !== void 0) config.x = node.x;
|
|
560
|
-
if (node.y !== void 0) config.y = node.y;
|
|
561
|
-
if (node.width !== void 0) config.width = node.width;
|
|
562
|
-
if (node.height !== void 0) config.height = node.height;
|
|
563
|
-
if (node.shape !== void 0) config.shape = node.shape;
|
|
564
|
-
if (node.color !== void 0) config.color = node.color;
|
|
565
|
-
if (node.style !== void 0) config.style = node.style;
|
|
566
|
-
return config;
|
|
567
|
-
}
|
|
568
|
-
function edgeToConfig(edge) {
|
|
569
|
-
const config = {
|
|
570
|
-
id: edge.id,
|
|
571
|
-
sourceId: edge.sourceId,
|
|
572
|
-
targetId: edge.targetId,
|
|
573
|
-
label: edge.label,
|
|
574
|
-
data: edge.data
|
|
575
|
-
};
|
|
576
|
-
if (edge.weight !== void 0) config.weight = edge.weight;
|
|
577
|
-
if (edge.x !== void 0) config.x = edge.x;
|
|
578
|
-
if (edge.y !== void 0) config.y = edge.y;
|
|
579
|
-
if (edge.width !== void 0) config.width = edge.width;
|
|
580
|
-
if (edge.height !== void 0) config.height = edge.height;
|
|
581
|
-
if (edge.color !== void 0) config.color = edge.color;
|
|
582
|
-
if (edge.style !== void 0) config.style = edge.style;
|
|
583
|
-
return config;
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Returns the induced subgraph containing only the given node IDs
|
|
587
|
-
* and edges whose endpoints are both in the set.
|
|
588
|
-
*
|
|
589
|
-
* Parent references to nodes outside the set are removed.
|
|
590
|
-
*
|
|
591
|
-
* @example
|
|
592
|
-
* ```ts
|
|
593
|
-
* import { createGraph, getSubgraph } from '@statelyai/graph';
|
|
594
|
-
*
|
|
595
|
-
* const graph = createGraph({
|
|
596
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
597
|
-
* edges: [
|
|
598
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
599
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
600
|
-
* ],
|
|
601
|
-
* });
|
|
602
|
-
*
|
|
603
|
-
* const sub = getSubgraph(graph, ['a', 'b']);
|
|
604
|
-
* // sub.nodes: [a, b], sub.edges: [ab]
|
|
605
|
-
* ```
|
|
606
|
-
*/
|
|
607
|
-
function getSubgraph(graph, nodeIds) {
|
|
608
|
-
const nodeIdSet = new Set(nodeIds);
|
|
609
|
-
return createGraph({
|
|
610
|
-
id: graph.id,
|
|
611
|
-
mode: graph.mode,
|
|
612
|
-
initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
|
|
613
|
-
nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => nodeToConfig(n, nodeIdSet)),
|
|
614
|
-
edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(edgeToConfig),
|
|
615
|
-
data: graph.data
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Returns a new graph with all edge directions flipped (source ↔ target).
|
|
620
|
-
* Optionally filters which edges to include.
|
|
621
|
-
*
|
|
622
|
-
* @example
|
|
623
|
-
* ```ts
|
|
624
|
-
* import { createGraph, reverseGraph } from '@statelyai/graph';
|
|
625
|
-
*
|
|
626
|
-
* const graph = createGraph({
|
|
627
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
628
|
-
* edges: [
|
|
629
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
630
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
631
|
-
* ],
|
|
632
|
-
* });
|
|
633
|
-
*
|
|
634
|
-
* const rev = reverseGraph(graph);
|
|
635
|
-
* // rev edges: b→a, c→b
|
|
636
|
-
*
|
|
637
|
-
* const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
|
|
638
|
-
* // filtered edges: b→a (only ab reversed, bc excluded)
|
|
639
|
-
* ```
|
|
640
|
-
*/
|
|
641
|
-
function reverseGraph(graph, filterEdge) {
|
|
642
|
-
const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
|
|
643
|
-
return createGraph({
|
|
644
|
-
id: graph.id,
|
|
645
|
-
mode: graph.mode,
|
|
646
|
-
initialNodeId: graph.initialNodeId ?? void 0,
|
|
647
|
-
nodes: graph.nodes.map((n) => nodeToConfig(n)),
|
|
648
|
-
edges: edges.map((e) => {
|
|
649
|
-
const config = edgeToConfig(e);
|
|
650
|
-
config.sourceId = e.targetId;
|
|
651
|
-
config.targetId = e.sourceId;
|
|
652
|
-
return config;
|
|
653
|
-
}),
|
|
654
|
-
data: graph.data
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
551
|
//#endregion
|
|
659
552
|
//#region src/walks.ts
|
|
660
553
|
function mulberry32(seed) {
|
|
@@ -680,7 +573,24 @@ function resolveFrom(graph, from) {
|
|
|
680
573
|
throw new Error("Cannot determine start node: provide `from`, set graph.initialNodeId, or have exactly one source node.");
|
|
681
574
|
}
|
|
682
575
|
/**
|
|
683
|
-
*
|
|
576
|
+
* Edges traversable from a node, with the node reached by taking each one.
|
|
577
|
+
* Out-edges always; in-edges too when their effective mode is not 'directed'.
|
|
578
|
+
*/
|
|
579
|
+
function getTraversableEdges(graph, nodeId) {
|
|
580
|
+
const result = [];
|
|
581
|
+
for (const edge of getOutEdges(graph, nodeId)) result.push({
|
|
582
|
+
edge,
|
|
583
|
+
nextId: edge.targetId
|
|
584
|
+
});
|
|
585
|
+
for (const edge of getInEdges(graph, nodeId)) if (edge.sourceId !== edge.targetId && getEdgeMode(graph, edge) !== "directed") result.push({
|
|
586
|
+
edge,
|
|
587
|
+
nextId: edge.sourceId
|
|
588
|
+
});
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Random walk. At each node, picks a uniformly random traversable edge
|
|
593
|
+
* (outgoing edges, plus non-directed edges both ways).
|
|
684
594
|
* Yields steps indefinitely (may revisit nodes) until a sink node is reached.
|
|
685
595
|
*/
|
|
686
596
|
function* genRandomWalk(graph, options) {
|
|
@@ -693,11 +603,11 @@ function* genRandomWalk(graph, options) {
|
|
|
693
603
|
stepCount: 0
|
|
694
604
|
};
|
|
695
605
|
while (true) {
|
|
696
|
-
let
|
|
697
|
-
if (options?.filter)
|
|
698
|
-
if (
|
|
699
|
-
const edge =
|
|
700
|
-
const node = getNode(graph,
|
|
606
|
+
let traversable = getTraversableEdges(graph, currentId);
|
|
607
|
+
if (options?.filter) traversable = traversable.filter(({ edge: edge$1 }) => options.filter(edge$1, ctx));
|
|
608
|
+
if (traversable.length === 0) return;
|
|
609
|
+
const { edge, nextId } = traversable[Math.floor(rng() * traversable.length)];
|
|
610
|
+
const node = getNode(graph, nextId);
|
|
701
611
|
const step = {
|
|
702
612
|
edge,
|
|
703
613
|
node
|
|
@@ -725,30 +635,30 @@ function* genWeightedRandomWalk(graph, options) {
|
|
|
725
635
|
stepCount: 0
|
|
726
636
|
};
|
|
727
637
|
while (true) {
|
|
728
|
-
let
|
|
729
|
-
if (options?.filter)
|
|
730
|
-
if (
|
|
731
|
-
const weights =
|
|
638
|
+
let traversable = getTraversableEdges(graph, currentId);
|
|
639
|
+
if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
|
|
640
|
+
if (traversable.length === 0) return;
|
|
641
|
+
const weights = traversable.map(({ edge }) => Math.max(0, getWeight(edge)));
|
|
732
642
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
733
643
|
if (total === 0) return;
|
|
734
644
|
let r = rng() * total;
|
|
735
|
-
let chosen =
|
|
736
|
-
for (let i = 0; i <
|
|
645
|
+
let chosen = traversable[0];
|
|
646
|
+
for (let i = 0; i < traversable.length; i++) {
|
|
737
647
|
r -= weights[i];
|
|
738
648
|
if (r <= 0) {
|
|
739
|
-
chosen =
|
|
649
|
+
chosen = traversable[i];
|
|
740
650
|
break;
|
|
741
651
|
}
|
|
742
652
|
}
|
|
743
|
-
const node = getNode(graph, chosen.
|
|
653
|
+
const node = getNode(graph, chosen.nextId);
|
|
744
654
|
const step = {
|
|
745
|
-
edge: chosen,
|
|
655
|
+
edge: chosen.edge,
|
|
746
656
|
node
|
|
747
657
|
};
|
|
748
658
|
currentId = node.id;
|
|
749
659
|
ctx.currentNodeId = currentId;
|
|
750
660
|
ctx.visitedNodes.add(currentId);
|
|
751
|
-
ctx.visitedEdges.add(chosen.id);
|
|
661
|
+
ctx.visitedEdges.add(chosen.edge.id);
|
|
752
662
|
ctx.stepCount++;
|
|
753
663
|
options?.onStep?.(step, ctx);
|
|
754
664
|
yield step;
|
|
@@ -756,8 +666,9 @@ function* genWeightedRandomWalk(graph, options) {
|
|
|
756
666
|
}
|
|
757
667
|
/**
|
|
758
668
|
* Quick random walk targeting unvisited edges.
|
|
759
|
-
* If unvisited
|
|
760
|
-
* Otherwise,
|
|
669
|
+
* If unvisited traversable edges exist at the current node, picks one randomly.
|
|
670
|
+
* Otherwise, walks the fewest-hop path (BFS, honoring `filter` and edge modes)
|
|
671
|
+
* to the nearest unvisited edge. Ends when no unvisited edge is reachable.
|
|
761
672
|
*/
|
|
762
673
|
function* genQuickRandomWalk(graph, options) {
|
|
763
674
|
const rng = makeRng(options?.seed);
|
|
@@ -770,13 +681,16 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
770
681
|
visitedEdges,
|
|
771
682
|
stepCount: 0
|
|
772
683
|
};
|
|
684
|
+
const allowedEdges = (nodeId) => {
|
|
685
|
+
let traversable = getTraversableEdges(graph, nodeId);
|
|
686
|
+
if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
|
|
687
|
+
return traversable;
|
|
688
|
+
};
|
|
773
689
|
while (visitedEdges.size < allEdgeIds.size) {
|
|
774
|
-
|
|
775
|
-
if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
|
|
776
|
-
const unvisited = edges.filter((e) => !visitedEdges.has(e.id));
|
|
690
|
+
const unvisited = allowedEdges(currentId).filter(({ edge }) => !visitedEdges.has(edge.id));
|
|
777
691
|
if (unvisited.length > 0) {
|
|
778
|
-
const edge = unvisited[Math.floor(rng() * unvisited.length)];
|
|
779
|
-
const node = getNode(graph,
|
|
692
|
+
const { edge, nextId } = unvisited[Math.floor(rng() * unvisited.length)];
|
|
693
|
+
const node = getNode(graph, nextId);
|
|
780
694
|
const step = {
|
|
781
695
|
edge,
|
|
782
696
|
node
|
|
@@ -789,22 +703,54 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
789
703
|
options?.onStep?.(step, ctx);
|
|
790
704
|
yield step;
|
|
791
705
|
} else {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
706
|
+
const prevStep = /* @__PURE__ */ new Map();
|
|
707
|
+
const seen = new Set([currentId]);
|
|
708
|
+
const queue = [currentId];
|
|
709
|
+
let found;
|
|
710
|
+
while (queue.length > 0 && !found) {
|
|
711
|
+
const id = queue.shift();
|
|
712
|
+
for (const t of allowedEdges(id)) {
|
|
713
|
+
if (!visitedEdges.has(t.edge.id)) {
|
|
714
|
+
found = {
|
|
715
|
+
atId: id,
|
|
716
|
+
edge: t.edge,
|
|
717
|
+
nextId: t.nextId
|
|
718
|
+
};
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
if (!seen.has(t.nextId)) {
|
|
722
|
+
seen.add(t.nextId);
|
|
723
|
+
prevStep.set(t.nextId, {
|
|
724
|
+
edge: t.edge,
|
|
725
|
+
fromId: id
|
|
726
|
+
});
|
|
727
|
+
queue.push(t.nextId);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
796
730
|
}
|
|
797
|
-
if (!
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
731
|
+
if (!found) return;
|
|
732
|
+
const pathSteps = [{
|
|
733
|
+
edge: found.edge,
|
|
734
|
+
nextId: found.nextId
|
|
735
|
+
}];
|
|
736
|
+
let cursor = found.atId;
|
|
737
|
+
while (cursor !== currentId) {
|
|
738
|
+
const p = prevStep.get(cursor);
|
|
739
|
+
pathSteps.unshift({
|
|
740
|
+
edge: p.edge,
|
|
741
|
+
nextId: cursor
|
|
742
|
+
});
|
|
743
|
+
cursor = p.fromId;
|
|
744
|
+
}
|
|
745
|
+
for (const { edge, nextId } of pathSteps) {
|
|
746
|
+
const step = {
|
|
747
|
+
edge,
|
|
748
|
+
node: getNode(graph, nextId)
|
|
749
|
+
};
|
|
750
|
+
currentId = nextId;
|
|
805
751
|
ctx.currentNodeId = currentId;
|
|
806
752
|
ctx.visitedNodes.add(currentId);
|
|
807
|
-
visitedEdges.add(
|
|
753
|
+
visitedEdges.add(edge.id);
|
|
808
754
|
ctx.stepCount++;
|
|
809
755
|
options?.onStep?.(step, ctx);
|
|
810
756
|
yield step;
|
|
@@ -815,14 +761,19 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
815
761
|
/**
|
|
816
762
|
* Walk a predefined sequence of edge IDs.
|
|
817
763
|
* Validates each edge exists and connects from the current position.
|
|
764
|
+
* Edges whose effective mode is not `'directed'` may be traversed
|
|
765
|
+
* target → source as well.
|
|
818
766
|
*/
|
|
819
767
|
function* genPredefinedWalk(graph, edgeIds, options) {
|
|
820
768
|
let currentId = resolveFrom(graph, options?.from);
|
|
821
769
|
for (const edgeId of edgeIds) {
|
|
822
770
|
const edge = graph.edges.find((e) => e.id === edgeId);
|
|
823
771
|
if (!edge) throw new Error(`Edge "${edgeId}" not found in graph.`);
|
|
824
|
-
|
|
825
|
-
|
|
772
|
+
let nextId;
|
|
773
|
+
if (edge.sourceId === currentId) nextId = edge.targetId;
|
|
774
|
+
else if (edge.targetId === currentId && getEdgeMode(graph, edge) !== "directed") nextId = edge.sourceId;
|
|
775
|
+
else throw new Error(`Edge "${edgeId}" connects "${edge.sourceId}" → "${edge.targetId}" but current position is "${currentId}".`);
|
|
776
|
+
const node = getNode(graph, nextId);
|
|
826
777
|
currentId = node.id;
|
|
827
778
|
yield {
|
|
828
779
|
edge,
|
|
@@ -833,7 +784,7 @@ function* genPredefinedWalk(graph, edgeIds, options) {
|
|
|
833
784
|
/**
|
|
834
785
|
* Yield at most `n` steps from the source generator.
|
|
835
786
|
*/
|
|
836
|
-
function*
|
|
787
|
+
function* genWalkSteps(gen, n) {
|
|
837
788
|
let count = 0;
|
|
838
789
|
for (const step of gen) {
|
|
839
790
|
yield step;
|
|
@@ -841,31 +792,50 @@ function* takeSteps(gen, n) {
|
|
|
841
792
|
}
|
|
842
793
|
}
|
|
843
794
|
/**
|
|
795
|
+
* @deprecated Use {@link genWalkSteps}.
|
|
796
|
+
*/
|
|
797
|
+
function* takeSteps(gen, n) {
|
|
798
|
+
yield* genWalkSteps(gen, n);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
844
801
|
* Yield steps until a specific node is reached.
|
|
845
802
|
*/
|
|
846
|
-
function*
|
|
803
|
+
function* genWalkUntilNode(gen, nodeId) {
|
|
847
804
|
for (const step of gen) {
|
|
848
805
|
yield step;
|
|
849
806
|
if (step.node.id === nodeId) return;
|
|
850
807
|
}
|
|
851
808
|
}
|
|
852
809
|
/**
|
|
810
|
+
* @deprecated Use {@link genWalkUntilNode}.
|
|
811
|
+
*/
|
|
812
|
+
function* takeUntilNode(gen, nodeId) {
|
|
813
|
+
yield* genWalkUntilNode(gen, nodeId);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
853
816
|
* Yield steps until a specific edge is traversed.
|
|
854
817
|
*/
|
|
855
|
-
function*
|
|
818
|
+
function* genWalkUntilEdge(gen, edgeId) {
|
|
856
819
|
for (const step of gen) {
|
|
857
820
|
yield step;
|
|
858
821
|
if (step.edge.id === edgeId) return;
|
|
859
822
|
}
|
|
860
823
|
}
|
|
861
824
|
/**
|
|
825
|
+
* @deprecated Use {@link genWalkUntilEdge}.
|
|
826
|
+
*/
|
|
827
|
+
function* takeUntilEdge(gen, edgeId) {
|
|
828
|
+
yield* genWalkUntilEdge(gen, edgeId);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
862
831
|
* Yield steps until node coverage reaches the target (0–1).
|
|
863
832
|
*/
|
|
864
|
-
function*
|
|
833
|
+
function* genWalkUntilNodeCoverage(gen, graph, coverage, options) {
|
|
865
834
|
const totalNodes = graph.nodes.length;
|
|
866
835
|
const target = Math.ceil(coverage * totalNodes);
|
|
867
836
|
const startId = options?.from ?? graph.initialNodeId ?? graph.nodes[0]?.id;
|
|
868
837
|
const visited = new Set(startId ? [startId] : []);
|
|
838
|
+
if (visited.size >= target) return;
|
|
869
839
|
for (const step of gen) {
|
|
870
840
|
visited.add(step.node.id);
|
|
871
841
|
yield step;
|
|
@@ -873,12 +843,19 @@ function* takeUntilNodeCoverage(gen, graph, coverage, options) {
|
|
|
873
843
|
}
|
|
874
844
|
}
|
|
875
845
|
/**
|
|
846
|
+
* @deprecated Use {@link genWalkUntilNodeCoverage}.
|
|
847
|
+
*/
|
|
848
|
+
function* takeUntilNodeCoverage(gen, graph, coverage, options) {
|
|
849
|
+
yield* genWalkUntilNodeCoverage(gen, graph, coverage, options);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
876
852
|
* Yield steps until edge coverage reaches the target (0–1).
|
|
877
853
|
*/
|
|
878
|
-
function*
|
|
854
|
+
function* genWalkUntilEdgeCoverage(gen, graph, coverage) {
|
|
879
855
|
const totalEdges = graph.edges.length;
|
|
880
856
|
const target = Math.ceil(coverage * totalEdges);
|
|
881
857
|
const visited = /* @__PURE__ */ new Set();
|
|
858
|
+
if (target <= 0) return;
|
|
882
859
|
for (const step of gen) {
|
|
883
860
|
visited.add(step.edge.id);
|
|
884
861
|
yield step;
|
|
@@ -886,6 +863,12 @@ function* takeUntilEdgeCoverage(gen, graph, coverage) {
|
|
|
886
863
|
}
|
|
887
864
|
}
|
|
888
865
|
/**
|
|
866
|
+
* @deprecated Use {@link genWalkUntilEdgeCoverage}.
|
|
867
|
+
*/
|
|
868
|
+
function* takeUntilEdgeCoverage(gen, graph, coverage) {
|
|
869
|
+
yield* genWalkUntilEdgeCoverage(gen, graph, coverage);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
889
872
|
* Compute coverage statistics for a completed walk.
|
|
890
873
|
*/
|
|
891
874
|
function getCoverage(graph, steps, options) {
|
|
@@ -906,4 +889,4 @@ function getCoverage(graph, steps, options) {
|
|
|
906
889
|
}
|
|
907
890
|
|
|
908
891
|
//#endregion
|
|
909
|
-
export { GraphInstance, LAYOUT_KEYS, addEdge, addEntities, addNode, applyPatches, areEntitiesEqual, bfs, createFormatConverter, createGraph, createGraphEdge, createGraphFromTransition, createGraphNode, createGraphPort, createVisualGraph, deleteEdge, deleteEntities, deleteNode, dfs, flatten, genCycles, genGirvanNewmanCommunities, genPostorders, genPredefinedWalk, genPreorders, genQuickRandomWalk, genRandomWalk, genShortestPaths, genSimplePaths, genWeightedRandomWalk, getAStarPath, getAllPairsShortestPaths, getAncestors, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getChildren, getClosenessCentrality, getConnectedComponents, getCoverage, getCycles, getDegree, getDegreeCentrality, getDepth, getDescendants, getDiff, getEdge, getEdgeMode, getEdgesBetween, getEdgesByPort, getEdgesOf, getEigenvectorCentrality, getGirvanNewmanCommunities, getGreedyModularityCommunities, getHITS, getInDegree, getInDegreeCentrality, getInEdges, getLCA, getLabelPropagationCommunities, getMinimumSpanningTree, getModularity, getNeighbors, getNode, getOutDegree, getOutDegreeCentrality, getOutEdges, getPageRank, getParent, getPatches, getPort, getPorts, getPostorder, getPostorders, getPredecessors, getPreorder, getPreorders, getRelativeDistance, getRelativeDistanceMap, getRoots, getShortestPath, getShortestPaths, getSiblings, getSimplePath, getSimplePaths, getSinks, getSources, getStronglyConnectedComponents, getSubgraph, getSuccessors, getTopologicalSort, hasEdge, hasNode, hasPath, invalidateIndex, invertDiff, isAcyclic, isCompound, isConnected, isEdgeDirected, isEmptyDiff, isIsomorphic, isLayoutEqual, isLeaf, isNonLayoutEqual, isTree, joinPaths, reverseGraph, takeSteps, takeUntilEdge, takeUntilEdgeCoverage, takeUntilNode, takeUntilNodeCoverage, toDiff, toPatches, updateEdge, updateEntities, updateNode };
|
|
892
|
+
export { GraphInstance, LAYOUT_KEYS, addEdge, addEntities, addNode, applyPatches, areEntitiesEqual, bfs, createCompleteGraph, createFormatConverter, createGraph, createGraphEdge, createGraphFromTransition, createGraphNode, createGraphPort, createGridGraph, createRandomGraph, createVisualGraph, deleteEdge, deleteEntities, deleteNode, dfs, flatten, genBFS, genCycles, genDFS, genGirvanNewmanCommunities, genPostorders, genPredefinedWalk, genPreorders, genQuickRandomWalk, genRandomWalk, genShortestPaths, genSimplePaths, genWalkSteps, genWalkUntilEdge, genWalkUntilEdgeCoverage, genWalkUntilNode, genWalkUntilNodeCoverage, genWeightedRandomWalk, getAStarPath, getAllPairsShortestPaths, getAncestors, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getChildren, getClosenessCentrality, getConnectedComponents, getCoreNumbers, getCoverage, getCycles, getDegree, getDegreeCentrality, getDepth, getDescendants, getDiff, getDominatorTree, getEdge, getEdgeMode, getEdgesBetween, getEdgesByPort, getEdgesOf, getEigenvectorCentrality, getFlattenedGraph, getGirvanNewmanCommunities, getGraphIssues, getGreedyModularityCommunities, getHITS, getInDegree, getInDegreeCentrality, getInEdges, getInvertedDiff, getJoinedPath, getKCore, getKatzCentrality, getLCA, getLabelPropagationCommunities, getLouvainCommunities, getMaxFlow, getMaximumBipartiteMatching, getMinCut, getMinimumSpanningTree, getModularity, getNeighbors, getNode, getOutDegree, getOutDegreeCentrality, getOutEdges, getPageRank, getParent, getPatches, getPort, getPorts, getPostorder, getPostorders, getPredecessors, getPreorder, getPreorders, getRelativeDistance, getRelativeDistanceMap, getReversedGraph, getRoots, getShortestPath, getShortestPaths, getSiblings, getSimplePath, getSimplePaths, getSinks, getSources, getStronglyConnectedComponents, getSubgraph, getSuccessors, getTopologicalSort, getTransitiveReduction, hasEdge, hasNode, hasPath, invalidateIndex, invertDiff, isAcyclic, isBipartite, isCompound, isConnected, isEdgeDirected, isEmptyDiff, isIsomorphic, isLayoutEqual, isLeaf, isNonLayoutEqual, isTree, joinPaths, reverseGraph, takeSteps, takeUntilEdge, takeUntilEdgeCoverage, takeUntilNode, takeUntilNodeCoverage, toDiff, toPatches, updateEdge, updateEntities, updateGraphWithPatches, updateNode };
|