@statelyai/graph 0.13.0 → 2.0.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 +57 -26
- package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-GeL1Cu-L.mjs} +5 -3
- package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-CsGNehct.d.mts} +137 -2
- package/dist/{algorithms-BNDQcHU3.mjs → algorithms-DF1pSQGv.mjs} +1494 -357
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/{converter-Dspillnn.mjs → converter-DyCJJfTe.mjs} +2 -2
- package/dist/{edge-list-gKe8-iRa.mjs → edge-list-BcZ0h6zz.mjs} +1 -1
- package/dist/format-support.mjs +67 -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 -60
- package/dist/formats/converter/index.mjs +1 -1
- package/dist/formats/cytoscape/index.d.mts +1 -1
- package/dist/formats/cytoscape/index.mjs +5 -3
- package/dist/formats/d2/index.d.mts +109 -0
- package/dist/formats/d2/index.mjs +1100 -0
- package/dist/formats/d3/index.d.mts +2 -2
- package/dist/formats/d3/index.mjs +5 -3
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +24 -8
- 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 +23 -16
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +30 -17
- package/dist/formats/gml/index.d.mts +1 -1
- package/dist/formats/gml/index.mjs +22 -13
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +83 -25
- package/dist/formats/jgf/index.d.mts +1 -1
- package/dist/formats/jgf/index.mjs +6 -3
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +57 -20
- package/dist/formats/tgf/index.d.mts +1 -1
- package/dist/formats/tgf/index.mjs +2 -2
- package/dist/formats/xyflow/index.d.mts +1 -1
- package/dist/formats/xyflow/index.mjs +33 -6
- package/dist/index-D51lJnt2.d.mts +61 -0
- package/dist/index-DWmo1mIp.d.mts +697 -0
- package/dist/index.d.mts +6 -631
- package/dist/index.mjs +144 -295
- package/dist/mode-D8OnHFBk.mjs +15 -0
- package/dist/queries-BfXeTXRf.d.mts +547 -0
- package/dist/queries-KirMDR7e.mjs +980 -0
- package/dist/queries.d.mts +1 -514
- package/dist/queries.mjs +1 -766
- package/dist/schemas.d.mts +21 -10
- package/dist/schemas.mjs +35 -86
- package/dist/{types-CnZ01raw.d.mts → types-DNYdIU21.d.mts} +83 -11
- package/dist/validate-TtH-x3JV.mjs +190 -0
- package/package.json +14 -3
- package/schemas/edge.schema.json +11 -0
- package/schemas/graph.schema.json +24 -3
- package/schemas/node.schema.json +6 -0
- package/dist/indexing-DUl3kTqm.mjs +0 -137
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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-KirMDR7e.mjs";
|
|
2
|
+
import { $ as joinPaths, A as dfs, B as toEdgeConfig, C as genPostorders, D as getPreorder, E as getPostorders, F as isConnected, G as getAStarPath, H as genCycles, I as isTree, J as getShortestPath, K as getAllPairsShortestPaths, L as flatten, M as getTopologicalSort, N as hasPath, O as getPreorders, P as isAcyclic, Q as getStronglyConnectedComponents, R as getSubgraph, S as getMinimumSpanningTree, T as getPostorder, U as genShortestPaths, V as toNodeConfig, W as genSimplePaths, X as getSimplePath, Y as getShortestPaths, Z as getSimplePaths, _ as getEigenvectorCentrality, _t as updateEdge, a as isIsomorphic, at as createGraphEdge, b as getOutDegreeCentrality, c as getBridges, ct as createGraphPort, d as getGreedyModularityCommunities, dt as deleteEntities, et as GraphInstance, f as getLabelPropagationCommunities, ft as deleteNode, g as getDegreeCentrality, gt as hasNode, h as getClosenessCentrality, ht as hasEdge, i as getLouvainCommunities, it as createGraph, j as getConnectedComponents, k as bfs, l as genGirvanNewmanCommunities, lt as createVisualGraph, m as getBetweennessCentrality, mt as getNode, n as getDominatorTree, nt as addEntities, o as getArticulationPoints, ot as createGraphFromTransition, p as getModularity, pt as getEdge, q as getCycles, r as getMaxFlow, rt as addNode, s as getBiconnectedComponents, st as createGraphNode, t as getTransitiveReduction, tt as addEdge, u as getGirvanNewmanCommunities, ut as deleteEdge, v as getHITS, vt as updateEntities, w as genPreorders, x as getPageRank, y as getInDegreeCentrality, yt as updateNode, z as reverseGraph } from "./algorithms-DF1pSQGv.mjs";
|
|
3
|
+
import { n as isEdgeDirected, t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
|
|
4
|
+
import { t as getGraphIssues } from "./validate-TtH-x3JV.mjs";
|
|
5
|
+
import { n as createFormatConverter } from "./converter-DyCJJfTe.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/equivalence.ts
|
|
7
8
|
/** Shallow-compare two values, returning true if they differ. */
|
|
@@ -51,7 +52,7 @@ const LAYOUT_KEY_SET = {
|
|
|
51
52
|
* ```
|
|
52
53
|
*/
|
|
53
54
|
function areEntitiesEqual(a, b, keys) {
|
|
54
|
-
const compareKeys = keys && keys.length > 0 ? keys : Object.keys(a);
|
|
55
|
+
const compareKeys = keys && keys.length > 0 ? keys : [...new Set([...Object.keys(a), ...Object.keys(b)])];
|
|
55
56
|
for (const key of compareKeys) if (differs$1(a[key], b[key])) return false;
|
|
56
57
|
return true;
|
|
57
58
|
}
|
|
@@ -86,47 +87,16 @@ function isLayoutEqual(a, b) {
|
|
|
86
87
|
*/
|
|
87
88
|
function isNonLayoutEqual(a, b) {
|
|
88
89
|
const skip = LAYOUT_KEY_SET[a.type];
|
|
89
|
-
const keys = Object.keys(a);
|
|
90
|
-
for (
|
|
91
|
-
if (skip.has(
|
|
92
|
-
if (differs$1(a[
|
|
90
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
if (skip.has(key)) continue;
|
|
93
|
+
if (differs$1(a[key], b[key])) return false;
|
|
93
94
|
}
|
|
94
95
|
return true;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
//#endregion
|
|
98
99
|
//#region src/diff.ts
|
|
99
|
-
function nodeToConfig$1(node) {
|
|
100
|
-
const config = { id: node.id };
|
|
101
|
-
if (node.parentId) config.parentId = node.parentId;
|
|
102
|
-
if (node.initialNodeId) config.initialNodeId = node.initialNodeId;
|
|
103
|
-
if (node.label !== "") config.label = node.label;
|
|
104
|
-
if (node.data !== void 0) config.data = node.data;
|
|
105
|
-
if (node.x !== void 0) config.x = node.x;
|
|
106
|
-
if (node.y !== void 0) config.y = node.y;
|
|
107
|
-
if (node.width !== void 0) config.width = node.width;
|
|
108
|
-
if (node.height !== void 0) config.height = node.height;
|
|
109
|
-
if (node.shape !== void 0) config.shape = node.shape;
|
|
110
|
-
if (node.color !== void 0) config.color = node.color;
|
|
111
|
-
if (node.style !== void 0) config.style = node.style;
|
|
112
|
-
return config;
|
|
113
|
-
}
|
|
114
|
-
function edgeToConfig$1(edge) {
|
|
115
|
-
const config = {
|
|
116
|
-
id: edge.id,
|
|
117
|
-
sourceId: edge.sourceId,
|
|
118
|
-
targetId: edge.targetId
|
|
119
|
-
};
|
|
120
|
-
if (edge.label !== "") config.label = edge.label;
|
|
121
|
-
if (edge.data !== void 0) config.data = edge.data;
|
|
122
|
-
if (edge.x !== void 0) config.x = edge.x;
|
|
123
|
-
if (edge.y !== void 0) config.y = edge.y;
|
|
124
|
-
if (edge.width !== void 0) config.width = edge.width;
|
|
125
|
-
if (edge.height !== void 0) config.height = edge.height;
|
|
126
|
-
if (edge.color !== void 0) config.color = edge.color;
|
|
127
|
-
if (edge.style !== void 0) config.style = edge.style;
|
|
128
|
-
return config;
|
|
129
|
-
}
|
|
130
100
|
/** Shallow-compare two values, returning true if they differ. */
|
|
131
101
|
function differs(a, b) {
|
|
132
102
|
if (a === b) return false;
|
|
@@ -139,6 +109,7 @@ const NODE_COMPARE_KEYS = [
|
|
|
139
109
|
"initialNodeId",
|
|
140
110
|
"label",
|
|
141
111
|
"data",
|
|
112
|
+
"ports",
|
|
142
113
|
"x",
|
|
143
114
|
"y",
|
|
144
115
|
"width",
|
|
@@ -152,6 +123,10 @@ const EDGE_COMPARE_KEYS = [
|
|
|
152
123
|
"targetId",
|
|
153
124
|
"label",
|
|
154
125
|
"data",
|
|
126
|
+
"weight",
|
|
127
|
+
"mode",
|
|
128
|
+
"sourcePort",
|
|
129
|
+
"targetPort",
|
|
155
130
|
"x",
|
|
156
131
|
"y",
|
|
157
132
|
"width",
|
|
@@ -193,13 +168,17 @@ function getDiff(a, b) {
|
|
|
193
168
|
};
|
|
194
169
|
for (const [id, nodeB] of bNodeMap) {
|
|
195
170
|
const nodeA = aNodeMap.get(id);
|
|
196
|
-
if (!nodeA) diff.nodes.added.push(
|
|
171
|
+
if (!nodeA) diff.nodes.added.push(toNodeConfig(nodeB));
|
|
197
172
|
else {
|
|
198
173
|
const oldPartial = {};
|
|
199
174
|
const newPartial = {};
|
|
200
|
-
for (const key of NODE_COMPARE_KEYS)
|
|
201
|
-
|
|
202
|
-
|
|
175
|
+
for (const key of NODE_COMPARE_KEYS) {
|
|
176
|
+
const oldValue = nodeA[key] ?? null;
|
|
177
|
+
const newValue = nodeB[key] ?? null;
|
|
178
|
+
if (differs(oldValue, newValue)) {
|
|
179
|
+
oldPartial[key] = oldValue;
|
|
180
|
+
newPartial[key] = newValue;
|
|
181
|
+
}
|
|
203
182
|
}
|
|
204
183
|
if (Object.keys(oldPartial).length > 0) diff.nodes.updated.push({
|
|
205
184
|
id,
|
|
@@ -208,16 +187,20 @@ function getDiff(a, b) {
|
|
|
208
187
|
});
|
|
209
188
|
}
|
|
210
189
|
}
|
|
211
|
-
for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(
|
|
190
|
+
for (const [id, nodeA] of aNodeMap) if (!bNodeMap.has(id)) diff.nodes.removed.push(toNodeConfig(nodeA));
|
|
212
191
|
for (const [id, edgeB] of bEdgeMap) {
|
|
213
192
|
const edgeA = aEdgeMap.get(id);
|
|
214
|
-
if (!edgeA) diff.edges.added.push(
|
|
193
|
+
if (!edgeA) diff.edges.added.push(toEdgeConfig(edgeB));
|
|
215
194
|
else {
|
|
216
195
|
const oldPartial = {};
|
|
217
196
|
const newPartial = {};
|
|
218
|
-
for (const key of EDGE_COMPARE_KEYS)
|
|
219
|
-
|
|
220
|
-
|
|
197
|
+
for (const key of EDGE_COMPARE_KEYS) {
|
|
198
|
+
const oldValue = edgeA[key] ?? null;
|
|
199
|
+
const newValue = edgeB[key] ?? null;
|
|
200
|
+
if (differs(oldValue, newValue)) {
|
|
201
|
+
oldPartial[key] = oldValue;
|
|
202
|
+
newPartial[key] = newValue;
|
|
203
|
+
}
|
|
221
204
|
}
|
|
222
205
|
if (Object.keys(oldPartial).length > 0) diff.edges.updated.push({
|
|
223
206
|
id,
|
|
@@ -226,7 +209,7 @@ function getDiff(a, b) {
|
|
|
226
209
|
});
|
|
227
210
|
}
|
|
228
211
|
}
|
|
229
|
-
for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(
|
|
212
|
+
for (const [id, edgeA] of aEdgeMap) if (!bEdgeMap.has(id)) diff.edges.removed.push(toEdgeConfig(edgeA));
|
|
230
213
|
return diff;
|
|
231
214
|
}
|
|
232
215
|
/**
|
|
@@ -263,28 +246,29 @@ function isEmptyDiff(diff) {
|
|
|
263
246
|
function invertDiff(diff) {
|
|
264
247
|
return {
|
|
265
248
|
nodes: {
|
|
266
|
-
added: diff.nodes.removed,
|
|
267
|
-
removed: diff.nodes.added,
|
|
249
|
+
added: diff.nodes.removed.map((c) => structuredClone(c)),
|
|
250
|
+
removed: diff.nodes.added.map((c) => structuredClone(c)),
|
|
268
251
|
updated: diff.nodes.updated.map((c) => ({
|
|
269
252
|
id: c.id,
|
|
270
|
-
old: c.new,
|
|
271
|
-
new: c.old
|
|
253
|
+
old: structuredClone(c.new),
|
|
254
|
+
new: structuredClone(c.old)
|
|
272
255
|
}))
|
|
273
256
|
},
|
|
274
257
|
edges: {
|
|
275
|
-
added: diff.edges.removed,
|
|
276
|
-
removed: diff.edges.added,
|
|
258
|
+
added: diff.edges.removed.map((c) => structuredClone(c)),
|
|
259
|
+
removed: diff.edges.added.map((c) => structuredClone(c)),
|
|
277
260
|
updated: diff.edges.updated.map((c) => ({
|
|
278
261
|
id: c.id,
|
|
279
|
-
old: c.new,
|
|
280
|
-
new: c.old
|
|
262
|
+
old: structuredClone(c.new),
|
|
263
|
+
new: structuredClone(c.old)
|
|
281
264
|
}))
|
|
282
265
|
}
|
|
283
266
|
};
|
|
284
267
|
}
|
|
285
268
|
/**
|
|
286
269
|
* Compute an ordered patch list from graph `a` to graph `b`.
|
|
287
|
-
* Order
|
|
270
|
+
* Order (see {@link toPatches}): add nodes → update edges → delete edges →
|
|
271
|
+
* delete nodes → add edges → update nodes.
|
|
288
272
|
*
|
|
289
273
|
* @example
|
|
290
274
|
* ```ts
|
|
@@ -364,7 +348,7 @@ function toPatches(diff) {
|
|
|
364
348
|
});
|
|
365
349
|
for (const change of diff.edges.updated) {
|
|
366
350
|
const data = {};
|
|
367
|
-
for (const [key, value] of Object.entries(change.new)) data[key] = value;
|
|
351
|
+
for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
|
|
368
352
|
patches.push({
|
|
369
353
|
op: "updateEdge",
|
|
370
354
|
id: change.id,
|
|
@@ -385,7 +369,7 @@ function toPatches(diff) {
|
|
|
385
369
|
});
|
|
386
370
|
for (const change of diff.nodes.updated) {
|
|
387
371
|
const data = {};
|
|
388
|
-
for (const [key, value] of Object.entries(change.new)) data[key] = value;
|
|
372
|
+
for (const [key, value] of Object.entries(change.new)) data[key] = value ?? null;
|
|
389
373
|
patches.push({
|
|
390
374
|
op: "updateNode",
|
|
391
375
|
id: change.id,
|
|
@@ -459,201 +443,6 @@ function toDiff(patches) {
|
|
|
459
443
|
return diff;
|
|
460
444
|
}
|
|
461
445
|
|
|
462
|
-
//#endregion
|
|
463
|
-
//#region src/transforms.ts
|
|
464
|
-
/**
|
|
465
|
-
* Flattens a hierarchical graph into a flat graph with only leaf nodes.
|
|
466
|
-
*
|
|
467
|
-
* - Edges targeting a compound node resolve to its initial child (recursively).
|
|
468
|
-
* - Edges originating from a compound node expand to all leaf descendants.
|
|
469
|
-
* - Only leaf nodes (nodes with no children) appear in the result.
|
|
470
|
-
* - Duplicate edges (same source + target) are deduplicated.
|
|
471
|
-
*
|
|
472
|
-
* @example
|
|
473
|
-
* ```ts
|
|
474
|
-
* import { createGraph, flatten } from '@statelyai/graph';
|
|
475
|
-
*
|
|
476
|
-
* const graph = createGraph({
|
|
477
|
-
* nodes: [
|
|
478
|
-
* { id: 'parent', initialNodeId: 'child1' },
|
|
479
|
-
* { id: 'child1', parentId: 'parent' },
|
|
480
|
-
* { id: 'child2', parentId: 'parent' },
|
|
481
|
-
* { id: 'other' },
|
|
482
|
-
* ],
|
|
483
|
-
* edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
|
|
484
|
-
* });
|
|
485
|
-
*
|
|
486
|
-
* const flat = flatten(graph);
|
|
487
|
-
* // flat.nodes → [child1, child2, other] (leaf nodes only)
|
|
488
|
-
* // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
|
|
489
|
-
* ```
|
|
490
|
-
*/
|
|
491
|
-
function flatten(graph) {
|
|
492
|
-
const idx = getIndex(graph);
|
|
493
|
-
const leaves = /* @__PURE__ */ new Set();
|
|
494
|
-
for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
|
|
495
|
-
function resolveInitial(nodeId) {
|
|
496
|
-
if (leaves.has(nodeId)) return nodeId;
|
|
497
|
-
const ni = idx.nodeById.get(nodeId);
|
|
498
|
-
if (ni === void 0) return null;
|
|
499
|
-
const node = graph.nodes[ni];
|
|
500
|
-
if (node.initialNodeId) return resolveInitial(node.initialNodeId);
|
|
501
|
-
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
502
|
-
if (childIds.length > 0) return resolveInitial(childIds[0]);
|
|
503
|
-
return nodeId;
|
|
504
|
-
}
|
|
505
|
-
function getLeafDescendants(nodeId) {
|
|
506
|
-
if (leaves.has(nodeId)) return [nodeId];
|
|
507
|
-
const result = [];
|
|
508
|
-
const collect = (id) => {
|
|
509
|
-
const childIds = idx.childNodes.get(id) ?? [];
|
|
510
|
-
for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
|
|
511
|
-
else collect(childId);
|
|
512
|
-
};
|
|
513
|
-
collect(nodeId);
|
|
514
|
-
return result;
|
|
515
|
-
}
|
|
516
|
-
const edgeSeen = /* @__PURE__ */ new Set();
|
|
517
|
-
const flatEdges = [];
|
|
518
|
-
for (const edge of graph.edges) {
|
|
519
|
-
const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
|
|
520
|
-
const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
|
|
521
|
-
if (target === null) continue;
|
|
522
|
-
for (const source of sources) {
|
|
523
|
-
if (source === target) continue;
|
|
524
|
-
const key = `${source}->${target}`;
|
|
525
|
-
if (edgeSeen.has(key)) continue;
|
|
526
|
-
edgeSeen.add(key);
|
|
527
|
-
flatEdges.push({
|
|
528
|
-
type: "edge",
|
|
529
|
-
id: `${edge.id}:${source}->${target}`,
|
|
530
|
-
sourceId: source,
|
|
531
|
-
targetId: target,
|
|
532
|
-
label: edge.label,
|
|
533
|
-
data: edge.data
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => ({
|
|
538
|
-
id: n.id,
|
|
539
|
-
label: n.label,
|
|
540
|
-
data: n.data
|
|
541
|
-
}));
|
|
542
|
-
return createGraph({
|
|
543
|
-
id: graph.id,
|
|
544
|
-
type: graph.type,
|
|
545
|
-
nodes: leafNodes,
|
|
546
|
-
edges: flatEdges,
|
|
547
|
-
data: graph.data
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
function nodeToConfig(node, nodeIdSet) {
|
|
551
|
-
const config = {
|
|
552
|
-
id: node.id,
|
|
553
|
-
label: node.label,
|
|
554
|
-
data: node.data
|
|
555
|
-
};
|
|
556
|
-
if (node.parentId !== void 0 && node.parentId !== null) config.parentId = nodeIdSet && !nodeIdSet.has(node.parentId) ? void 0 : node.parentId;
|
|
557
|
-
if (node.initialNodeId !== void 0) config.initialNodeId = node.initialNodeId ?? void 0;
|
|
558
|
-
if (node.x !== void 0) config.x = node.x;
|
|
559
|
-
if (node.y !== void 0) config.y = node.y;
|
|
560
|
-
if (node.width !== void 0) config.width = node.width;
|
|
561
|
-
if (node.height !== void 0) config.height = node.height;
|
|
562
|
-
if (node.shape !== void 0) config.shape = node.shape;
|
|
563
|
-
if (node.color !== void 0) config.color = node.color;
|
|
564
|
-
if (node.style !== void 0) config.style = node.style;
|
|
565
|
-
return config;
|
|
566
|
-
}
|
|
567
|
-
function edgeToConfig(edge) {
|
|
568
|
-
const config = {
|
|
569
|
-
id: edge.id,
|
|
570
|
-
sourceId: edge.sourceId,
|
|
571
|
-
targetId: edge.targetId,
|
|
572
|
-
label: edge.label,
|
|
573
|
-
data: edge.data
|
|
574
|
-
};
|
|
575
|
-
if (edge.weight !== void 0) config.weight = edge.weight;
|
|
576
|
-
if (edge.x !== void 0) config.x = edge.x;
|
|
577
|
-
if (edge.y !== void 0) config.y = edge.y;
|
|
578
|
-
if (edge.width !== void 0) config.width = edge.width;
|
|
579
|
-
if (edge.height !== void 0) config.height = edge.height;
|
|
580
|
-
if (edge.color !== void 0) config.color = edge.color;
|
|
581
|
-
if (edge.style !== void 0) config.style = edge.style;
|
|
582
|
-
return config;
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Returns the induced subgraph containing only the given node IDs
|
|
586
|
-
* and edges whose endpoints are both in the set.
|
|
587
|
-
*
|
|
588
|
-
* Parent references to nodes outside the set are removed.
|
|
589
|
-
*
|
|
590
|
-
* @example
|
|
591
|
-
* ```ts
|
|
592
|
-
* import { createGraph, getSubgraph } from '@statelyai/graph';
|
|
593
|
-
*
|
|
594
|
-
* const graph = createGraph({
|
|
595
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
596
|
-
* edges: [
|
|
597
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
598
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
599
|
-
* ],
|
|
600
|
-
* });
|
|
601
|
-
*
|
|
602
|
-
* const sub = getSubgraph(graph, ['a', 'b']);
|
|
603
|
-
* // sub.nodes: [a, b], sub.edges: [ab]
|
|
604
|
-
* ```
|
|
605
|
-
*/
|
|
606
|
-
function getSubgraph(graph, nodeIds) {
|
|
607
|
-
const nodeIdSet = new Set(nodeIds);
|
|
608
|
-
return createGraph({
|
|
609
|
-
id: graph.id,
|
|
610
|
-
type: graph.type,
|
|
611
|
-
initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
|
|
612
|
-
nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => nodeToConfig(n, nodeIdSet)),
|
|
613
|
-
edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(edgeToConfig),
|
|
614
|
-
data: graph.data
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
/**
|
|
618
|
-
* Returns a new graph with all edge directions flipped (source ↔ target).
|
|
619
|
-
* Optionally filters which edges to include.
|
|
620
|
-
*
|
|
621
|
-
* @example
|
|
622
|
-
* ```ts
|
|
623
|
-
* import { createGraph, reverseGraph } from '@statelyai/graph';
|
|
624
|
-
*
|
|
625
|
-
* const graph = createGraph({
|
|
626
|
-
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
627
|
-
* edges: [
|
|
628
|
-
* { id: 'ab', sourceId: 'a', targetId: 'b' },
|
|
629
|
-
* { id: 'bc', sourceId: 'b', targetId: 'c' },
|
|
630
|
-
* ],
|
|
631
|
-
* });
|
|
632
|
-
*
|
|
633
|
-
* const rev = reverseGraph(graph);
|
|
634
|
-
* // rev edges: b→a, c→b
|
|
635
|
-
*
|
|
636
|
-
* const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
|
|
637
|
-
* // filtered edges: b→a (only ab reversed, bc excluded)
|
|
638
|
-
* ```
|
|
639
|
-
*/
|
|
640
|
-
function reverseGraph(graph, filterEdge) {
|
|
641
|
-
const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
|
|
642
|
-
return createGraph({
|
|
643
|
-
id: graph.id,
|
|
644
|
-
type: graph.type,
|
|
645
|
-
initialNodeId: graph.initialNodeId ?? void 0,
|
|
646
|
-
nodes: graph.nodes.map((n) => nodeToConfig(n)),
|
|
647
|
-
edges: edges.map((e) => {
|
|
648
|
-
const config = edgeToConfig(e);
|
|
649
|
-
config.sourceId = e.targetId;
|
|
650
|
-
config.targetId = e.sourceId;
|
|
651
|
-
return config;
|
|
652
|
-
}),
|
|
653
|
-
data: graph.data
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
446
|
//#endregion
|
|
658
447
|
//#region src/walks.ts
|
|
659
448
|
function mulberry32(seed) {
|
|
@@ -679,7 +468,24 @@ function resolveFrom(graph, from) {
|
|
|
679
468
|
throw new Error("Cannot determine start node: provide `from`, set graph.initialNodeId, or have exactly one source node.");
|
|
680
469
|
}
|
|
681
470
|
/**
|
|
682
|
-
*
|
|
471
|
+
* Edges traversable from a node, with the node reached by taking each one.
|
|
472
|
+
* Out-edges always; in-edges too when their effective mode is not 'directed'.
|
|
473
|
+
*/
|
|
474
|
+
function getTraversableEdges(graph, nodeId) {
|
|
475
|
+
const result = [];
|
|
476
|
+
for (const edge of getOutEdges(graph, nodeId)) result.push({
|
|
477
|
+
edge,
|
|
478
|
+
nextId: edge.targetId
|
|
479
|
+
});
|
|
480
|
+
for (const edge of getInEdges(graph, nodeId)) if (edge.sourceId !== edge.targetId && getEdgeMode(graph, edge) !== "directed") result.push({
|
|
481
|
+
edge,
|
|
482
|
+
nextId: edge.sourceId
|
|
483
|
+
});
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Random walk. At each node, picks a uniformly random traversable edge
|
|
488
|
+
* (outgoing edges, plus non-directed edges both ways).
|
|
683
489
|
* Yields steps indefinitely (may revisit nodes) until a sink node is reached.
|
|
684
490
|
*/
|
|
685
491
|
function* genRandomWalk(graph, options) {
|
|
@@ -692,11 +498,11 @@ function* genRandomWalk(graph, options) {
|
|
|
692
498
|
stepCount: 0
|
|
693
499
|
};
|
|
694
500
|
while (true) {
|
|
695
|
-
let
|
|
696
|
-
if (options?.filter)
|
|
697
|
-
if (
|
|
698
|
-
const edge =
|
|
699
|
-
const node = getNode(graph,
|
|
501
|
+
let traversable = getTraversableEdges(graph, currentId);
|
|
502
|
+
if (options?.filter) traversable = traversable.filter(({ edge: edge$1 }) => options.filter(edge$1, ctx));
|
|
503
|
+
if (traversable.length === 0) return;
|
|
504
|
+
const { edge, nextId } = traversable[Math.floor(rng() * traversable.length)];
|
|
505
|
+
const node = getNode(graph, nextId);
|
|
700
506
|
const step = {
|
|
701
507
|
edge,
|
|
702
508
|
node
|
|
@@ -724,30 +530,30 @@ function* genWeightedRandomWalk(graph, options) {
|
|
|
724
530
|
stepCount: 0
|
|
725
531
|
};
|
|
726
532
|
while (true) {
|
|
727
|
-
let
|
|
728
|
-
if (options?.filter)
|
|
729
|
-
if (
|
|
730
|
-
const weights =
|
|
533
|
+
let traversable = getTraversableEdges(graph, currentId);
|
|
534
|
+
if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
|
|
535
|
+
if (traversable.length === 0) return;
|
|
536
|
+
const weights = traversable.map(({ edge }) => Math.max(0, getWeight(edge)));
|
|
731
537
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
732
538
|
if (total === 0) return;
|
|
733
539
|
let r = rng() * total;
|
|
734
|
-
let chosen =
|
|
735
|
-
for (let i = 0; i <
|
|
540
|
+
let chosen = traversable[0];
|
|
541
|
+
for (let i = 0; i < traversable.length; i++) {
|
|
736
542
|
r -= weights[i];
|
|
737
543
|
if (r <= 0) {
|
|
738
|
-
chosen =
|
|
544
|
+
chosen = traversable[i];
|
|
739
545
|
break;
|
|
740
546
|
}
|
|
741
547
|
}
|
|
742
|
-
const node = getNode(graph, chosen.
|
|
548
|
+
const node = getNode(graph, chosen.nextId);
|
|
743
549
|
const step = {
|
|
744
|
-
edge: chosen,
|
|
550
|
+
edge: chosen.edge,
|
|
745
551
|
node
|
|
746
552
|
};
|
|
747
553
|
currentId = node.id;
|
|
748
554
|
ctx.currentNodeId = currentId;
|
|
749
555
|
ctx.visitedNodes.add(currentId);
|
|
750
|
-
ctx.visitedEdges.add(chosen.id);
|
|
556
|
+
ctx.visitedEdges.add(chosen.edge.id);
|
|
751
557
|
ctx.stepCount++;
|
|
752
558
|
options?.onStep?.(step, ctx);
|
|
753
559
|
yield step;
|
|
@@ -755,8 +561,9 @@ function* genWeightedRandomWalk(graph, options) {
|
|
|
755
561
|
}
|
|
756
562
|
/**
|
|
757
563
|
* Quick random walk targeting unvisited edges.
|
|
758
|
-
* If unvisited
|
|
759
|
-
* Otherwise,
|
|
564
|
+
* If unvisited traversable edges exist at the current node, picks one randomly.
|
|
565
|
+
* Otherwise, walks the fewest-hop path (BFS, honoring `filter` and edge modes)
|
|
566
|
+
* to the nearest unvisited edge. Ends when no unvisited edge is reachable.
|
|
760
567
|
*/
|
|
761
568
|
function* genQuickRandomWalk(graph, options) {
|
|
762
569
|
const rng = makeRng(options?.seed);
|
|
@@ -769,13 +576,16 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
769
576
|
visitedEdges,
|
|
770
577
|
stepCount: 0
|
|
771
578
|
};
|
|
579
|
+
const allowedEdges = (nodeId) => {
|
|
580
|
+
let traversable = getTraversableEdges(graph, nodeId);
|
|
581
|
+
if (options?.filter) traversable = traversable.filter(({ edge }) => options.filter(edge, ctx));
|
|
582
|
+
return traversable;
|
|
583
|
+
};
|
|
772
584
|
while (visitedEdges.size < allEdgeIds.size) {
|
|
773
|
-
|
|
774
|
-
if (options?.filter) edges = edges.filter((e) => options.filter(e, ctx));
|
|
775
|
-
const unvisited = edges.filter((e) => !visitedEdges.has(e.id));
|
|
585
|
+
const unvisited = allowedEdges(currentId).filter(({ edge }) => !visitedEdges.has(edge.id));
|
|
776
586
|
if (unvisited.length > 0) {
|
|
777
|
-
const edge = unvisited[Math.floor(rng() * unvisited.length)];
|
|
778
|
-
const node = getNode(graph,
|
|
587
|
+
const { edge, nextId } = unvisited[Math.floor(rng() * unvisited.length)];
|
|
588
|
+
const node = getNode(graph, nextId);
|
|
779
589
|
const step = {
|
|
780
590
|
edge,
|
|
781
591
|
node
|
|
@@ -788,22 +598,54 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
788
598
|
options?.onStep?.(step, ctx);
|
|
789
599
|
yield step;
|
|
790
600
|
} else {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
601
|
+
const prevStep = /* @__PURE__ */ new Map();
|
|
602
|
+
const seen = new Set([currentId]);
|
|
603
|
+
const queue = [currentId];
|
|
604
|
+
let found;
|
|
605
|
+
while (queue.length > 0 && !found) {
|
|
606
|
+
const id = queue.shift();
|
|
607
|
+
for (const t of allowedEdges(id)) {
|
|
608
|
+
if (!visitedEdges.has(t.edge.id)) {
|
|
609
|
+
found = {
|
|
610
|
+
atId: id,
|
|
611
|
+
edge: t.edge,
|
|
612
|
+
nextId: t.nextId
|
|
613
|
+
};
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
if (!seen.has(t.nextId)) {
|
|
617
|
+
seen.add(t.nextId);
|
|
618
|
+
prevStep.set(t.nextId, {
|
|
619
|
+
edge: t.edge,
|
|
620
|
+
fromId: id
|
|
621
|
+
});
|
|
622
|
+
queue.push(t.nextId);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
795
625
|
}
|
|
796
|
-
if (!
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
626
|
+
if (!found) return;
|
|
627
|
+
const pathSteps = [{
|
|
628
|
+
edge: found.edge,
|
|
629
|
+
nextId: found.nextId
|
|
630
|
+
}];
|
|
631
|
+
let cursor = found.atId;
|
|
632
|
+
while (cursor !== currentId) {
|
|
633
|
+
const p = prevStep.get(cursor);
|
|
634
|
+
pathSteps.unshift({
|
|
635
|
+
edge: p.edge,
|
|
636
|
+
nextId: cursor
|
|
637
|
+
});
|
|
638
|
+
cursor = p.fromId;
|
|
639
|
+
}
|
|
640
|
+
for (const { edge, nextId } of pathSteps) {
|
|
641
|
+
const step = {
|
|
642
|
+
edge,
|
|
643
|
+
node: getNode(graph, nextId)
|
|
644
|
+
};
|
|
645
|
+
currentId = nextId;
|
|
804
646
|
ctx.currentNodeId = currentId;
|
|
805
647
|
ctx.visitedNodes.add(currentId);
|
|
806
|
-
visitedEdges.add(
|
|
648
|
+
visitedEdges.add(edge.id);
|
|
807
649
|
ctx.stepCount++;
|
|
808
650
|
options?.onStep?.(step, ctx);
|
|
809
651
|
yield step;
|
|
@@ -814,14 +656,19 @@ function* genQuickRandomWalk(graph, options) {
|
|
|
814
656
|
/**
|
|
815
657
|
* Walk a predefined sequence of edge IDs.
|
|
816
658
|
* Validates each edge exists and connects from the current position.
|
|
659
|
+
* Edges whose effective mode is not `'directed'` may be traversed
|
|
660
|
+
* target → source as well.
|
|
817
661
|
*/
|
|
818
662
|
function* genPredefinedWalk(graph, edgeIds, options) {
|
|
819
663
|
let currentId = resolveFrom(graph, options?.from);
|
|
820
664
|
for (const edgeId of edgeIds) {
|
|
821
665
|
const edge = graph.edges.find((e) => e.id === edgeId);
|
|
822
666
|
if (!edge) throw new Error(`Edge "${edgeId}" not found in graph.`);
|
|
823
|
-
|
|
824
|
-
|
|
667
|
+
let nextId;
|
|
668
|
+
if (edge.sourceId === currentId) nextId = edge.targetId;
|
|
669
|
+
else if (edge.targetId === currentId && getEdgeMode(graph, edge) !== "directed") nextId = edge.sourceId;
|
|
670
|
+
else throw new Error(`Edge "${edgeId}" connects "${edge.sourceId}" → "${edge.targetId}" but current position is "${currentId}".`);
|
|
671
|
+
const node = getNode(graph, nextId);
|
|
825
672
|
currentId = node.id;
|
|
826
673
|
yield {
|
|
827
674
|
edge,
|
|
@@ -865,6 +712,7 @@ function* takeUntilNodeCoverage(gen, graph, coverage, options) {
|
|
|
865
712
|
const target = Math.ceil(coverage * totalNodes);
|
|
866
713
|
const startId = options?.from ?? graph.initialNodeId ?? graph.nodes[0]?.id;
|
|
867
714
|
const visited = new Set(startId ? [startId] : []);
|
|
715
|
+
if (visited.size >= target) return;
|
|
868
716
|
for (const step of gen) {
|
|
869
717
|
visited.add(step.node.id);
|
|
870
718
|
yield step;
|
|
@@ -878,6 +726,7 @@ function* takeUntilEdgeCoverage(gen, graph, coverage) {
|
|
|
878
726
|
const totalEdges = graph.edges.length;
|
|
879
727
|
const target = Math.ceil(coverage * totalEdges);
|
|
880
728
|
const visited = /* @__PURE__ */ new Set();
|
|
729
|
+
if (target <= 0) return;
|
|
881
730
|
for (const step of gen) {
|
|
882
731
|
visited.add(step.edge.id);
|
|
883
732
|
yield step;
|
|
@@ -905,4 +754,4 @@ function getCoverage(graph, steps, options) {
|
|
|
905
754
|
}
|
|
906
755
|
|
|
907
756
|
//#endregion
|
|
908
|
-
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, 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, isEmptyDiff, isIsomorphic, isLayoutEqual, isLeaf, isNonLayoutEqual, isTree, joinPaths, reverseGraph, takeSteps, takeUntilEdge, takeUntilEdgeCoverage, takeUntilNode, takeUntilNodeCoverage, toDiff, toPatches, updateEdge, updateEntities, updateNode };
|
|
757
|
+
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, getDominatorTree, getEdge, getEdgeMode, getEdgesBetween, getEdgesByPort, getEdgesOf, getEigenvectorCentrality, getGirvanNewmanCommunities, getGraphIssues, getGreedyModularityCommunities, getHITS, getInDegree, getInDegreeCentrality, getInEdges, getLCA, getLabelPropagationCommunities, getLouvainCommunities, getMaxFlow, 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, getTransitiveReduction, 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 };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/mode.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an edge's effective directedness. Falls back to the graph's
|
|
4
|
+
* {@link Graph.mode} when the edge has no per-edge override.
|
|
5
|
+
*/
|
|
6
|
+
function getEdgeMode(graph, edge) {
|
|
7
|
+
return edge.mode ?? graph.mode;
|
|
8
|
+
}
|
|
9
|
+
/** Whether an edge points only from source to target. */
|
|
10
|
+
function isEdgeDirected(graph, edge) {
|
|
11
|
+
return getEdgeMode(graph, edge) === "directed";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { isEdgeDirected as n, getEdgeMode as t };
|