@statelyai/graph 2.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 +67 -19
- package/dist/{algorithms-CsGNehct.d.mts → algorithms-D1cgly0g.d.mts} +145 -6
- package/dist/{algorithms-DF1pSQGv.mjs → algorithms-DBpH74hR.mjs} +673 -891
- package/dist/algorithms.d.mts +2 -2
- package/dist/algorithms.mjs +2 -2
- package/dist/config-Dt5u1gSf.mjs +793 -0
- package/dist/{converter-DyCJJfTe.mjs → converter-DB6Rg6Vd.mjs} +2 -2
- 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 +8 -4
- package/dist/formats/d2/index.d.mts +1 -1
- package/dist/formats/d2/index.mjs +1 -1
- package/dist/formats/d3/index.d.mts +4 -4
- package/dist/formats/d3/index.mjs +8 -4
- package/dist/formats/dot/index.d.mts +1 -1
- package/dist/formats/dot/index.mjs +1 -1
- 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 +43 -11
- package/dist/formats/gexf/index.d.mts +1 -1
- package/dist/formats/gexf/index.mjs +22 -2
- package/dist/formats/gml/index.d.mts +4 -4
- package/dist/formats/gml/index.mjs +8 -4
- package/dist/formats/graphml/index.d.mts +1 -1
- package/dist/formats/graphml/index.mjs +24 -2
- package/dist/formats/jgf/index.d.mts +4 -4
- package/dist/formats/jgf/index.mjs +8 -4
- package/dist/formats/mermaid/index.d.mts +1 -1
- package/dist/formats/mermaid/index.mjs +1 -1
- 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 +11 -6
- package/dist/{index-D51lJnt2.d.mts → index-BlbSWUvH.d.mts} +1 -1
- package/dist/{index-DWmo1mIp.d.mts → index-CNvqxPLJ.d.mts} +82 -14
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +152 -17
- 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-BfXeTXRf.d.mts → queries-B6quF529.d.mts} +1 -1
- package/dist/{queries-KirMDR7e.mjs → queries-BMM0XAv_.mjs} +23 -17
- package/dist/queries.d.mts +1 -1
- package/dist/queries.mjs +1 -1
- package/dist/schemas.d.mts +19 -1
- package/dist/schemas.mjs +10 -1
- package/dist/{types-DNYdIU21.d.mts → types-BAEQTwK_.d.mts} +46 -3
- package/package.json +47 -5
- package/schemas/edge.schema.json +27 -0
- package/schemas/graph.schema.json +27 -0
- /package/dist/{adjacency-list-GeL1Cu-L.mjs → adjacency-list-DQ32Mmhx.mjs} +0 -0
- /package/dist/{edge-list-BcZ0h6zz.mjs → edge-list-CA9UTvn2.mjs} +0 -0
- /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
- /package/dist/{validate-TtH-x3JV.mjs → validate-BsfSOv0S.mjs} +0 -0
|
@@ -1,719 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
1
|
+
import { O as getIndex, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-BMM0XAv_.mjs";
|
|
2
|
+
import { n as toNodeConfig, s as createGraph, t as toEdgeConfig } from "./config-Dt5u1gSf.mjs";
|
|
3
|
+
import { t as getEdgeMode } from "./mode-gu_mhKKs.mjs";
|
|
3
4
|
|
|
4
|
-
//#region src/graph.ts
|
|
5
|
-
/**
|
|
6
|
-
* Create a resolved graph port from a config. Fills in defaults.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* const port = createGraphPort({ name: 'output', direction: 'out' });
|
|
11
|
-
* // { name: 'output', direction: 'out', data: null }
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
function createGraphPort(config) {
|
|
15
|
-
if (!config.name) throw new Error("Port name must be a non-empty string");
|
|
16
|
-
const port = {
|
|
17
|
-
name: config.name,
|
|
18
|
-
direction: config.direction ?? "inout",
|
|
19
|
-
data: config.data ?? null
|
|
20
|
-
};
|
|
21
|
-
if (config.label !== void 0) port.label = config.label;
|
|
22
|
-
if (config.x !== void 0) port.x = config.x;
|
|
23
|
-
if (config.y !== void 0) port.y = config.y;
|
|
24
|
-
if (config.width !== void 0) port.width = config.width;
|
|
25
|
-
if (config.height !== void 0) port.height = config.height;
|
|
26
|
-
if (config.style !== void 0) port.style = config.style;
|
|
27
|
-
return port;
|
|
28
|
-
}
|
|
29
|
-
function validatePortNames(ports) {
|
|
30
|
-
const seen = /* @__PURE__ */ new Set();
|
|
31
|
-
for (const port of ports) {
|
|
32
|
-
if (seen.has(port.name)) throw new Error(`Duplicate port name "${port.name}" on node`);
|
|
33
|
-
seen.add(port.name);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Create a resolved graph node from a config. Fills in defaults.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* const node = createGraphNode({ id: 'a', data: { label: 'hi' } });
|
|
42
|
-
* // { type: 'node', id: 'a', label: '', data: { label: 'hi' } }
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
function createGraphNode(config) {
|
|
46
|
-
if (!config.id) throw new Error("Node id must be a non-empty string");
|
|
47
|
-
if (config.parentId === "") throw new Error("Node parentId must be a non-empty string");
|
|
48
|
-
const node = {
|
|
49
|
-
type: "node",
|
|
50
|
-
id: config.id,
|
|
51
|
-
...config.parentId !== void 0 && { parentId: config.parentId ?? null },
|
|
52
|
-
...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
|
|
53
|
-
label: config.label ?? null,
|
|
54
|
-
data: config.data ?? null
|
|
55
|
-
};
|
|
56
|
-
if (config.ports !== void 0 && config.ports.length > 0) {
|
|
57
|
-
validatePortNames(config.ports);
|
|
58
|
-
node.ports = config.ports.map(createGraphPort);
|
|
59
|
-
}
|
|
60
|
-
if (config.x !== void 0) node.x = config.x;
|
|
61
|
-
if (config.y !== void 0) node.y = config.y;
|
|
62
|
-
if (config.width !== void 0) node.width = config.width;
|
|
63
|
-
if (config.height !== void 0) node.height = config.height;
|
|
64
|
-
if (config.shape !== void 0) node.shape = config.shape;
|
|
65
|
-
if (config.color !== void 0) node.color = config.color;
|
|
66
|
-
if (config.style !== void 0) node.style = config.style;
|
|
67
|
-
return node;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Create a resolved graph edge from a config. Fills in defaults.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* const edge = createGraphEdge({ id: 'e1', sourceId: 'a', targetId: 'b' });
|
|
75
|
-
* // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: null }
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
function createGraphEdge(config) {
|
|
79
|
-
if (!config.id) throw new Error("Edge id must be a non-empty string");
|
|
80
|
-
if (!config.sourceId) throw new Error("Edge sourceId must be a non-empty string");
|
|
81
|
-
if (!config.targetId) throw new Error("Edge targetId must be a non-empty string");
|
|
82
|
-
const edge = {
|
|
83
|
-
type: "edge",
|
|
84
|
-
id: config.id,
|
|
85
|
-
sourceId: config.sourceId,
|
|
86
|
-
targetId: config.targetId,
|
|
87
|
-
label: config.label ?? null,
|
|
88
|
-
data: config.data ?? null
|
|
89
|
-
};
|
|
90
|
-
if (config.sourcePort !== void 0) edge.sourcePort = config.sourcePort;
|
|
91
|
-
if (config.targetPort !== void 0) edge.targetPort = config.targetPort;
|
|
92
|
-
if (config.mode !== void 0) edge.mode = config.mode;
|
|
93
|
-
if (config.weight !== void 0) edge.weight = config.weight;
|
|
94
|
-
if (config.x !== void 0) edge.x = config.x;
|
|
95
|
-
if (config.y !== void 0) edge.y = config.y;
|
|
96
|
-
if (config.width !== void 0) edge.width = config.width;
|
|
97
|
-
if (config.height !== void 0) edge.height = config.height;
|
|
98
|
-
if (config.color !== void 0) edge.color = config.color;
|
|
99
|
-
if (config.style !== void 0) edge.style = config.style;
|
|
100
|
-
return edge;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Create a graph from a config. Resolves defaults for all fields.
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```ts
|
|
107
|
-
* const graph = createGraph({
|
|
108
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
109
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
110
|
-
* });
|
|
111
|
-
* ```
|
|
112
|
-
*/
|
|
113
|
-
function createGraph(config) {
|
|
114
|
-
const graph = {
|
|
115
|
-
id: config?.id ?? "",
|
|
116
|
-
mode: config?.mode ?? "directed",
|
|
117
|
-
initialNodeId: config?.initialNodeId ?? null,
|
|
118
|
-
nodes: (config?.nodes ?? []).map(createGraphNode),
|
|
119
|
-
edges: (config?.edges ?? []).map(createGraphEdge),
|
|
120
|
-
data: config?.data ?? null
|
|
121
|
-
};
|
|
122
|
-
if (config?.direction !== void 0) graph.direction = config.direction;
|
|
123
|
-
if (config?.style !== void 0) graph.style = config.style;
|
|
124
|
-
return graph;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Create a visual graph with required position/size on all nodes and edges.
|
|
128
|
-
*
|
|
129
|
-
* @example
|
|
130
|
-
* ```ts
|
|
131
|
-
* const graph = createVisualGraph({
|
|
132
|
-
* nodes: [{ id: 'a', x: 0, y: 0, width: 100, height: 50 }],
|
|
133
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', x: 0, y: 0, width: 0, height: 0 }],
|
|
134
|
-
* });
|
|
135
|
-
* // graph.nodes[0].x === 0
|
|
136
|
-
* ```
|
|
137
|
-
*/
|
|
138
|
-
function createVisualGraph(config) {
|
|
139
|
-
const base = createGraph(config);
|
|
140
|
-
return {
|
|
141
|
-
...base,
|
|
142
|
-
direction: config?.direction ?? "down",
|
|
143
|
-
nodes: base.nodes.map((n) => {
|
|
144
|
-
const { ports, ...rest } = n;
|
|
145
|
-
return {
|
|
146
|
-
...rest,
|
|
147
|
-
x: n.x ?? 0,
|
|
148
|
-
y: n.y ?? 0,
|
|
149
|
-
width: n.width ?? 0,
|
|
150
|
-
height: n.height ?? 0,
|
|
151
|
-
...n.shape !== void 0 && { shape: n.shape },
|
|
152
|
-
...ports !== void 0 && { ports: ports.map((p) => ({
|
|
153
|
-
...p,
|
|
154
|
-
x: p.x ?? 0,
|
|
155
|
-
y: p.y ?? 0,
|
|
156
|
-
width: p.width ?? 0,
|
|
157
|
-
height: p.height ?? 0
|
|
158
|
-
})) }
|
|
159
|
-
};
|
|
160
|
-
}),
|
|
161
|
-
edges: base.edges.map((e) => ({
|
|
162
|
-
...e,
|
|
163
|
-
x: e.x ?? 0,
|
|
164
|
-
y: e.y ?? 0,
|
|
165
|
-
width: e.width ?? 0,
|
|
166
|
-
height: e.height ?? 0
|
|
167
|
-
}))
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Create a graph by BFS exploration of a transition function.
|
|
172
|
-
* Each unique state becomes a node; each (state, event) -> nextState becomes an edge.
|
|
173
|
-
*
|
|
174
|
-
* - Node IDs are determined by `serializeState` (default: `JSON.stringify`).
|
|
175
|
-
* - Edge IDs use the format `sourceId|serializedEvent|targetId` for uniqueness
|
|
176
|
-
* and debuggability. Edge labels are just the serialized event string.
|
|
177
|
-
*
|
|
178
|
-
* @example
|
|
179
|
-
* ```ts
|
|
180
|
-
* const graph = createGraphFromTransition(
|
|
181
|
-
* (state, event) => {
|
|
182
|
-
* if (state === 'green' && event === 'TIMER') return 'yellow';
|
|
183
|
-
* if (state === 'yellow' && event === 'TIMER') return 'red';
|
|
184
|
-
* if (state === 'red' && event === 'TIMER') return 'green';
|
|
185
|
-
* return state;
|
|
186
|
-
* },
|
|
187
|
-
* {
|
|
188
|
-
* initialState: 'green',
|
|
189
|
-
* events: ['TIMER'],
|
|
190
|
-
* serializeState: (s) => s,
|
|
191
|
-
* serializeEvent: (e) => e,
|
|
192
|
-
* },
|
|
193
|
-
* );
|
|
194
|
-
* // graph.nodes.length === 3
|
|
195
|
-
* ```
|
|
196
|
-
*/
|
|
197
|
-
function createGraphFromTransition(transition, options) {
|
|
198
|
-
const serializeState = options.serializeState ?? JSON.stringify;
|
|
199
|
-
const serializeEvent = options.serializeEvent ?? JSON.stringify;
|
|
200
|
-
const limit = options.limit ?? Infinity;
|
|
201
|
-
const getEvents = typeof options.events === "function" ? options.events : () => options.events;
|
|
202
|
-
const nodes = [];
|
|
203
|
-
const edges = [];
|
|
204
|
-
const visited = /* @__PURE__ */ new Set();
|
|
205
|
-
const edgeSet = /* @__PURE__ */ new Set();
|
|
206
|
-
const queue = [options.initialState];
|
|
207
|
-
const initialStateId = serializeState(options.initialState);
|
|
208
|
-
visited.add(initialStateId);
|
|
209
|
-
nodes.push({
|
|
210
|
-
id: initialStateId,
|
|
211
|
-
label: initialStateId,
|
|
212
|
-
data: options.initialState
|
|
213
|
-
});
|
|
214
|
-
let iterations = 0;
|
|
215
|
-
while (queue.length > 0) {
|
|
216
|
-
const state = queue.shift();
|
|
217
|
-
const stateId = serializeState(state);
|
|
218
|
-
if (++iterations > limit) throw new Error("Traversal limit exceeded");
|
|
219
|
-
if (options.stopWhen?.(state)) continue;
|
|
220
|
-
const events = getEvents(state);
|
|
221
|
-
for (const event of events) {
|
|
222
|
-
const nextState = transition(state, event);
|
|
223
|
-
const nextStateId = serializeState(nextState);
|
|
224
|
-
const eventStr = serializeEvent(event);
|
|
225
|
-
if (!visited.has(nextStateId)) {
|
|
226
|
-
visited.add(nextStateId);
|
|
227
|
-
nodes.push({
|
|
228
|
-
id: nextStateId,
|
|
229
|
-
label: nextStateId,
|
|
230
|
-
data: nextState
|
|
231
|
-
});
|
|
232
|
-
queue.push(nextState);
|
|
233
|
-
}
|
|
234
|
-
const edgeKey = `${stateId}|${eventStr}|${nextStateId}`;
|
|
235
|
-
if (!edgeSet.has(edgeKey)) {
|
|
236
|
-
edgeSet.add(edgeKey);
|
|
237
|
-
edges.push({
|
|
238
|
-
id: edgeKey,
|
|
239
|
-
sourceId: stateId,
|
|
240
|
-
targetId: nextStateId,
|
|
241
|
-
label: eventStr,
|
|
242
|
-
data: event
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return createGraph({
|
|
248
|
-
id: options.id ?? "",
|
|
249
|
-
mode: "directed",
|
|
250
|
-
initialNodeId: initialStateId,
|
|
251
|
-
nodes,
|
|
252
|
-
edges
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Get a node by id, or `undefined` if not found.
|
|
257
|
-
*
|
|
258
|
-
* @example
|
|
259
|
-
* ```ts
|
|
260
|
-
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
261
|
-
* const node = getNode(graph, 'a'); // GraphNode
|
|
262
|
-
* const missing = getNode(graph, 'z'); // undefined
|
|
263
|
-
* ```
|
|
264
|
-
*/
|
|
265
|
-
function getNode(graph, id) {
|
|
266
|
-
const arrayIdx = getIndex(graph).nodeById.get(id);
|
|
267
|
-
return arrayIdx !== void 0 ? graph.nodes[arrayIdx] : void 0;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Get an edge by id, or `undefined` if not found.
|
|
271
|
-
*
|
|
272
|
-
* @example
|
|
273
|
-
* ```ts
|
|
274
|
-
* const graph = createGraph({
|
|
275
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
276
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
277
|
-
* });
|
|
278
|
-
* const edge = getEdge(graph, 'e1'); // GraphEdge
|
|
279
|
-
* const missing = getEdge(graph, 'z'); // undefined
|
|
280
|
-
* ```
|
|
281
|
-
*/
|
|
282
|
-
function getEdge(graph, id) {
|
|
283
|
-
const arrayIdx = getIndex(graph).edgeById.get(id);
|
|
284
|
-
return arrayIdx !== void 0 ? graph.edges[arrayIdx] : void 0;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Check if a node exists in the graph.
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* ```ts
|
|
291
|
-
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
292
|
-
* hasNode(graph, 'a'); // true
|
|
293
|
-
* hasNode(graph, 'z'); // false
|
|
294
|
-
* ```
|
|
295
|
-
*/
|
|
296
|
-
function hasNode(graph, id) {
|
|
297
|
-
return getIndex(graph).nodeById.has(id);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Check if an edge exists in the graph.
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```ts
|
|
304
|
-
* const graph = createGraph({
|
|
305
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
306
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
307
|
-
* });
|
|
308
|
-
* hasEdge(graph, 'e1'); // true
|
|
309
|
-
* hasEdge(graph, 'z'); // false
|
|
310
|
-
* ```
|
|
311
|
-
*/
|
|
312
|
-
function hasEdge(graph, id) {
|
|
313
|
-
return getIndex(graph).edgeById.has(id);
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place.
|
|
317
|
-
* @returns The resolved node that was added.
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```ts
|
|
321
|
-
* const graph = createGraph();
|
|
322
|
-
* const node = addNode(graph, { id: 'a', label: 'Node A' });
|
|
323
|
-
* // graph.nodes.length === 1
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
function addNode(graph, config) {
|
|
327
|
-
const node = createGraphNode(config);
|
|
328
|
-
const idx = getIndex(graph);
|
|
329
|
-
if (idx.nodeById.has(config.id)) throw new Error(`Node "${config.id}" already exists`);
|
|
330
|
-
if (config.parentId && !idx.nodeById.has(config.parentId)) throw new Error(`Parent node "${config.parentId}" does not exist`);
|
|
331
|
-
indexAddNode(idx, node, graph.nodes.push(node) - 1);
|
|
332
|
-
return node;
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place.
|
|
336
|
-
* @returns The resolved edge that was added.
|
|
337
|
-
*
|
|
338
|
-
* @example
|
|
339
|
-
* ```ts
|
|
340
|
-
* const graph = createGraph({ nodes: [{ id: 'a' }, { id: 'b' }] });
|
|
341
|
-
* const edge = addEdge(graph, { id: 'e1', sourceId: 'a', targetId: 'b' });
|
|
342
|
-
* // graph.edges.length === 1
|
|
343
|
-
* ```
|
|
344
|
-
*/
|
|
345
|
-
function addEdge(graph, config) {
|
|
346
|
-
const edge = createGraphEdge(config);
|
|
347
|
-
const idx = getIndex(graph);
|
|
348
|
-
if (idx.edgeById.has(config.id)) throw new Error(`Edge "${config.id}" already exists`);
|
|
349
|
-
if (!idx.nodeById.has(config.sourceId)) throw new Error(`Source node "${config.sourceId}" does not exist`);
|
|
350
|
-
if (!idx.nodeById.has(config.targetId)) throw new Error(`Target node "${config.targetId}" does not exist`);
|
|
351
|
-
if (config.sourcePort !== void 0) {
|
|
352
|
-
if (!graph.nodes[idx.nodeById.get(config.sourceId)].ports?.some((p) => p.name === config.sourcePort)) throw new Error(`Port "${config.sourcePort}" does not exist on source node "${config.sourceId}"`);
|
|
353
|
-
}
|
|
354
|
-
if (config.targetPort !== void 0) {
|
|
355
|
-
if (!graph.nodes[idx.nodeById.get(config.targetId)].ports?.some((p) => p.name === config.targetPort)) throw new Error(`Port "${config.targetPort}" does not exist on target node "${config.targetId}"`);
|
|
356
|
-
}
|
|
357
|
-
indexAddEdge(idx, edge, graph.edges.push(edge) - 1);
|
|
358
|
-
return edge;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* **Mutable.** Delete a node and its connected edges. Mutates `graph.nodes`
|
|
362
|
-
* and `graph.edges` in place.
|
|
363
|
-
*
|
|
364
|
-
* By default, children are deleted recursively.
|
|
365
|
-
* With `{ reparent: true }`, children are re-parented to the deleted node's parent.
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* ```ts
|
|
369
|
-
* const graph = createGraph({
|
|
370
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
371
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
372
|
-
* });
|
|
373
|
-
* deleteNode(graph, 'a');
|
|
374
|
-
* // graph.nodes.length === 1, edge e1 also removed
|
|
375
|
-
* ```
|
|
376
|
-
*/
|
|
377
|
-
function deleteNode(graph, id, opts) {
|
|
378
|
-
const node = getNode(graph, id);
|
|
379
|
-
if (!node) throw new Error(`Node "${id}" does not exist`);
|
|
380
|
-
if (opts?.reparent) {
|
|
381
|
-
for (const n of graph.nodes) if (n.parentId === id) n.parentId = node.parentId;
|
|
382
|
-
graph.nodes = graph.nodes.filter((n) => n.id !== id);
|
|
383
|
-
graph.edges = graph.edges.filter((e) => e.sourceId !== id && e.targetId !== id);
|
|
384
|
-
} else {
|
|
385
|
-
const toDelete = collectDescendants(graph, id);
|
|
386
|
-
graph.nodes = graph.nodes.filter((n) => !toDelete.has(n.id));
|
|
387
|
-
graph.edges = graph.edges.filter((e) => !toDelete.has(e.sourceId) && !toDelete.has(e.targetId));
|
|
388
|
-
}
|
|
389
|
-
invalidateIndex(graph);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* **Mutable.** Delete an edge. Mutates `graph.edges` in place.
|
|
393
|
-
*
|
|
394
|
-
* @example
|
|
395
|
-
* ```ts
|
|
396
|
-
* const graph = createGraph({
|
|
397
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
398
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
399
|
-
* });
|
|
400
|
-
* deleteEdge(graph, 'e1');
|
|
401
|
-
* // graph.edges.length === 0
|
|
402
|
-
* ```
|
|
403
|
-
*/
|
|
404
|
-
function deleteEdge(graph, id) {
|
|
405
|
-
if (!hasEdge(graph, id)) throw new Error(`Edge "${id}" does not exist`);
|
|
406
|
-
graph.edges = graph.edges.filter((e) => e.id !== id);
|
|
407
|
-
invalidateIndex(graph);
|
|
408
|
-
}
|
|
409
|
-
/** Optional fields where `null` in an update unsets the field. */
|
|
410
|
-
const NODE_OPTIONAL_KEYS = [
|
|
411
|
-
"x",
|
|
412
|
-
"y",
|
|
413
|
-
"width",
|
|
414
|
-
"height",
|
|
415
|
-
"shape",
|
|
416
|
-
"color",
|
|
417
|
-
"style"
|
|
418
|
-
];
|
|
419
|
-
const EDGE_OPTIONAL_KEYS = [
|
|
420
|
-
"weight",
|
|
421
|
-
"mode",
|
|
422
|
-
"x",
|
|
423
|
-
"y",
|
|
424
|
-
"width",
|
|
425
|
-
"height",
|
|
426
|
-
"color",
|
|
427
|
-
"style"
|
|
428
|
-
];
|
|
429
|
-
/** Apply optional-field updates: `null` unsets, a value sets, `undefined` is ignored. */
|
|
430
|
-
function applyOptionalUpdates(target, update, keys) {
|
|
431
|
-
for (const key of keys) {
|
|
432
|
-
const value = update[key];
|
|
433
|
-
if (value === void 0) continue;
|
|
434
|
-
if (value === null) delete target[key];
|
|
435
|
-
else target[key] = value;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* **Mutable.** Update a node in place.
|
|
440
|
-
* Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
|
|
441
|
-
* `ports`) accept `null` to unset; `undefined` leaves them unchanged.
|
|
442
|
-
* @returns The updated node.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* ```ts
|
|
446
|
-
* const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
|
|
447
|
-
* const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
|
|
448
|
-
* // updated.label === 'new', updated.x === 100
|
|
449
|
-
* ```
|
|
450
|
-
*/
|
|
451
|
-
function updateNode(graph, id, update) {
|
|
452
|
-
const idx = getIndex(graph);
|
|
453
|
-
const arrayIdx = idx.nodeById.get(id);
|
|
454
|
-
if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
|
|
455
|
-
if (update.parentId !== void 0 && update.parentId !== null) {
|
|
456
|
-
if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
|
|
457
|
-
let ancestorId = update.parentId;
|
|
458
|
-
const seen = /* @__PURE__ */ new Set();
|
|
459
|
-
while (ancestorId !== null && !seen.has(ancestorId)) {
|
|
460
|
-
if (ancestorId === id) throw new Error(`Cannot set parentId of node "${id}" to "${update.parentId}": "${update.parentId}" is "${id}" or one of its descendants, which would create a hierarchy cycle. Reparent "${update.parentId}" elsewhere first.`);
|
|
461
|
-
seen.add(ancestorId);
|
|
462
|
-
const ai = idx.nodeById.get(ancestorId);
|
|
463
|
-
ancestorId = ai !== void 0 ? graph.nodes[ai].parentId ?? null : null;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
|
|
467
|
-
const node = graph.nodes[arrayIdx];
|
|
468
|
-
if (update.ports !== void 0) {
|
|
469
|
-
const newPortNames = new Set((update.ports ?? []).map((p) => p.name));
|
|
470
|
-
for (const eid of idx.outEdges.get(id) ?? []) {
|
|
471
|
-
const e = graph.edges[idx.edgeById.get(eid)];
|
|
472
|
-
if (e.sourcePort !== void 0 && !newPortNames.has(e.sourcePort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.sourcePort}" via sourcePort. Keep that port, or update/delete the edge first.`);
|
|
473
|
-
}
|
|
474
|
-
for (const eid of idx.inEdges.get(id) ?? []) {
|
|
475
|
-
const e = graph.edges[idx.edgeById.get(eid)];
|
|
476
|
-
if (e.targetPort !== void 0 && !newPortNames.has(e.targetPort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.targetPort}" via targetPort. Keep that port, or update/delete the edge first.`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
const oldParentId = node.parentId;
|
|
480
|
-
const updated = {
|
|
481
|
-
...node,
|
|
482
|
-
...update.parentId !== void 0 && { parentId: update.parentId ?? null },
|
|
483
|
-
...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
|
|
484
|
-
...update.label !== void 0 && { label: update.label },
|
|
485
|
-
...update.data !== void 0 && { data: update.data }
|
|
486
|
-
};
|
|
487
|
-
if (update.ports !== void 0) if (update.ports === null) delete updated.ports;
|
|
488
|
-
else updated.ports = update.ports.map(createGraphPort);
|
|
489
|
-
applyOptionalUpdates(updated, update, NODE_OPTIONAL_KEYS);
|
|
490
|
-
graph.nodes[arrayIdx] = updated;
|
|
491
|
-
if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
|
|
492
|
-
return updated;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* **Mutable.** Update an edge in place.
|
|
496
|
-
* Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
|
|
497
|
-
* `width`, `height`, `color`, `style`) accept `null` to unset; `undefined`
|
|
498
|
-
* leaves them unchanged.
|
|
499
|
-
* @returns The updated edge.
|
|
500
|
-
*
|
|
501
|
-
* @example
|
|
502
|
-
* ```ts
|
|
503
|
-
* const graph = createGraph({
|
|
504
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
505
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
|
|
506
|
-
* });
|
|
507
|
-
* const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
|
|
508
|
-
* // updated.label === 'new', updated.weight === 2
|
|
509
|
-
* ```
|
|
510
|
-
*/
|
|
511
|
-
function updateEdge(graph, id, update) {
|
|
512
|
-
const idx = getIndex(graph);
|
|
513
|
-
const arrayIdx = idx.edgeById.get(id);
|
|
514
|
-
if (arrayIdx === void 0) throw new Error(`Edge "${id}" does not exist`);
|
|
515
|
-
if (update.sourceId !== void 0 && !idx.nodeById.has(update.sourceId)) throw new Error(`Source node "${update.sourceId}" does not exist`);
|
|
516
|
-
if (update.targetId !== void 0 && !idx.nodeById.has(update.targetId)) throw new Error(`Target node "${update.targetId}" does not exist`);
|
|
517
|
-
const edge = graph.edges[arrayIdx];
|
|
518
|
-
const oldSourceId = edge.sourceId;
|
|
519
|
-
const oldTargetId = edge.targetId;
|
|
520
|
-
const effectiveSourceId = update.sourceId ?? edge.sourceId;
|
|
521
|
-
const effectiveTargetId = update.targetId ?? edge.targetId;
|
|
522
|
-
const effectiveSourcePort = update.sourcePort !== void 0 ? update.sourcePort ?? void 0 : edge.sourcePort;
|
|
523
|
-
const effectiveTargetPort = update.targetPort !== void 0 ? update.targetPort ?? void 0 : edge.targetPort;
|
|
524
|
-
if (effectiveSourcePort !== void 0) {
|
|
525
|
-
if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === effectiveSourcePort)) throw new Error(update.sourcePort !== void 0 ? `Port "${effectiveSourcePort}" does not exist on source node "${effectiveSourceId}"` : `Cannot update edge "${id}": its sourcePort "${effectiveSourcePort}" does not exist on the new source node "${effectiveSourceId}". Include sourcePort in the update (a port on "${effectiveSourceId}", or null to clear it).`);
|
|
526
|
-
}
|
|
527
|
-
if (effectiveTargetPort !== void 0) {
|
|
528
|
-
if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === effectiveTargetPort)) throw new Error(update.targetPort !== void 0 ? `Port "${effectiveTargetPort}" does not exist on target node "${effectiveTargetId}"` : `Cannot update edge "${id}": its targetPort "${effectiveTargetPort}" does not exist on the new target node "${effectiveTargetId}". Include targetPort in the update (a port on "${effectiveTargetId}", or null to clear it).`);
|
|
529
|
-
}
|
|
530
|
-
const updated = {
|
|
531
|
-
...edge,
|
|
532
|
-
...update.sourceId !== void 0 && { sourceId: update.sourceId },
|
|
533
|
-
...update.targetId !== void 0 && { targetId: update.targetId },
|
|
534
|
-
...update.label !== void 0 && { label: update.label },
|
|
535
|
-
...update.data !== void 0 && { data: update.data }
|
|
536
|
-
};
|
|
537
|
-
if (update.sourcePort !== void 0) if (update.sourcePort === null) delete updated.sourcePort;
|
|
538
|
-
else updated.sourcePort = update.sourcePort;
|
|
539
|
-
if (update.targetPort !== void 0) if (update.targetPort === null) delete updated.targetPort;
|
|
540
|
-
else updated.targetPort = update.targetPort;
|
|
541
|
-
applyOptionalUpdates(updated, update, EDGE_OPTIONAL_KEYS);
|
|
542
|
-
graph.edges[arrayIdx] = updated;
|
|
543
|
-
if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
|
|
544
|
-
if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
|
|
545
|
-
return updated;
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* **Mutable.** Add multiple nodes and edges to the graph.
|
|
549
|
-
* Nodes are added first, then edges (so edges can reference new nodes).
|
|
550
|
-
*
|
|
551
|
-
* @example
|
|
552
|
-
* ```ts
|
|
553
|
-
* const graph = createGraph();
|
|
554
|
-
* addEntities(graph, {
|
|
555
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
556
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
557
|
-
* });
|
|
558
|
-
* // graph.nodes.length === 2, graph.edges.length === 1
|
|
559
|
-
* ```
|
|
560
|
-
*/
|
|
561
|
-
function addEntities(graph, entities) {
|
|
562
|
-
for (const nodeConfig of entities.nodes ?? []) addNode(graph, nodeConfig);
|
|
563
|
-
for (const edgeConfig of entities.edges ?? []) addEdge(graph, edgeConfig);
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* **Mutable.** Delete entities by id(s). Automatically detects whether each id
|
|
567
|
-
* is a node or edge. Node deletions cascade to children and connected edges.
|
|
568
|
-
*
|
|
569
|
-
* @example
|
|
570
|
-
* ```ts
|
|
571
|
-
* const graph = createGraph({
|
|
572
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
573
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
574
|
-
* });
|
|
575
|
-
* deleteEntities(graph, ['a', 'e1']);
|
|
576
|
-
* // graph.nodes.length === 1, graph.edges.length === 0
|
|
577
|
-
* ```
|
|
578
|
-
*/
|
|
579
|
-
function deleteEntities(graph, ids, opts) {
|
|
580
|
-
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
581
|
-
for (const id of idArray) if (hasNode(graph, id)) deleteNode(graph, id, opts);
|
|
582
|
-
else if (hasEdge(graph, id)) deleteEdge(graph, id);
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* **Mutable.** Update multiple nodes and edges in place.
|
|
586
|
-
* Each entry must include an `id` to identify which entity to update.
|
|
587
|
-
*
|
|
588
|
-
* @example
|
|
589
|
-
* ```ts
|
|
590
|
-
* const graph = createGraph({
|
|
591
|
-
* nodes: [{ id: 'a', label: 'old' }],
|
|
592
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', label: 'old' }],
|
|
593
|
-
* });
|
|
594
|
-
* updateEntities(graph, {
|
|
595
|
-
* nodes: [{ id: 'a', label: 'new' }],
|
|
596
|
-
* edges: [{ id: 'e1', label: 'new' }],
|
|
597
|
-
* });
|
|
598
|
-
* ```
|
|
599
|
-
*/
|
|
600
|
-
function updateEntities(graph, updates) {
|
|
601
|
-
for (const nodeUpdate of updates.nodes ?? []) {
|
|
602
|
-
const { id, ...patch } = nodeUpdate;
|
|
603
|
-
updateNode(graph, id, patch);
|
|
604
|
-
}
|
|
605
|
-
for (const edgeUpdate of updates.edges ?? []) {
|
|
606
|
-
const { id, ...patch } = edgeUpdate;
|
|
607
|
-
updateEdge(graph, id, patch);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* OOP wrapper around a plain `Graph` object.
|
|
612
|
-
* Delegates to the standalone mutable functions.
|
|
613
|
-
*
|
|
614
|
-
* @example
|
|
615
|
-
* ```ts
|
|
616
|
-
* const instance = new GraphInstance({
|
|
617
|
-
* nodes: [{ id: 'a' }, { id: 'b' }],
|
|
618
|
-
* edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
|
|
619
|
-
* });
|
|
620
|
-
* instance.addNode({ id: 'c' });
|
|
621
|
-
* instance.hasNode('c'); // true
|
|
622
|
-
* instance.toJSON(); // plain Graph object
|
|
623
|
-
* ```
|
|
624
|
-
*/
|
|
625
|
-
var GraphInstance = class GraphInstance {
|
|
626
|
-
graph;
|
|
627
|
-
constructor(config) {
|
|
628
|
-
this.graph = createGraph(config);
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* Wrap an existing plain graph object.
|
|
632
|
-
*
|
|
633
|
-
* @example
|
|
634
|
-
* ```ts
|
|
635
|
-
* const graph = createGraph({ nodes: [{ id: 'a' }] });
|
|
636
|
-
* const instance = GraphInstance.from(graph);
|
|
637
|
-
* instance.hasNode('a'); // true
|
|
638
|
-
* ```
|
|
639
|
-
*/
|
|
640
|
-
static from(graph) {
|
|
641
|
-
const instance = Object.create(GraphInstance.prototype);
|
|
642
|
-
instance.graph = graph;
|
|
643
|
-
return instance;
|
|
644
|
-
}
|
|
645
|
-
get id() {
|
|
646
|
-
return this.graph.id;
|
|
647
|
-
}
|
|
648
|
-
/** Default directedness for all edges. */
|
|
649
|
-
get mode() {
|
|
650
|
-
return this.graph.mode;
|
|
651
|
-
}
|
|
652
|
-
get nodes() {
|
|
653
|
-
return this.graph.nodes;
|
|
654
|
-
}
|
|
655
|
-
get edges() {
|
|
656
|
-
return this.graph.edges;
|
|
657
|
-
}
|
|
658
|
-
get data() {
|
|
659
|
-
return this.graph.data;
|
|
660
|
-
}
|
|
661
|
-
getNode(id) {
|
|
662
|
-
return getNode(this.graph, id);
|
|
663
|
-
}
|
|
664
|
-
getEdge(id) {
|
|
665
|
-
return getEdge(this.graph, id);
|
|
666
|
-
}
|
|
667
|
-
hasNode(id) {
|
|
668
|
-
return hasNode(this.graph, id);
|
|
669
|
-
}
|
|
670
|
-
hasEdge(id) {
|
|
671
|
-
return hasEdge(this.graph, id);
|
|
672
|
-
}
|
|
673
|
-
addNode(config) {
|
|
674
|
-
return addNode(this.graph, config);
|
|
675
|
-
}
|
|
676
|
-
addEdge(config) {
|
|
677
|
-
return addEdge(this.graph, config);
|
|
678
|
-
}
|
|
679
|
-
deleteNode(id, opts) {
|
|
680
|
-
return deleteNode(this.graph, id, opts);
|
|
681
|
-
}
|
|
682
|
-
deleteEdge(id) {
|
|
683
|
-
return deleteEdge(this.graph, id);
|
|
684
|
-
}
|
|
685
|
-
updateNode(id, update) {
|
|
686
|
-
return updateNode(this.graph, id, update);
|
|
687
|
-
}
|
|
688
|
-
updateEdge(id, update) {
|
|
689
|
-
return updateEdge(this.graph, id, update);
|
|
690
|
-
}
|
|
691
|
-
addEntities(entities) {
|
|
692
|
-
return addEntities(this.graph, entities);
|
|
693
|
-
}
|
|
694
|
-
deleteEntities(ids, opts) {
|
|
695
|
-
return deleteEntities(this.graph, ids, opts);
|
|
696
|
-
}
|
|
697
|
-
updateEntities(updates) {
|
|
698
|
-
return updateEntities(this.graph, updates);
|
|
699
|
-
}
|
|
700
|
-
toJSON() {
|
|
701
|
-
return this.graph;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
function collectDescendants(graph, id) {
|
|
705
|
-
const idx = getIndex(graph);
|
|
706
|
-
const toDelete = /* @__PURE__ */ new Set();
|
|
707
|
-
const walk = (nodeId) => {
|
|
708
|
-
toDelete.add(nodeId);
|
|
709
|
-
const childIds = idx.childNodes.get(nodeId) ?? [];
|
|
710
|
-
for (const childId of childIds) if (!toDelete.has(childId)) walk(childId);
|
|
711
|
-
};
|
|
712
|
-
walk(id);
|
|
713
|
-
return toDelete;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
//#endregion
|
|
717
5
|
//#region src/algorithms/shared.ts
|
|
718
6
|
var MinPriorityQueue = class {
|
|
719
7
|
items = [];
|
|
@@ -764,6 +52,19 @@ var MinPriorityQueue = class {
|
|
|
764
52
|
}
|
|
765
53
|
};
|
|
766
54
|
/**
|
|
55
|
+
* Seeded PRNG (mulberry32). Same generator as src/walks.ts — kept here so
|
|
56
|
+
* algorithm modules and generators can share it without importing walks.
|
|
57
|
+
*/
|
|
58
|
+
function mulberry32(seed) {
|
|
59
|
+
let s = seed | 0;
|
|
60
|
+
return () => {
|
|
61
|
+
s = s + 1831565813 | 0;
|
|
62
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
63
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
64
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
767
68
|
* Classify a graph by the *effective* mode of its edges (per-edge `mode`
|
|
768
69
|
* overrides included): all-directed, all-non-directed, or genuinely mixed.
|
|
769
70
|
* Edge-less graphs fall back to `graph.mode`.
|
|
@@ -954,19 +255,84 @@ function buildCSR(graph) {
|
|
|
954
255
|
|
|
955
256
|
//#endregion
|
|
956
257
|
//#region src/algorithms/paths.ts
|
|
258
|
+
/**
|
|
259
|
+
* Flat binary min-heap of `(distance, node position)` entries in parallel
|
|
260
|
+
* typed arrays. The Dijkstra/A* hot loops push one entry per relaxation, so
|
|
261
|
+
* avoiding a `{ pos, dist }` wrapper object per push (allocation + property
|
|
262
|
+
* loads in the sift comparisons) is a measurable win on 10k+ node graphs.
|
|
263
|
+
* Sifts move "holes" instead of swapping, halving array writes.
|
|
264
|
+
*/
|
|
265
|
+
var TypedMinHeap = class {
|
|
266
|
+
keys;
|
|
267
|
+
vals;
|
|
268
|
+
size = 0;
|
|
269
|
+
constructor(capacity) {
|
|
270
|
+
const cap = Math.max(capacity, 16);
|
|
271
|
+
this.keys = new Float64Array(cap);
|
|
272
|
+
this.vals = new Int32Array(cap);
|
|
273
|
+
}
|
|
274
|
+
push(key, val) {
|
|
275
|
+
if (this.size === this.keys.length) {
|
|
276
|
+
const keys$1 = new Float64Array(this.keys.length * 2);
|
|
277
|
+
const vals$1 = new Int32Array(this.vals.length * 2);
|
|
278
|
+
keys$1.set(this.keys);
|
|
279
|
+
vals$1.set(this.vals);
|
|
280
|
+
this.keys = keys$1;
|
|
281
|
+
this.vals = vals$1;
|
|
282
|
+
}
|
|
283
|
+
const { keys, vals } = this;
|
|
284
|
+
let hole = this.size++;
|
|
285
|
+
while (hole > 0) {
|
|
286
|
+
const parent = hole - 1 >> 1;
|
|
287
|
+
if (keys[parent] <= key) break;
|
|
288
|
+
keys[hole] = keys[parent];
|
|
289
|
+
vals[hole] = vals[parent];
|
|
290
|
+
hole = parent;
|
|
291
|
+
}
|
|
292
|
+
keys[hole] = key;
|
|
293
|
+
vals[hole] = val;
|
|
294
|
+
}
|
|
295
|
+
/** Key of the minimum entry; garbage when empty (check `size` first). */
|
|
296
|
+
peekKey() {
|
|
297
|
+
return this.keys[0];
|
|
298
|
+
}
|
|
299
|
+
/** Value of the minimum entry; garbage when empty (check `size` first). */
|
|
300
|
+
peekVal() {
|
|
301
|
+
return this.vals[0];
|
|
302
|
+
}
|
|
303
|
+
/** Remove the minimum entry (no-op shape: read via peek* first). */
|
|
304
|
+
pop() {
|
|
305
|
+
const { keys, vals } = this;
|
|
306
|
+
const last = --this.size;
|
|
307
|
+
if (last === 0) return;
|
|
308
|
+
const key = keys[last];
|
|
309
|
+
const val = vals[last];
|
|
310
|
+
let hole = 0;
|
|
311
|
+
for (;;) {
|
|
312
|
+
let child = hole * 2 + 1;
|
|
313
|
+
if (child >= last) break;
|
|
314
|
+
const right = child + 1;
|
|
315
|
+
if (right < last && keys[right] < keys[child]) child = right;
|
|
316
|
+
if (keys[child] >= key) break;
|
|
317
|
+
keys[hole] = keys[child];
|
|
318
|
+
vals[hole] = vals[child];
|
|
319
|
+
hole = child;
|
|
320
|
+
}
|
|
321
|
+
keys[hole] = key;
|
|
322
|
+
vals[hole] = val;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
957
325
|
function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
|
|
958
|
-
if (algorithm === "bellman-ford") return
|
|
959
|
-
const dist = /* @__PURE__ */ new Map();
|
|
960
|
-
const prev = /* @__PURE__ */ new Map();
|
|
961
|
-
dist.set(sourceId, 0);
|
|
962
|
-
prev.set(sourceId, []);
|
|
326
|
+
if (algorithm === "bellman-ford") return bellmanFordTyped(graph, sourceId, getWeight);
|
|
963
327
|
const csr = getCSR(graph);
|
|
328
|
+
const n = csr.ids.length;
|
|
964
329
|
const source = csr.indexOf.get(sourceId);
|
|
965
330
|
if (source === void 0) return {
|
|
966
|
-
|
|
967
|
-
|
|
331
|
+
source: -1,
|
|
332
|
+
distArr: new Float64Array(0),
|
|
333
|
+
prevArr: [],
|
|
334
|
+
stopDistance: Infinity
|
|
968
335
|
};
|
|
969
|
-
const n = csr.ids.length;
|
|
970
336
|
const distArr = new Float64Array(n).fill(Infinity);
|
|
971
337
|
const prevArr = new Array(n);
|
|
972
338
|
distArr[source] = 0;
|
|
@@ -996,13 +362,12 @@ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtI
|
|
|
996
362
|
} else {
|
|
997
363
|
const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
|
|
998
364
|
const visited = new Uint8Array(n);
|
|
999
|
-
const pq = new
|
|
1000
|
-
pq.push(
|
|
1001
|
-
pos: source,
|
|
1002
|
-
dist: 0
|
|
1003
|
-
});
|
|
365
|
+
const pq = new TypedMinHeap(n);
|
|
366
|
+
pq.push(0, source);
|
|
1004
367
|
while (pq.size > 0) {
|
|
1005
|
-
const
|
|
368
|
+
const distance = pq.peekKey();
|
|
369
|
+
const u = pq.peekVal();
|
|
370
|
+
pq.pop();
|
|
1006
371
|
if (visited[u] || distance !== distArr[u]) continue;
|
|
1007
372
|
if (distance > stopDistance) break;
|
|
1008
373
|
if (u === stopAt) stopDistance = distance;
|
|
@@ -1016,28 +381,48 @@ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtI
|
|
|
1016
381
|
if (nextDistance < distArr[v]) {
|
|
1017
382
|
distArr[v] = nextDistance;
|
|
1018
383
|
prevArr[v] = [u, csr.outEdgeIndex[a]];
|
|
1019
|
-
pq.push(
|
|
1020
|
-
pos: v,
|
|
1021
|
-
dist: nextDistance
|
|
1022
|
-
});
|
|
384
|
+
pq.push(nextDistance, v);
|
|
1023
385
|
} else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
|
|
1024
386
|
}
|
|
1025
387
|
}
|
|
1026
388
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
389
|
+
return {
|
|
390
|
+
source,
|
|
391
|
+
distArr,
|
|
392
|
+
prevArr,
|
|
393
|
+
stopDistance
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Bellman-Ford adapted to the typed-array result shape. The O(VE) relaxation
|
|
398
|
+
* dominates, so the id→position conversion here is noise — and it keeps a
|
|
399
|
+
* single reconstruction path for both algorithms.
|
|
400
|
+
*/
|
|
401
|
+
function bellmanFordTyped(graph, sourceId, getWeight) {
|
|
402
|
+
const { dist, prev } = bellmanFord(graph, sourceId, getWeight);
|
|
403
|
+
const csr = getCSR(graph);
|
|
404
|
+
const idx = getIndex(graph);
|
|
405
|
+
const n = csr.ids.length;
|
|
406
|
+
const distArr = new Float64Array(n).fill(Infinity);
|
|
407
|
+
const prevArr = new Array(n);
|
|
408
|
+
for (const [id, distance] of dist) {
|
|
409
|
+
const pos = csr.indexOf.get(id);
|
|
410
|
+
if (pos === void 0) continue;
|
|
411
|
+
distArr[pos] = distance;
|
|
412
|
+
const pairs = [];
|
|
413
|
+
for (const { from, edge } of prev.get(id) ?? []) {
|
|
414
|
+
const fromPos = csr.indexOf.get(from);
|
|
415
|
+
const edgeIndex = idx.edgeById.get(edge.id);
|
|
416
|
+
if (fromPos === void 0 || edgeIndex === void 0) continue;
|
|
417
|
+
pairs.push(fromPos, edgeIndex);
|
|
418
|
+
}
|
|
419
|
+
prevArr[pos] = pairs;
|
|
1037
420
|
}
|
|
1038
421
|
return {
|
|
1039
|
-
|
|
1040
|
-
|
|
422
|
+
source: csr.indexOf.get(sourceId) ?? -1,
|
|
423
|
+
distArr,
|
|
424
|
+
prevArr,
|
|
425
|
+
stopDistance: Infinity
|
|
1041
426
|
};
|
|
1042
427
|
}
|
|
1043
428
|
function bellmanFord(graph, sourceId, getWeight) {
|
|
@@ -1100,22 +485,23 @@ function bellmanFord(graph, sourceId, getWeight) {
|
|
|
1100
485
|
prev
|
|
1101
486
|
};
|
|
1102
487
|
}
|
|
1103
|
-
function*
|
|
1104
|
-
if (
|
|
488
|
+
function* reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, targetPos, onPath = /* @__PURE__ */ new Set()) {
|
|
489
|
+
if (targetPos === sourcePos) {
|
|
1105
490
|
yield {
|
|
1106
491
|
source: sourceNode,
|
|
1107
492
|
steps: []
|
|
1108
493
|
};
|
|
1109
494
|
return;
|
|
1110
495
|
}
|
|
1111
|
-
const
|
|
1112
|
-
if (!
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (onPath.has(
|
|
1118
|
-
|
|
496
|
+
const pairs = prevArr[targetPos];
|
|
497
|
+
if (!pairs || pairs.length === 0) return;
|
|
498
|
+
const targetNode = graph.nodes[targetPos];
|
|
499
|
+
onPath.add(targetPos);
|
|
500
|
+
for (let k = 0; k < pairs.length; k += 2) {
|
|
501
|
+
const fromPos = pairs[k];
|
|
502
|
+
if (onPath.has(fromPos)) continue;
|
|
503
|
+
const edge = graph.edges[pairs[k + 1]];
|
|
504
|
+
for (const prefix of reconstructPathsAt(graph, prevArr, sourceNode, sourcePos, fromPos, onPath)) yield {
|
|
1119
505
|
source: sourceNode,
|
|
1120
506
|
steps: [...prefix.steps, {
|
|
1121
507
|
edge,
|
|
@@ -1123,16 +509,30 @@ function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PUR
|
|
|
1123
509
|
}]
|
|
1124
510
|
};
|
|
1125
511
|
}
|
|
1126
|
-
onPath.delete(
|
|
512
|
+
onPath.delete(targetPos);
|
|
1127
513
|
}
|
|
1128
514
|
function* genShortestPaths(graph, opts) {
|
|
1129
|
-
const idx = getIndex(graph);
|
|
1130
515
|
const sourceId = resolveFrom(graph, opts);
|
|
1131
|
-
const {
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
516
|
+
const { source, distArr, prevArr, stopDistance } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
|
|
517
|
+
const sourceNode = source !== -1 ? graph.nodes[source] : graph.nodes.find((node) => node.id === sourceId);
|
|
518
|
+
if (source === -1) {
|
|
519
|
+
if (opts?.to === sourceId) yield {
|
|
520
|
+
source: sourceNode,
|
|
521
|
+
steps: []
|
|
522
|
+
};
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const csr = getCSR(graph);
|
|
526
|
+
if (opts?.to) {
|
|
527
|
+
const target = csr.indexOf.get(opts.to);
|
|
528
|
+
if (target === void 0 || distArr[target] === Infinity || distArr[target] > stopDistance) return;
|
|
529
|
+
yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
for (let target = 0; target < distArr.length; target++) {
|
|
533
|
+
if (target === source || distArr[target] === Infinity || distArr[target] > stopDistance) continue;
|
|
534
|
+
yield* reconstructPathsAt(graph, prevArr, sourceNode, source, target);
|
|
535
|
+
}
|
|
1136
536
|
}
|
|
1137
537
|
function getShortestPaths(graph, opts) {
|
|
1138
538
|
return [...genShortestPaths(graph, opts)];
|
|
@@ -1193,35 +593,30 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
|
1193
593
|
const predBEdge = new Int32Array(n).fill(-1);
|
|
1194
594
|
const settledF = new Uint8Array(n);
|
|
1195
595
|
const settledB = new Uint8Array(n);
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
const pqB = new MinPriorityQueue(compare);
|
|
596
|
+
const pqF = new TypedMinHeap(n);
|
|
597
|
+
const pqB = new TypedMinHeap(n);
|
|
1199
598
|
distF[source] = 0;
|
|
1200
599
|
distB[target] = 0;
|
|
1201
|
-
pqF.push(
|
|
1202
|
-
|
|
1203
|
-
dist: 0
|
|
1204
|
-
});
|
|
1205
|
-
pqB.push({
|
|
1206
|
-
pos: target,
|
|
1207
|
-
dist: 0
|
|
1208
|
-
});
|
|
600
|
+
pqF.push(0, source);
|
|
601
|
+
pqB.push(0, target);
|
|
1209
602
|
let mu = Infinity;
|
|
1210
603
|
let meet = -1;
|
|
1211
604
|
/** Discard stale/settled heap entries; return the next valid key. */
|
|
1212
605
|
const validTop = (pq, dist, settled) => {
|
|
1213
|
-
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
if (settled[
|
|
606
|
+
while (pq.size > 0) {
|
|
607
|
+
const key = pq.peekKey();
|
|
608
|
+
const pos = pq.peekVal();
|
|
609
|
+
if (settled[pos] || key !== dist[pos]) {
|
|
1217
610
|
pq.pop();
|
|
1218
611
|
continue;
|
|
1219
612
|
}
|
|
1220
|
-
return
|
|
613
|
+
return key;
|
|
1221
614
|
}
|
|
1222
615
|
};
|
|
1223
616
|
const scanForward = () => {
|
|
1224
|
-
const
|
|
617
|
+
const d = pqF.peekKey();
|
|
618
|
+
const u = pqF.peekVal();
|
|
619
|
+
pqF.pop();
|
|
1225
620
|
settledF[u] = 1;
|
|
1226
621
|
for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
|
|
1227
622
|
const edge = graph.edges[csr.outEdgeIndex[a]];
|
|
@@ -1232,10 +627,7 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
|
1232
627
|
distF[v] = next;
|
|
1233
628
|
predF[v] = u;
|
|
1234
629
|
predFEdge[v] = csr.outEdgeIndex[a];
|
|
1235
|
-
pqF.push(
|
|
1236
|
-
pos: v,
|
|
1237
|
-
dist: next
|
|
1238
|
-
});
|
|
630
|
+
pqF.push(next, v);
|
|
1239
631
|
}
|
|
1240
632
|
if (distB[v] !== Infinity && next + distB[v] < mu) {
|
|
1241
633
|
mu = next + distB[v];
|
|
@@ -1244,7 +636,9 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
|
1244
636
|
}
|
|
1245
637
|
};
|
|
1246
638
|
const scanBackward = () => {
|
|
1247
|
-
const
|
|
639
|
+
const d = pqB.peekKey();
|
|
640
|
+
const u = pqB.peekVal();
|
|
641
|
+
pqB.pop();
|
|
1248
642
|
settledB[u] = 1;
|
|
1249
643
|
for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
|
|
1250
644
|
const edge = graph.edges[csr.inEdgeIndex[a]];
|
|
@@ -1255,10 +649,7 @@ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
|
|
|
1255
649
|
distB[v] = next;
|
|
1256
650
|
predB[v] = u;
|
|
1257
651
|
predBEdge[v] = csr.inEdgeIndex[a];
|
|
1258
|
-
pqB.push(
|
|
1259
|
-
pos: v,
|
|
1260
|
-
dist: next
|
|
1261
|
-
});
|
|
652
|
+
pqB.push(next, v);
|
|
1262
653
|
}
|
|
1263
654
|
if (distF[v] !== Infinity && next + distF[v] < mu) {
|
|
1264
655
|
mu = next + distF[v];
|
|
@@ -1642,15 +1033,13 @@ function getAStarPath(graph, opts) {
|
|
|
1642
1033
|
const cameFromPos = new Int32Array(n).fill(-1);
|
|
1643
1034
|
const cameFromEdge = new Int32Array(n).fill(-1);
|
|
1644
1035
|
const closed = new Uint8Array(n);
|
|
1645
|
-
const openSet = new
|
|
1036
|
+
const openSet = new TypedMinHeap(n);
|
|
1646
1037
|
assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
|
|
1647
1038
|
gScore[source] = 0;
|
|
1648
|
-
openSet.push(
|
|
1649
|
-
pos: source,
|
|
1650
|
-
f: heuristic(sourceId)
|
|
1651
|
-
});
|
|
1039
|
+
openSet.push(heuristic(sourceId), source);
|
|
1652
1040
|
while (openSet.size > 0) {
|
|
1653
|
-
const
|
|
1041
|
+
const current = openSet.peekVal();
|
|
1042
|
+
openSet.pop();
|
|
1654
1043
|
if (closed[current]) continue;
|
|
1655
1044
|
if (current === target) {
|
|
1656
1045
|
const steps = [];
|
|
@@ -1678,15 +1067,12 @@ function getAStarPath(graph, opts) {
|
|
|
1678
1067
|
cameFromPos[neighbor] = current;
|
|
1679
1068
|
cameFromEdge[neighbor] = csr.outEdgeIndex[a];
|
|
1680
1069
|
gScore[neighbor] = tentativeScore;
|
|
1681
|
-
openSet.push(
|
|
1682
|
-
pos: neighbor,
|
|
1683
|
-
f: tentativeScore + heuristic(csr.ids[neighbor])
|
|
1684
|
-
});
|
|
1070
|
+
openSet.push(tentativeScore + heuristic(csr.ids[neighbor]), neighbor);
|
|
1685
1071
|
}
|
|
1686
1072
|
}
|
|
1687
1073
|
}
|
|
1688
1074
|
}
|
|
1689
|
-
function
|
|
1075
|
+
function getJoinedPath(headPath, tailPath) {
|
|
1690
1076
|
const headEnd = headPath.steps.length > 0 ? headPath.steps[headPath.steps.length - 1].node : headPath.source;
|
|
1691
1077
|
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}"`);
|
|
1692
1078
|
return {
|
|
@@ -1694,57 +1080,11 @@ function joinPaths(headPath, tailPath) {
|
|
|
1694
1080
|
steps: [...headPath.steps, ...tailPath.steps]
|
|
1695
1081
|
};
|
|
1696
1082
|
}
|
|
1697
|
-
|
|
1698
|
-
//#endregion
|
|
1699
|
-
//#region src/config.ts
|
|
1700
|
-
/**
|
|
1701
|
-
* Convert a resolved {@link GraphNode} back into a {@link NodeConfig}.
|
|
1702
|
-
*
|
|
1703
|
-
* Faithful and complete: round-tripping through `createGraphNode` yields a
|
|
1704
|
-
* deep-equal node. Optional fields are only included when present; ports are
|
|
1705
|
-
* deep-copied so the config does not share port objects with the source node.
|
|
1706
|
-
*/
|
|
1707
|
-
function toNodeConfig(node) {
|
|
1708
|
-
const config = { id: node.id };
|
|
1709
|
-
if (node.parentId != null) config.parentId = node.parentId;
|
|
1710
|
-
if (node.initialNodeId != null) config.initialNodeId = node.initialNodeId;
|
|
1711
|
-
if (node.label != null) config.label = node.label;
|
|
1712
|
-
if (node.data != null) config.data = node.data;
|
|
1713
|
-
if (node.ports !== void 0) config.ports = node.ports.map((p) => ({ ...p }));
|
|
1714
|
-
if (node.x !== void 0) config.x = node.x;
|
|
1715
|
-
if (node.y !== void 0) config.y = node.y;
|
|
1716
|
-
if (node.width !== void 0) config.width = node.width;
|
|
1717
|
-
if (node.height !== void 0) config.height = node.height;
|
|
1718
|
-
if (node.shape !== void 0) config.shape = node.shape;
|
|
1719
|
-
if (node.color !== void 0) config.color = node.color;
|
|
1720
|
-
if (node.style !== void 0) config.style = node.style;
|
|
1721
|
-
return config;
|
|
1722
|
-
}
|
|
1723
1083
|
/**
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
* Faithful and complete: round-tripping through `createGraphEdge` yields a
|
|
1727
|
-
* deep-equal edge. Optional fields are only included when present.
|
|
1084
|
+
* @deprecated Use {@link getJoinedPath}.
|
|
1728
1085
|
*/
|
|
1729
|
-
function
|
|
1730
|
-
|
|
1731
|
-
id: edge.id,
|
|
1732
|
-
sourceId: edge.sourceId,
|
|
1733
|
-
targetId: edge.targetId
|
|
1734
|
-
};
|
|
1735
|
-
if (edge.label != null) config.label = edge.label;
|
|
1736
|
-
if (edge.data != null) config.data = edge.data;
|
|
1737
|
-
if (edge.weight !== void 0) config.weight = edge.weight;
|
|
1738
|
-
if (edge.mode !== void 0) config.mode = edge.mode;
|
|
1739
|
-
if (edge.sourcePort !== void 0) config.sourcePort = edge.sourcePort;
|
|
1740
|
-
if (edge.targetPort !== void 0) config.targetPort = edge.targetPort;
|
|
1741
|
-
if (edge.x !== void 0) config.x = edge.x;
|
|
1742
|
-
if (edge.y !== void 0) config.y = edge.y;
|
|
1743
|
-
if (edge.width !== void 0) config.width = edge.width;
|
|
1744
|
-
if (edge.height !== void 0) config.height = edge.height;
|
|
1745
|
-
if (edge.color !== void 0) config.color = edge.color;
|
|
1746
|
-
if (edge.style !== void 0) config.style = edge.style;
|
|
1747
|
-
return config;
|
|
1086
|
+
function joinPaths(headPath, tailPath) {
|
|
1087
|
+
return getJoinedPath(headPath, tailPath);
|
|
1748
1088
|
}
|
|
1749
1089
|
|
|
1750
1090
|
//#endregion
|
|
@@ -1759,7 +1099,7 @@ function toEdgeConfig(edge) {
|
|
|
1759
1099
|
*
|
|
1760
1100
|
* @example
|
|
1761
1101
|
* ```ts
|
|
1762
|
-
* import { createGraph,
|
|
1102
|
+
* import { createGraph, getFlattenedGraph } from '@statelyai/graph';
|
|
1763
1103
|
*
|
|
1764
1104
|
* const graph = createGraph({
|
|
1765
1105
|
* nodes: [
|
|
@@ -1771,12 +1111,12 @@ function toEdgeConfig(edge) {
|
|
|
1771
1111
|
* edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
|
|
1772
1112
|
* });
|
|
1773
1113
|
*
|
|
1774
|
-
* const flat =
|
|
1114
|
+
* const flat = getFlattenedGraph(graph);
|
|
1775
1115
|
* // flat.nodes → [child1, child2, other] (leaf nodes only)
|
|
1776
1116
|
* // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
|
|
1777
1117
|
* ```
|
|
1778
1118
|
*/
|
|
1779
|
-
function
|
|
1119
|
+
function getFlattenedGraph(graph) {
|
|
1780
1120
|
const idx = getIndex(graph);
|
|
1781
1121
|
const leaves = /* @__PURE__ */ new Set();
|
|
1782
1122
|
for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
|
|
@@ -1843,6 +1183,12 @@ function flatten(graph) {
|
|
|
1843
1183
|
});
|
|
1844
1184
|
}
|
|
1845
1185
|
/**
|
|
1186
|
+
* @deprecated Use {@link getFlattenedGraph}.
|
|
1187
|
+
*/
|
|
1188
|
+
function flatten(graph) {
|
|
1189
|
+
return getFlattenedGraph(graph);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1846
1192
|
* Convert a node to a config, stripping parentId/initialNodeId references
|
|
1847
1193
|
* to nodes outside the given set.
|
|
1848
1194
|
*/
|
|
@@ -1893,7 +1239,7 @@ function getSubgraph(graph, nodeIds) {
|
|
|
1893
1239
|
*
|
|
1894
1240
|
* @example
|
|
1895
1241
|
* ```ts
|
|
1896
|
-
* import { createGraph,
|
|
1242
|
+
* import { createGraph, getReversedGraph } from '@statelyai/graph';
|
|
1897
1243
|
*
|
|
1898
1244
|
* const graph = createGraph({
|
|
1899
1245
|
* nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
@@ -1903,14 +1249,14 @@ function getSubgraph(graph, nodeIds) {
|
|
|
1903
1249
|
* ],
|
|
1904
1250
|
* });
|
|
1905
1251
|
*
|
|
1906
|
-
* const rev =
|
|
1252
|
+
* const rev = getReversedGraph(graph);
|
|
1907
1253
|
* // rev edges: b→a, c→b
|
|
1908
1254
|
*
|
|
1909
|
-
* const filtered =
|
|
1255
|
+
* const filtered = getReversedGraph(graph, (e) => e.id !== 'bc');
|
|
1910
1256
|
* // filtered edges: b→a (only ab reversed, bc excluded)
|
|
1911
1257
|
* ```
|
|
1912
1258
|
*/
|
|
1913
|
-
function
|
|
1259
|
+
function getReversedGraph(graph, filterEdge) {
|
|
1914
1260
|
const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
|
|
1915
1261
|
return createGraph({
|
|
1916
1262
|
id: graph.id,
|
|
@@ -1930,10 +1276,16 @@ function reverseGraph(graph, filterEdge) {
|
|
|
1930
1276
|
data: graph.data
|
|
1931
1277
|
});
|
|
1932
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* @deprecated Use {@link getReversedGraph}.
|
|
1281
|
+
*/
|
|
1282
|
+
function reverseGraph(graph, filterEdge) {
|
|
1283
|
+
return getReversedGraph(graph, filterEdge);
|
|
1284
|
+
}
|
|
1933
1285
|
|
|
1934
1286
|
//#endregion
|
|
1935
1287
|
//#region src/algorithms/traversal.ts
|
|
1936
|
-
function*
|
|
1288
|
+
function* genBFS(graph, startId) {
|
|
1937
1289
|
const csr = getCSR(graph);
|
|
1938
1290
|
const start = csr.indexOf.get(startId);
|
|
1939
1291
|
if (start === void 0) return;
|
|
@@ -1956,7 +1308,13 @@ function* bfs(graph, startId) {
|
|
|
1956
1308
|
}
|
|
1957
1309
|
}
|
|
1958
1310
|
}
|
|
1959
|
-
|
|
1311
|
+
/**
|
|
1312
|
+
* @deprecated Use {@link genBFS}.
|
|
1313
|
+
*/
|
|
1314
|
+
function* bfs(graph, startId) {
|
|
1315
|
+
yield* genBFS(graph, startId);
|
|
1316
|
+
}
|
|
1317
|
+
function* genDFS(graph, startId) {
|
|
1960
1318
|
const csr = getCSR(graph);
|
|
1961
1319
|
const start = csr.indexOf.get(startId);
|
|
1962
1320
|
if (start === void 0) return;
|
|
@@ -1974,6 +1332,12 @@ function* dfs(graph, startId) {
|
|
|
1974
1332
|
}
|
|
1975
1333
|
}
|
|
1976
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* @deprecated Use {@link genDFS}.
|
|
1337
|
+
*/
|
|
1338
|
+
function* dfs(graph, startId) {
|
|
1339
|
+
yield* genDFS(graph, startId);
|
|
1340
|
+
}
|
|
1977
1341
|
function isAcyclic(graph) {
|
|
1978
1342
|
const kind = getEffectiveModeKind(graph);
|
|
1979
1343
|
if (kind === "mixed") return isAcyclicMixed(graph);
|
|
@@ -2636,31 +2000,366 @@ function getHITS(graph, options) {
|
|
|
2636
2000
|
/**
|
|
2637
2001
|
* Returns eigenvector centrality scores for all nodes.
|
|
2638
2002
|
*
|
|
2639
|
-
*
|
|
2640
|
-
*
|
|
2003
|
+
* Power iteration with the `A + I` shift (same scheme as graphology and
|
|
2004
|
+
* networkx, so bipartite structures converge instead of oscillating).
|
|
2005
|
+
* Scores flow along edge direction: a node's score is fed by its incoming
|
|
2006
|
+
* neighbors; undirected edges feed both endpoints. The result vector is
|
|
2007
|
+
* Euclidean (L2) normalized.
|
|
2008
|
+
*
|
|
2009
|
+
* Throws when the iteration has not converged (L1 error < `n × tolerance`)
|
|
2010
|
+
* within `maxIterations`.
|
|
2641
2011
|
*/
|
|
2642
2012
|
function getEigenvectorCentrality(graph, options) {
|
|
2643
2013
|
if (getNodeIds(graph).length === 0) return {};
|
|
2644
2014
|
const maxIterations = options?.maxIterations ?? 100;
|
|
2645
2015
|
const tolerance = options?.tolerance ?? 1e-6;
|
|
2016
|
+
const getWeight = options?.getWeight;
|
|
2646
2017
|
const csr = getCSR(graph);
|
|
2647
2018
|
const n = csr.ids.length;
|
|
2648
|
-
let current = new Float64Array(n).fill(1);
|
|
2649
|
-
|
|
2019
|
+
let current = new Float64Array(n).fill(1 / n);
|
|
2020
|
+
let converged = false;
|
|
2650
2021
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2651
|
-
const next =
|
|
2652
|
-
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++)
|
|
2022
|
+
const next = Float64Array.from(current);
|
|
2023
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
2024
|
+
const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
|
|
2025
|
+
next[w] += current[csr.inOrigins[a]] * weight;
|
|
2026
|
+
}
|
|
2653
2027
|
normalizeTypedVector(next);
|
|
2654
|
-
let
|
|
2655
|
-
for (let i = 0; i < n; i++)
|
|
2028
|
+
let error = 0;
|
|
2029
|
+
for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
|
|
2656
2030
|
current = next;
|
|
2657
|
-
if (
|
|
2031
|
+
if (error < n * tolerance) {
|
|
2032
|
+
converged = true;
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (!converged) throw new Error(`getEigenvectorCentrality: power iteration failed to converge within ${maxIterations} iterations (tolerance ${tolerance}) — increase options.maxIterations or loosen options.tolerance`);
|
|
2037
|
+
const scores = createEmptyScoreMap(graph);
|
|
2038
|
+
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
2039
|
+
return scores;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Returns Katz centrality scores for all nodes.
|
|
2043
|
+
*
|
|
2044
|
+
* Iterates `x' = alpha · Aᵀx + beta` to its fixed point (networkx-style),
|
|
2045
|
+
* then Euclidean (L2) normalizes the result. Scores flow along edge
|
|
2046
|
+
* direction: a node's score is fed by its incoming neighbors; undirected
|
|
2047
|
+
* edges feed both endpoints.
|
|
2048
|
+
*
|
|
2049
|
+
* Converges only when `alpha` is below the reciprocal of the largest
|
|
2050
|
+
* eigenvalue of the adjacency matrix; throws when the iteration has not
|
|
2051
|
+
* converged (L1 error < `n × tolerance`) within `maxIterations`.
|
|
2052
|
+
*/
|
|
2053
|
+
function getKatzCentrality(graph, options) {
|
|
2054
|
+
if (getNodeIds(graph).length === 0) return {};
|
|
2055
|
+
const alpha = options?.alpha ?? .1;
|
|
2056
|
+
const beta = options?.beta ?? 1;
|
|
2057
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
2058
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
2059
|
+
const getWeight = options?.getWeight;
|
|
2060
|
+
const csr = getCSR(graph);
|
|
2061
|
+
const n = csr.ids.length;
|
|
2062
|
+
let current = new Float64Array(n);
|
|
2063
|
+
let converged = false;
|
|
2064
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2065
|
+
const next = new Float64Array(n).fill(beta);
|
|
2066
|
+
for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
|
|
2067
|
+
const weight = getWeight ? getWeight(graph.edges[csr.inEdgeIndex[a]]) : 1;
|
|
2068
|
+
next[w] += alpha * current[csr.inOrigins[a]] * weight;
|
|
2069
|
+
}
|
|
2070
|
+
let error = 0;
|
|
2071
|
+
for (let i = 0; i < n; i++) error += Math.abs(next[i] - current[i]);
|
|
2072
|
+
current = next;
|
|
2073
|
+
if (error < n * tolerance) {
|
|
2074
|
+
converged = true;
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2658
2077
|
}
|
|
2078
|
+
if (!converged) throw new Error(`getKatzCentrality: iteration failed to converge within ${maxIterations} iterations — alpha ${alpha} may be >= 1/λ_max of the adjacency matrix; decrease options.alpha or increase options.maxIterations`);
|
|
2079
|
+
normalizeTypedVector(current);
|
|
2659
2080
|
const scores = createEmptyScoreMap(graph);
|
|
2660
2081
|
for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
|
|
2661
2082
|
return scores;
|
|
2662
2083
|
}
|
|
2663
2084
|
|
|
2085
|
+
//#endregion
|
|
2086
|
+
//#region src/algorithms/cores.ts
|
|
2087
|
+
/**
|
|
2088
|
+
* Undirected adjacency over node positions: every edge (regardless of mode
|
|
2089
|
+
* or direction) contributes both arcs, matching the standard k-core
|
|
2090
|
+
* definition. Self-loops are ignored. Positions come from the CSR snapshot
|
|
2091
|
+
* so `ids` order matches `graph.nodes`.
|
|
2092
|
+
*/
|
|
2093
|
+
function buildUndirectedAdjacency(graph) {
|
|
2094
|
+
const csr = getCSR(graph);
|
|
2095
|
+
const n = csr.ids.length;
|
|
2096
|
+
const m = graph.edges.length;
|
|
2097
|
+
const sourcePos = new Int32Array(m);
|
|
2098
|
+
const targetPos = new Int32Array(m);
|
|
2099
|
+
const degree = new Int32Array(n);
|
|
2100
|
+
let arcCount = 0;
|
|
2101
|
+
for (let e = 0; e < m; e++) {
|
|
2102
|
+
const edge = graph.edges[e];
|
|
2103
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2104
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2105
|
+
if (s === t) {
|
|
2106
|
+
sourcePos[e] = -1;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
sourcePos[e] = s;
|
|
2110
|
+
targetPos[e] = t;
|
|
2111
|
+
degree[s]++;
|
|
2112
|
+
degree[t]++;
|
|
2113
|
+
arcCount += 2;
|
|
2114
|
+
}
|
|
2115
|
+
const offsets = new Int32Array(n + 1);
|
|
2116
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2117
|
+
const targets = new Int32Array(arcCount);
|
|
2118
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2119
|
+
for (let e = 0; e < m; e++) {
|
|
2120
|
+
if (sourcePos[e] === -1) continue;
|
|
2121
|
+
targets[cursor[sourcePos[e]]++] = targetPos[e];
|
|
2122
|
+
targets[cursor[targetPos[e]]++] = sourcePos[e];
|
|
2123
|
+
}
|
|
2124
|
+
return {
|
|
2125
|
+
ids: csr.ids,
|
|
2126
|
+
offsets,
|
|
2127
|
+
targets
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Returns the core number of every node (largest `k` such that the node
|
|
2132
|
+
* belongs to the k-core).
|
|
2133
|
+
*
|
|
2134
|
+
* Uses Batagelj–Zaveršnik bucket peeling — O(m). Edges are treated as
|
|
2135
|
+
* undirected (the standard k-core definition); self-loops are ignored.
|
|
2136
|
+
*
|
|
2137
|
+
* @example
|
|
2138
|
+
* ```ts
|
|
2139
|
+
* const cores = getCoreNumbers(graph);
|
|
2140
|
+
* console.log(cores.a); // 3
|
|
2141
|
+
* ```
|
|
2142
|
+
*/
|
|
2143
|
+
function getCoreNumbers(graph) {
|
|
2144
|
+
const { ids, offsets, targets } = buildUndirectedAdjacency(graph);
|
|
2145
|
+
const n = ids.length;
|
|
2146
|
+
const core = new Int32Array(n);
|
|
2147
|
+
let maxDegree = 0;
|
|
2148
|
+
for (let i = 0; i < n; i++) {
|
|
2149
|
+
core[i] = offsets[i + 1] - offsets[i];
|
|
2150
|
+
if (core[i] > maxDegree) maxDegree = core[i];
|
|
2151
|
+
}
|
|
2152
|
+
const bin = new Int32Array(maxDegree + 1);
|
|
2153
|
+
for (let i = 0; i < n; i++) bin[core[i]]++;
|
|
2154
|
+
let start = 0;
|
|
2155
|
+
for (let d = 0; d <= maxDegree; d++) {
|
|
2156
|
+
const count = bin[d];
|
|
2157
|
+
bin[d] = start;
|
|
2158
|
+
start += count;
|
|
2159
|
+
}
|
|
2160
|
+
const vert = new Int32Array(n);
|
|
2161
|
+
const pos = new Int32Array(n);
|
|
2162
|
+
for (let i = 0; i < n; i++) {
|
|
2163
|
+
pos[i] = bin[core[i]];
|
|
2164
|
+
vert[pos[i]] = i;
|
|
2165
|
+
bin[core[i]]++;
|
|
2166
|
+
}
|
|
2167
|
+
for (let d = maxDegree; d > 0; d--) bin[d] = bin[d - 1];
|
|
2168
|
+
bin[0] = 0;
|
|
2169
|
+
for (let k = 0; k < n; k++) {
|
|
2170
|
+
const v = vert[k];
|
|
2171
|
+
for (let a = offsets[v]; a < offsets[v + 1]; a++) {
|
|
2172
|
+
const u = targets[a];
|
|
2173
|
+
if (core[u] > core[v]) {
|
|
2174
|
+
const du = core[u];
|
|
2175
|
+
const pu = pos[u];
|
|
2176
|
+
const pw = bin[du];
|
|
2177
|
+
const w = vert[pw];
|
|
2178
|
+
if (u !== w) {
|
|
2179
|
+
vert[pu] = w;
|
|
2180
|
+
pos[w] = pu;
|
|
2181
|
+
vert[pw] = u;
|
|
2182
|
+
pos[u] = pw;
|
|
2183
|
+
}
|
|
2184
|
+
bin[du]++;
|
|
2185
|
+
core[u]--;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
const result = {};
|
|
2190
|
+
for (let i = 0; i < n; i++) result[ids[i]] = core[i];
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Returns the ids of all nodes in the k-core: the maximal subgraph in which
|
|
2195
|
+
* every node has at least `k` neighbors (edges treated as undirected).
|
|
2196
|
+
*
|
|
2197
|
+
* Node order follows `graph.nodes`. `k <= 0` returns every node id.
|
|
2198
|
+
*/
|
|
2199
|
+
function getKCore(graph, k) {
|
|
2200
|
+
const cores = getCoreNumbers(graph);
|
|
2201
|
+
return graph.nodes.filter((node) => cores[node.id] >= k).map((node) => node.id);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
//#endregion
|
|
2205
|
+
//#region src/algorithms/bipartite.ts
|
|
2206
|
+
/**
|
|
2207
|
+
* BFS 2-coloring over undirected adjacency. Edges are treated as undirected
|
|
2208
|
+
* regardless of mode/direction. Returns either the coloring or the edge that
|
|
2209
|
+
* proves the graph is not bipartite.
|
|
2210
|
+
*/
|
|
2211
|
+
function getTwoColoring(graph) {
|
|
2212
|
+
const csr = getCSR(graph);
|
|
2213
|
+
const n = csr.ids.length;
|
|
2214
|
+
const m = graph.edges.length;
|
|
2215
|
+
const degree = new Int32Array(n);
|
|
2216
|
+
for (let e = 0; e < m; e++) {
|
|
2217
|
+
const edge = graph.edges[e];
|
|
2218
|
+
if (edge.sourceId === edge.targetId) return { conflictEdgeId: edge.id };
|
|
2219
|
+
degree[csr.indexOf.get(edge.sourceId)]++;
|
|
2220
|
+
degree[csr.indexOf.get(edge.targetId)]++;
|
|
2221
|
+
}
|
|
2222
|
+
const offsets = new Int32Array(n + 1);
|
|
2223
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2224
|
+
const targets = new Int32Array(offsets[n]);
|
|
2225
|
+
const arcEdge = new Int32Array(offsets[n]);
|
|
2226
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2227
|
+
for (let e = 0; e < m; e++) {
|
|
2228
|
+
const edge = graph.edges[e];
|
|
2229
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2230
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2231
|
+
targets[cursor[s]] = t;
|
|
2232
|
+
arcEdge[cursor[s]++] = e;
|
|
2233
|
+
targets[cursor[t]] = s;
|
|
2234
|
+
arcEdge[cursor[t]++] = e;
|
|
2235
|
+
}
|
|
2236
|
+
const colors = new Int8Array(n).fill(-1);
|
|
2237
|
+
const queue = new Int32Array(n);
|
|
2238
|
+
for (let root = 0; root < n; root++) {
|
|
2239
|
+
if (colors[root] !== -1) continue;
|
|
2240
|
+
colors[root] = 0;
|
|
2241
|
+
queue[0] = root;
|
|
2242
|
+
let head = 0;
|
|
2243
|
+
let tail = 1;
|
|
2244
|
+
while (head < tail) {
|
|
2245
|
+
const u = queue[head++];
|
|
2246
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2247
|
+
const v = targets[a];
|
|
2248
|
+
if (colors[v] === -1) {
|
|
2249
|
+
colors[v] = 1 - colors[u];
|
|
2250
|
+
queue[tail++] = v;
|
|
2251
|
+
} else if (colors[v] === colors[u]) return { conflictEdgeId: graph.edges[arcEdge[a]].id };
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return { colors };
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Returns whether the graph is bipartite (2-colorable).
|
|
2259
|
+
*
|
|
2260
|
+
* Edges are treated as undirected; self-loops make a graph non-bipartite.
|
|
2261
|
+
* Runs a BFS 2-coloring per connected component — O(n + m).
|
|
2262
|
+
*/
|
|
2263
|
+
function isBipartite(graph) {
|
|
2264
|
+
return "colors" in getTwoColoring(graph);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Returns a maximum-cardinality matching of a bipartite graph using
|
|
2268
|
+
* Hopcroft–Karp — O(m·√n).
|
|
2269
|
+
*
|
|
2270
|
+
* The bipartition is derived by 2-coloring (edges treated as undirected).
|
|
2271
|
+
* Each match reports the realizing edge with its stored `sourceId`/
|
|
2272
|
+
* `targetId` orientation; for parallel edges between a matched pair, the
|
|
2273
|
+
* first edge used by the algorithm is reported.
|
|
2274
|
+
*
|
|
2275
|
+
* Throws if the graph is not bipartite, naming the offending edge.
|
|
2276
|
+
*/
|
|
2277
|
+
function getMaximumBipartiteMatching(graph) {
|
|
2278
|
+
const coloring = getTwoColoring(graph);
|
|
2279
|
+
if (!("colors" in coloring)) throw new Error(`getMaximumBipartiteMatching: graph is not bipartite — edge "${coloring.conflictEdgeId}" closes an odd cycle (or is a self-loop); a maximum bipartite matching requires a bipartite graph, check with isBipartite() first`);
|
|
2280
|
+
const { colors } = coloring;
|
|
2281
|
+
const csr = getCSR(graph);
|
|
2282
|
+
const n = csr.ids.length;
|
|
2283
|
+
const m = graph.edges.length;
|
|
2284
|
+
const degree = new Int32Array(n);
|
|
2285
|
+
for (let e = 0; e < m; e++) {
|
|
2286
|
+
const edge = graph.edges[e];
|
|
2287
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2288
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2289
|
+
degree[colors[s] === 0 ? s : t]++;
|
|
2290
|
+
}
|
|
2291
|
+
const offsets = new Int32Array(n + 1);
|
|
2292
|
+
for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i] + degree[i];
|
|
2293
|
+
const targets = new Int32Array(offsets[n]);
|
|
2294
|
+
const arcEdge = new Int32Array(offsets[n]);
|
|
2295
|
+
const cursor = Int32Array.from(offsets.subarray(0, n));
|
|
2296
|
+
for (let e = 0; e < m; e++) {
|
|
2297
|
+
const edge = graph.edges[e];
|
|
2298
|
+
const s = csr.indexOf.get(edge.sourceId);
|
|
2299
|
+
const t = csr.indexOf.get(edge.targetId);
|
|
2300
|
+
const left = colors[s] === 0 ? s : t;
|
|
2301
|
+
const right = colors[s] === 0 ? t : s;
|
|
2302
|
+
targets[cursor[left]] = right;
|
|
2303
|
+
arcEdge[cursor[left]++] = e;
|
|
2304
|
+
}
|
|
2305
|
+
const INF = 2147483647;
|
|
2306
|
+
const matchLeft = new Int32Array(n).fill(-1);
|
|
2307
|
+
const matchRight = new Int32Array(n).fill(-1);
|
|
2308
|
+
const matchEdge = new Int32Array(n).fill(-1);
|
|
2309
|
+
const dist = new Int32Array(n);
|
|
2310
|
+
const queue = new Int32Array(n);
|
|
2311
|
+
function hasAugmentingLayer() {
|
|
2312
|
+
let head = 0;
|
|
2313
|
+
let tail = 0;
|
|
2314
|
+
for (let u = 0; u < n; u++) {
|
|
2315
|
+
if (colors[u] !== 0) continue;
|
|
2316
|
+
if (matchLeft[u] === -1) {
|
|
2317
|
+
dist[u] = 0;
|
|
2318
|
+
queue[tail++] = u;
|
|
2319
|
+
} else dist[u] = INF;
|
|
2320
|
+
}
|
|
2321
|
+
let foundAugmenting = false;
|
|
2322
|
+
while (head < tail) {
|
|
2323
|
+
const u = queue[head++];
|
|
2324
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2325
|
+
const w = matchRight[targets[a]];
|
|
2326
|
+
if (w === -1) foundAugmenting = true;
|
|
2327
|
+
else if (dist[w] === INF) {
|
|
2328
|
+
dist[w] = dist[u] + 1;
|
|
2329
|
+
queue[tail++] = w;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return foundAugmenting;
|
|
2334
|
+
}
|
|
2335
|
+
function hasAugmentedMatchFrom(u) {
|
|
2336
|
+
for (let a = offsets[u]; a < offsets[u + 1]; a++) {
|
|
2337
|
+
const v = targets[a];
|
|
2338
|
+
const w = matchRight[v];
|
|
2339
|
+
if (w === -1 || dist[w] === dist[u] + 1 && hasAugmentedMatchFrom(w)) {
|
|
2340
|
+
matchLeft[u] = v;
|
|
2341
|
+
matchRight[v] = u;
|
|
2342
|
+
matchEdge[u] = arcEdge[a];
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
dist[u] = INF;
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
while (hasAugmentingLayer()) for (let u = 0; u < n; u++) if (colors[u] === 0 && matchLeft[u] === -1) hasAugmentedMatchFrom(u);
|
|
2350
|
+
const matches = [];
|
|
2351
|
+
for (let u = 0; u < n; u++) {
|
|
2352
|
+
if (matchEdge[u] === -1) continue;
|
|
2353
|
+
const edge = graph.edges[matchEdge[u]];
|
|
2354
|
+
matches.push({
|
|
2355
|
+
sourceId: edge.sourceId,
|
|
2356
|
+
targetId: edge.targetId,
|
|
2357
|
+
edgeId: edge.id
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
return matches;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2664
2363
|
//#endregion
|
|
2665
2364
|
//#region src/algorithms/community.ts
|
|
2666
2365
|
function getUndirectedNeighbors$1(graph, nodeId) {
|
|
@@ -2777,14 +2476,52 @@ function toCommunityIds(communities) {
|
|
|
2777
2476
|
return communities.map((community) => new Set(community.map((node) => node.id)));
|
|
2778
2477
|
}
|
|
2779
2478
|
/**
|
|
2479
|
+
* Asynchronous LPA: labels update in place as the (seeded-shuffled) round
|
|
2480
|
+
* proceeds; ties among maximal neighbor labels break uniformly at random,
|
|
2481
|
+
* keeping the current label when it is already maximal so rounds terminate.
|
|
2482
|
+
* Deterministic per seed.
|
|
2483
|
+
*/
|
|
2484
|
+
function getSeededLabelPropagation(graph, seed, maxIterations) {
|
|
2485
|
+
const rng = mulberry32(seed);
|
|
2486
|
+
const labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
2487
|
+
const order = graph.nodes.map((node) => node.id);
|
|
2488
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
2489
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
2490
|
+
const j = Math.floor(rng() * (i + 1));
|
|
2491
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
2492
|
+
}
|
|
2493
|
+
let changed = false;
|
|
2494
|
+
for (const nodeId of order) {
|
|
2495
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2496
|
+
for (const neighbor of getUndirectedNeighbors$1(graph, nodeId)) {
|
|
2497
|
+
const label = labels[neighbor.nodeId];
|
|
2498
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2499
|
+
}
|
|
2500
|
+
if (counts.size === 0) continue;
|
|
2501
|
+
let maxCount = 0;
|
|
2502
|
+
for (const count of counts.values()) if (count > maxCount) maxCount = count;
|
|
2503
|
+
const best = [];
|
|
2504
|
+
for (const [label, count] of counts) if (count === maxCount) best.push(label);
|
|
2505
|
+
if (best.includes(labels[nodeId])) continue;
|
|
2506
|
+
labels[nodeId] = best[Math.floor(rng() * best.length)];
|
|
2507
|
+
changed = true;
|
|
2508
|
+
}
|
|
2509
|
+
if (!changed) break;
|
|
2510
|
+
}
|
|
2511
|
+
return normalizeCommunities(graph, labels);
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2780
2514
|
* Returns label-propagation communities for the graph.
|
|
2781
2515
|
*
|
|
2782
2516
|
* The implementation is deterministic: ties are broken by lexicographic label
|
|
2783
|
-
* order so test results remain stable.
|
|
2517
|
+
* order so test results remain stable. Pass `options.seed` for the classic
|
|
2518
|
+
* asynchronous variant (shuffled node order per round, random tie-breaking)
|
|
2519
|
+
* — still deterministic per seed.
|
|
2784
2520
|
*/
|
|
2785
2521
|
function getLabelPropagationCommunities(graph, options) {
|
|
2786
2522
|
if (graph.nodes.length === 0) return [];
|
|
2787
2523
|
const maxIterations = options?.maxIterations ?? 50;
|
|
2524
|
+
if (options?.seed !== void 0) return getSeededLabelPropagation(graph, options.seed, maxIterations);
|
|
2788
2525
|
let labels = Object.fromEntries(graph.nodes.map((node) => [node.id, node.id]));
|
|
2789
2526
|
const nodeIds = graph.nodes.map((node) => node.id).sort();
|
|
2790
2527
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
@@ -3242,30 +2979,14 @@ function getLouvainCommunities(graph, options) {
|
|
|
3242
2979
|
//#endregion
|
|
3243
2980
|
//#region src/algorithms/flow.ts
|
|
3244
2981
|
/**
|
|
3245
|
-
*
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
* Directed edges carry capacity from source to target only. Edges whose
|
|
3249
|
-
* effective mode is not `'directed'` (undirected/bidirectional) are modeled
|
|
3250
|
-
* as two independent opposite arcs, each with the edge's full capacity.
|
|
3251
|
-
*
|
|
3252
|
-
* The returned `flows` record maps every edge id to its net flow (positive
|
|
3253
|
-
* in the source→target direction). `cutEdges` is a minimum s-t cut: the
|
|
3254
|
-
* edges crossing from the source side to the sink side of the final
|
|
3255
|
-
* residual graph; the sum of their capacities equals `value`.
|
|
3256
|
-
*
|
|
3257
|
-
* @example
|
|
3258
|
-
* ```ts
|
|
3259
|
-
* const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
|
|
3260
|
-
* ```
|
|
2982
|
+
* Shared Edmonds-Karp solver behind {@link getMaxFlow} and {@link getMinCut}.
|
|
2983
|
+
* `caller`/`fromOption`/`toOption` only shape the error messages.
|
|
3261
2984
|
*/
|
|
3262
|
-
function
|
|
3263
|
-
const { from, to } = options;
|
|
3264
|
-
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
2985
|
+
function solveMaxFlow(graph, caller, fromOption, toOption, from, to, getCapacity) {
|
|
3265
2986
|
const idx = getIndex(graph);
|
|
3266
|
-
if (!idx.nodeById.has(from)) throw new Error(
|
|
3267
|
-
if (!idx.nodeById.has(to)) throw new Error(
|
|
3268
|
-
if (from === to) throw new Error(
|
|
2987
|
+
if (!idx.nodeById.has(from)) throw new Error(`${caller}: source node "${from}" not found in graph — pass an existing node id as ${fromOption}`);
|
|
2988
|
+
if (!idx.nodeById.has(to)) throw new Error(`${caller}: sink node "${to}" not found in graph — pass an existing node id as ${toOption}`);
|
|
2989
|
+
if (from === to) throw new Error(`${caller}: source and sink are both "${from}" — they must be different nodes`);
|
|
3269
2990
|
const arcs = [];
|
|
3270
2991
|
const outArcs = /* @__PURE__ */ new Map();
|
|
3271
2992
|
for (const node of graph.nodes) outArcs.set(node.id, []);
|
|
@@ -3287,7 +3008,7 @@ function getMaxFlow(graph, options) {
|
|
|
3287
3008
|
}
|
|
3288
3009
|
for (const edge of graph.edges) {
|
|
3289
3010
|
const capacity = getCapacity(edge);
|
|
3290
|
-
if (capacity < 0) throw new Error(
|
|
3011
|
+
if (capacity < 0) throw new Error(`${caller}: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
|
|
3291
3012
|
if (edge.sourceId === edge.targetId) continue;
|
|
3292
3013
|
addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
|
|
3293
3014
|
if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
|
|
@@ -3349,12 +3070,73 @@ function getMaxFlow(graph, options) {
|
|
|
3349
3070
|
if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
|
|
3350
3071
|
}
|
|
3351
3072
|
const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
|
|
3073
|
+
return {
|
|
3074
|
+
value,
|
|
3075
|
+
flows,
|
|
3076
|
+
cutEdges,
|
|
3077
|
+
sourceSide
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Returns the maximum flow from `from` to `to` using the Edmonds-Karp
|
|
3082
|
+
* algorithm (BFS augmenting paths).
|
|
3083
|
+
*
|
|
3084
|
+
* Directed edges carry capacity from source to target only. Edges whose
|
|
3085
|
+
* effective mode is not `'directed'` (undirected/bidirectional) are modeled
|
|
3086
|
+
* as two independent opposite arcs, each with the edge's full capacity.
|
|
3087
|
+
*
|
|
3088
|
+
* The returned `flows` record maps every edge id to its net flow (positive
|
|
3089
|
+
* in the source→target direction). `cutEdges` is a minimum s-t cut: the
|
|
3090
|
+
* edges crossing from the source side to the sink side of the final
|
|
3091
|
+
* residual graph; the sum of their capacities equals `value`.
|
|
3092
|
+
*
|
|
3093
|
+
* @example
|
|
3094
|
+
* ```ts
|
|
3095
|
+
* const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
|
|
3096
|
+
* ```
|
|
3097
|
+
*/
|
|
3098
|
+
function getMaxFlow(graph, options) {
|
|
3099
|
+
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
3100
|
+
const { value, flows, cutEdges } = solveMaxFlow(graph, "getMaxFlow", "options.from", "options.to", options.from, options.to, getCapacity);
|
|
3352
3101
|
return {
|
|
3353
3102
|
value,
|
|
3354
3103
|
flows,
|
|
3355
3104
|
cutEdges
|
|
3356
3105
|
};
|
|
3357
3106
|
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Returns a minimum s-t cut between `source` and `sink` via the max-flow
|
|
3109
|
+
* min-cut theorem: runs the same Edmonds-Karp solver as {@link getMaxFlow},
|
|
3110
|
+
* then splits the nodes by residual reachability from the source.
|
|
3111
|
+
*
|
|
3112
|
+
* `partition.source` holds every node reachable from `source` in the final
|
|
3113
|
+
* residual graph; `partition.sink` holds the rest (both in `graph.nodes`
|
|
3114
|
+
* order). `cutEdges` are the ids of the edges crossing the cut, and their
|
|
3115
|
+
* total capacity equals `value` (the max-flow value).
|
|
3116
|
+
*
|
|
3117
|
+
* @example
|
|
3118
|
+
* ```ts
|
|
3119
|
+
* const { value, cutEdges, partition } = getMinCut(graph, {
|
|
3120
|
+
* source: 's',
|
|
3121
|
+
* sink: 't',
|
|
3122
|
+
* });
|
|
3123
|
+
* ```
|
|
3124
|
+
*/
|
|
3125
|
+
function getMinCut(graph, options) {
|
|
3126
|
+
const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
|
|
3127
|
+
const { value, cutEdges, sourceSide } = solveMaxFlow(graph, "getMinCut", "options.source", "options.sink", options.source, options.sink, getCapacity);
|
|
3128
|
+
const sourcePartition = [];
|
|
3129
|
+
const sinkPartition = [];
|
|
3130
|
+
for (const node of graph.nodes) (sourceSide.has(node.id) ? sourcePartition : sinkPartition).push(node.id);
|
|
3131
|
+
return {
|
|
3132
|
+
value,
|
|
3133
|
+
cutEdges: cutEdges.map((edge) => edge.id),
|
|
3134
|
+
partition: {
|
|
3135
|
+
source: sourcePartition,
|
|
3136
|
+
sink: sinkPartition
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3358
3140
|
|
|
3359
3141
|
//#endregion
|
|
3360
3142
|
//#region src/algorithms/dominators.ts
|
|
@@ -3524,4 +3306,4 @@ function getTransitiveReduction(graph) {
|
|
|
3524
3306
|
}
|
|
3525
3307
|
|
|
3526
3308
|
//#endregion
|
|
3527
|
-
export {
|
|
3309
|
+
export { getAStarPath as $, genPreorders as A, getTopologicalSort as B, getHITS as C, getPageRank as D, getOutDegreeCentrality as E, bfs as F, flatten as G, isAcyclic as H, dfs as I, getSubgraph as J, getFlattenedGraph as K, genBFS as L, getPostorders as M, getPreorder as N, getMinimumSpanningTree as O, getPreorders as P, genSimplePaths as Q, genDFS as R, getEigenvectorCentrality as S, getKatzCentrality as T, isConnected as U, hasPath as V, isTree as W, genCycles as X, reverseGraph as Y, genShortestPaths as Z, getCoreNumbers as _, getLouvainCommunities as a, getSimplePath as at, getClosenessCentrality as b, getBiconnectedComponents as c, joinPaths as ct, getGirvanNewmanCommunities as d, getAllPairsShortestPaths as et, getGreedyModularityCommunities as f, isBipartite as g, getMaximumBipartiteMatching as h, getMinCut as i, getShortestPaths as it, getPostorder as j, genPostorders as k, getBridges as l, mulberry32 as lt, getModularity as m, getDominatorTree as n, getJoinedPath as nt, isIsomorphic as o, getSimplePaths as ot, getLabelPropagationCommunities as p, getReversedGraph as q, getMaxFlow as r, getShortestPath as rt, getArticulationPoints as s, getStronglyConnectedComponents as st, getTransitiveReduction as t, getCycles as tt, genGirvanNewmanCommunities as u, getKCore as v, getInDegreeCentrality as w, getDegreeCentrality as x, getBetweennessCentrality as y, getConnectedComponents as z };
|