@graphty/layout 1.2.9 → 1.3.2
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 +393 -403
- package/dist/layout.js +867 -808
- package/dist/layout.js.map +1 -1
- package/dist/src/algorithms/index.d.ts +2 -2
- package/dist/src/algorithms/index.js +2 -2
- package/dist/src/algorithms/index.js.map +1 -1
- package/dist/src/algorithms/optimization/index.d.ts +4 -4
- package/dist/src/algorithms/optimization/index.js +3 -3
- package/dist/src/algorithms/optimization/index.js.map +1 -1
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +2 -5
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +6 -9
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -1
- package/dist/src/algorithms/optimization/lbfgs.d.ts +0 -1
- package/dist/src/algorithms/optimization/lbfgs.js +3 -5
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -1
- package/dist/src/algorithms/optimization/line-search.d.ts +0 -1
- package/dist/src/algorithms/optimization/line-search.js +0 -1
- package/dist/src/algorithms/optimization/line-search.js.map +1 -1
- package/dist/src/algorithms/optimization/types.d.ts +1 -1
- package/dist/src/algorithms/planarity/check.d.ts +2 -2
- package/dist/src/algorithms/planarity/check.js +4 -4
- package/dist/src/algorithms/planarity/check.js.map +1 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +2 -4
- package/dist/src/algorithms/planarity/embedding.js +13 -12
- package/dist/src/algorithms/planarity/embedding.js.map +1 -1
- package/dist/src/algorithms/planarity/index.d.ts +4 -4
- package/dist/src/algorithms/planarity/index.js +4 -4
- package/dist/src/algorithms/planarity/index.js.map +1 -1
- package/dist/src/algorithms/planarity/lr-test.d.ts +2 -2
- package/dist/src/algorithms/planarity/lr-test.js +2 -2
- package/dist/src/algorithms/planarity/lr-test.js.map +1 -1
- package/dist/src/algorithms/planarity/special-graphs.d.ts +1 -4
- package/dist/src/algorithms/planarity/special-graphs.js +18 -15
- package/dist/src/algorithms/planarity/special-graphs.js.map +1 -1
- package/dist/src/generators/basic.d.ts +1 -1
- package/dist/src/generators/basic.js +4 -4
- package/dist/src/generators/basic.js.map +1 -1
- package/dist/src/generators/bipartite.d.ts +1 -1
- package/dist/src/generators/bipartite.js +2 -2
- package/dist/src/generators/bipartite.js.map +1 -1
- package/dist/src/generators/grid.d.ts +1 -1
- package/dist/src/generators/grid.js +1 -1
- package/dist/src/generators/grid.js.map +1 -1
- package/dist/src/generators/index.d.ts +5 -5
- package/dist/src/generators/index.js +5 -5
- package/dist/src/generators/index.js.map +1 -1
- package/dist/src/generators/random.d.ts +1 -1
- package/dist/src/generators/random.js +2 -2
- package/dist/src/generators/random.js.map +1 -1
- package/dist/src/generators/scale-free.d.ts +1 -1
- package/dist/src/generators/scale-free.js +4 -4
- package/dist/src/generators/scale-free.js.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/layouts/basic/index.d.ts +1 -1
- package/dist/src/layouts/basic/index.js +1 -1
- package/dist/src/layouts/basic/random.d.ts +1 -2
- package/dist/src/layouts/basic/random.js +3 -4
- package/dist/src/layouts/basic/random.js.map +1 -1
- package/dist/src/layouts/force-directed/arf.d.ts +1 -2
- package/dist/src/layouts/force-directed/arf.js +13 -8
- package/dist/src/layouts/force-directed/arf.js.map +1 -1
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +1 -2
- package/dist/src/layouts/force-directed/forceatlas2.js +58 -35
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -1
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +11 -12
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +17 -17
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -1
- package/dist/src/layouts/force-directed/index.d.ts +5 -5
- package/dist/src/layouts/force-directed/index.js +5 -5
- package/dist/src/layouts/force-directed/index.js.map +1 -1
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +2 -3
- package/dist/src/layouts/force-directed/kamada-kawai.js +12 -10
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -1
- package/dist/src/layouts/force-directed/spring.d.ts +11 -12
- package/dist/src/layouts/force-directed/spring.js +11 -12
- package/dist/src/layouts/force-directed/spring.js.map +1 -1
- package/dist/src/layouts/geometric/circular.d.ts +1 -2
- package/dist/src/layouts/geometric/circular.js +10 -9
- package/dist/src/layouts/geometric/circular.js.map +1 -1
- package/dist/src/layouts/geometric/index.d.ts +3 -3
- package/dist/src/layouts/geometric/index.js +3 -3
- package/dist/src/layouts/geometric/shell.d.ts +1 -2
- package/dist/src/layouts/geometric/shell.js +5 -5
- package/dist/src/layouts/geometric/shell.js.map +1 -1
- package/dist/src/layouts/geometric/spiral.d.ts +1 -2
- package/dist/src/layouts/geometric/spiral.js +5 -9
- package/dist/src/layouts/geometric/spiral.js.map +1 -1
- package/dist/src/layouts/hierarchical/bfs.d.ts +2 -3
- package/dist/src/layouts/hierarchical/bfs.js +5 -6
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -1
- package/dist/src/layouts/hierarchical/bipartite.d.ts +2 -3
- package/dist/src/layouts/hierarchical/bipartite.js +8 -9
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -1
- package/dist/src/layouts/hierarchical/index.d.ts +3 -3
- package/dist/src/layouts/hierarchical/index.js +3 -3
- package/dist/src/layouts/hierarchical/index.js.map +1 -1
- package/dist/src/layouts/hierarchical/multipartite.d.ts +2 -3
- package/dist/src/layouts/hierarchical/multipartite.js +7 -8
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -1
- package/dist/src/layouts/index.d.ts +5 -5
- package/dist/src/layouts/index.js +5 -5
- package/dist/src/layouts/index.js.map +1 -1
- package/dist/src/layouts/specialized/index.d.ts +2 -2
- package/dist/src/layouts/specialized/index.js +2 -2
- package/dist/src/layouts/specialized/index.js.map +1 -1
- package/dist/src/layouts/specialized/planar.d.ts +1 -2
- package/dist/src/layouts/specialized/planar.js +5 -6
- package/dist/src/layouts/specialized/planar.js.map +1 -1
- package/dist/src/layouts/specialized/spectral.d.ts +1 -2
- package/dist/src/layouts/specialized/spectral.js +30 -20
- package/dist/src/layouts/specialized/spectral.js.map +1 -1
- package/dist/src/types/embedding.d.ts +1 -1
- package/dist/src/types/index.d.ts +3 -3
- package/dist/src/types/index.js +3 -3
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/layout.d.ts +1 -1
- package/dist/src/utils/graph.d.ts +1 -5
- package/dist/src/utils/graph.js +4 -6
- package/dist/src/utils/graph.js.map +1 -1
- package/dist/src/utils/index.d.ts +5 -5
- package/dist/src/utils/index.js +5 -5
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/numpy.js +22 -16
- package/dist/src/utils/numpy.js.map +1 -1
- package/dist/src/utils/params.d.ts +1 -2
- package/dist/src/utils/params.js +0 -1
- package/dist/src/utils/params.js.map +1 -1
- package/dist/src/utils/random.d.ts +17 -0
- package/dist/src/utils/random.js +18 -1
- package/dist/src/utils/random.js.map +1 -1
- package/dist/src/utils/rescale.d.ts +1 -3
- package/dist/src/utils/rescale.js +12 -10
- package/dist/src/utils/rescale.js.map +1 -1
- package/dist/vitest.config.js +14 -14
- package/dist/vitest.config.js.map +1 -1
- package/package.json +29 -24
- package/src/algorithms/index.ts +2 -2
- package/src/algorithms/optimization/index.ts +4 -8
- package/src/algorithms/optimization/kamada-kawai-solver.ts +170 -179
- package/src/algorithms/optimization/lbfgs.ts +41 -48
- package/src/algorithms/optimization/line-search.ts +27 -28
- package/src/algorithms/optimization/types.ts +2 -2
- package/src/algorithms/planarity/check.ts +22 -22
- package/src/algorithms/planarity/embedding.ts +159 -170
- package/src/algorithms/planarity/index.ts +4 -8
- package/src/algorithms/planarity/lr-test.ts +46 -46
- package/src/algorithms/planarity/special-graphs.ts +73 -83
- package/src/generators/basic.ts +57 -57
- package/src/generators/bipartite.ts +37 -31
- package/src/generators/grid.ts +28 -28
- package/src/generators/index.ts +5 -11
- package/src/generators/random.ts +26 -25
- package/src/generators/scale-free.ts +56 -55
- package/src/index.ts +5 -5
- package/src/layouts/basic/index.ts +1 -1
- package/src/layouts/basic/random.ts +20 -16
- package/src/layouts/force-directed/arf.ts +107 -105
- package/src/layouts/force-directed/forceatlas2.ts +365 -351
- package/src/layouts/force-directed/fruchterman-reingold.ts +133 -134
- package/src/layouts/force-directed/index.ts +5 -5
- package/src/layouts/force-directed/kamada-kawai.ts +78 -80
- package/src/layouts/force-directed/spring.ts +24 -25
- package/src/layouts/geometric/circular.ts +63 -57
- package/src/layouts/geometric/index.ts +3 -3
- package/src/layouts/geometric/shell.ts +58 -53
- package/src/layouts/geometric/spiral.ts +60 -64
- package/src/layouts/hierarchical/bfs.ts +58 -59
- package/src/layouts/hierarchical/bipartite.ts +62 -63
- package/src/layouts/hierarchical/index.ts +3 -3
- package/src/layouts/hierarchical/multipartite.ts +62 -63
- package/src/layouts/index.ts +5 -5
- package/src/layouts/specialized/index.ts +2 -2
- package/src/layouts/specialized/planar.ts +41 -42
- package/src/layouts/specialized/spectral.ts +110 -99
- package/src/types/embedding.ts +5 -5
- package/src/types/graph.ts +4 -4
- package/src/types/index.ts +3 -3
- package/src/types/layout.ts +2 -2
- package/src/utils/graph.ts +31 -35
- package/src/utils/index.ts +5 -5
- package/src/utils/numpy.ts +96 -92
- package/src/utils/params.ts +14 -11
- package/src/utils/random.ts +57 -40
- package/src/utils/rescale.ts +169 -174
package/dist/layout.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
function rescaleLayout(pos, scale = 1, center = null) {
|
|
2
2
|
if (Array.isArray(pos)) {
|
|
3
|
-
if (pos.length === 0)
|
|
3
|
+
if (pos.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
4
6
|
} else {
|
|
5
|
-
if (Object.keys(pos).length === 0)
|
|
7
|
+
if (Object.keys(pos).length === 0) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
6
10
|
}
|
|
7
11
|
const posValues = Array.isArray(pos) ? pos : Object.values(pos);
|
|
8
12
|
const dim = posValues[0].length;
|
|
@@ -65,7 +69,9 @@ function rescaleLayout(pos, scale = 1, center = null) {
|
|
|
65
69
|
if (i >= origPos.length) {
|
|
66
70
|
return center[i];
|
|
67
71
|
}
|
|
68
|
-
if (isNaN(val))
|
|
72
|
+
if (isNaN(val)) {
|
|
73
|
+
return NaN;
|
|
74
|
+
}
|
|
69
75
|
return val * scaleFactor + center[i];
|
|
70
76
|
})
|
|
71
77
|
);
|
|
@@ -76,7 +82,9 @@ function rescaleLayout(pos, scale = 1, center = null) {
|
|
|
76
82
|
if (i >= origPos.length) {
|
|
77
83
|
return center[i];
|
|
78
84
|
}
|
|
79
|
-
if (isNaN(val))
|
|
85
|
+
if (isNaN(val)) {
|
|
86
|
+
return NaN;
|
|
87
|
+
}
|
|
80
88
|
return val * scaleFactor + center[i];
|
|
81
89
|
});
|
|
82
90
|
}
|
|
@@ -140,15 +148,6 @@ function rescaleLayoutDict(pos, scale = 1) {
|
|
|
140
148
|
}
|
|
141
149
|
return scaledPos;
|
|
142
150
|
}
|
|
143
|
-
function _processParams(G, center, dim) {
|
|
144
|
-
if (!center) {
|
|
145
|
-
center = Array(dim).fill(0);
|
|
146
|
-
}
|
|
147
|
-
if (center.length !== dim) {
|
|
148
|
-
throw new Error("length of center coordinates must match dimension of layout");
|
|
149
|
-
}
|
|
150
|
-
return { G, center };
|
|
151
|
-
}
|
|
152
151
|
function getNodesFromGraph(G) {
|
|
153
152
|
if (Array.isArray(G)) {
|
|
154
153
|
return G;
|
|
@@ -162,7 +161,9 @@ function getEdgesFromGraph(G) {
|
|
|
162
161
|
return G.edges();
|
|
163
162
|
}
|
|
164
163
|
function getNeighbors(graph, node) {
|
|
165
|
-
if (!graph.edges)
|
|
164
|
+
if (!graph.edges) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
166
167
|
const neighbors = /* @__PURE__ */ new Set();
|
|
167
168
|
const edges = graph.edges();
|
|
168
169
|
for (const [source, target] of edges) {
|
|
@@ -174,7 +175,20 @@ function getNeighbors(graph, node) {
|
|
|
174
175
|
}
|
|
175
176
|
return Array.from(neighbors);
|
|
176
177
|
}
|
|
178
|
+
function _processParams(G, center, dim) {
|
|
179
|
+
if (!center) {
|
|
180
|
+
center = Array(dim).fill(0);
|
|
181
|
+
}
|
|
182
|
+
if (center.length !== dim) {
|
|
183
|
+
throw new Error("length of center coordinates must match dimension of layout");
|
|
184
|
+
}
|
|
185
|
+
return { G, center };
|
|
186
|
+
}
|
|
177
187
|
class RandomNumberGenerator {
|
|
188
|
+
/**
|
|
189
|
+
* Creates a new random number generator instance.
|
|
190
|
+
* @param seed - Initial seed value for the generator. If not provided, uses a random seed.
|
|
191
|
+
*/
|
|
178
192
|
constructor(seed) {
|
|
179
193
|
this.seed = seed || Math.floor(Math.random() * 1e6);
|
|
180
194
|
this.m = 2 ** 35 - 31;
|
|
@@ -182,10 +196,19 @@ class RandomNumberGenerator {
|
|
|
182
196
|
this.c = 1;
|
|
183
197
|
this._state = this.seed % this.m;
|
|
184
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Generates the next random number in the sequence.
|
|
201
|
+
* @returns A random number between 0 and 1.
|
|
202
|
+
*/
|
|
185
203
|
_next() {
|
|
186
204
|
this._state = (this.a * this._state + this.c) % this.m;
|
|
187
205
|
return this._state / this.m;
|
|
188
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Generates random numbers according to the specified shape.
|
|
209
|
+
* @param shape - Determines the output format: null for single number, number for 1D array, array for multi-dimensional array.
|
|
210
|
+
* @returns A random number, 1D array, or multi-dimensional array depending on the shape parameter.
|
|
211
|
+
*/
|
|
189
212
|
rand(shape = null) {
|
|
190
213
|
if (shape === null) {
|
|
191
214
|
return this._next();
|
|
@@ -222,235 +245,354 @@ function randomLayout(G, center = null, dim = 2, seed = null) {
|
|
|
222
245
|
});
|
|
223
246
|
return pos;
|
|
224
247
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1e3, seed = null) {
|
|
249
|
+
if (a <= 1) {
|
|
250
|
+
throw new Error("The parameter a should be larger than 1");
|
|
251
|
+
}
|
|
252
|
+
const nodes = getNodesFromGraph(G);
|
|
253
|
+
const edges = getEdgesFromGraph(G);
|
|
254
|
+
if (nodes.length === 0) {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
if (!pos) {
|
|
258
|
+
pos = randomLayout(G, null, 2, seed);
|
|
259
|
+
} else {
|
|
260
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
261
|
+
const defaultPos = {};
|
|
262
|
+
nodes.forEach((node) => {
|
|
263
|
+
if (!pos[node]) {
|
|
264
|
+
defaultPos[node] = [rng.rand(), rng.rand()];
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
pos = { ...pos, ...defaultPos };
|
|
268
|
+
}
|
|
269
|
+
const nodeIndex = {};
|
|
270
|
+
nodes.forEach((node, i) => {
|
|
271
|
+
nodeIndex[node] = i;
|
|
272
|
+
});
|
|
273
|
+
const positions = nodes.map((node) => [...pos[node]]);
|
|
274
|
+
const N = nodes.length;
|
|
275
|
+
const K = Array(N).fill(0).map(() => Array(N).fill(1));
|
|
276
|
+
for (let i = 0; i < N; i++) {
|
|
277
|
+
K[i][i] = 0;
|
|
278
|
+
}
|
|
279
|
+
for (const [source, target] of edges) {
|
|
280
|
+
if (source === target) {
|
|
281
|
+
continue;
|
|
229
282
|
}
|
|
230
|
-
|
|
231
|
-
|
|
283
|
+
const i = nodeIndex[source];
|
|
284
|
+
const j = nodeIndex[target];
|
|
285
|
+
K[i][j] = a;
|
|
286
|
+
K[j][i] = a;
|
|
287
|
+
}
|
|
288
|
+
const rho = scaling * Math.sqrt(N);
|
|
289
|
+
const dt = 1e-3;
|
|
290
|
+
const etol = 1e-6;
|
|
291
|
+
let error = etol + 1;
|
|
292
|
+
let nIter = 0;
|
|
293
|
+
while (error > etol && nIter < maxIter) {
|
|
294
|
+
const change = Array(N).fill(0).map(() => [0, 0]);
|
|
295
|
+
for (let i = 0; i < N; i++) {
|
|
296
|
+
for (let j = 0; j < N; j++) {
|
|
297
|
+
if (i === j) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
|
|
301
|
+
const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
|
|
302
|
+
for (let d = 0; d < diff.length; d++) {
|
|
303
|
+
change[i][d] += K[i][j] * diff[d] - rho / dist * diff[d];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
232
306
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return Array(shape).fill(1);
|
|
307
|
+
for (let i = 0; i < N; i++) {
|
|
308
|
+
for (let d = 0; d < positions[i].length; d++) {
|
|
309
|
+
positions[i][d] += change[i][d] * dt;
|
|
310
|
+
}
|
|
238
311
|
}
|
|
239
|
-
|
|
240
|
-
|
|
312
|
+
error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
|
|
313
|
+
nIter++;
|
|
314
|
+
}
|
|
315
|
+
const finalPos = {};
|
|
316
|
+
nodes.forEach((node, i) => {
|
|
317
|
+
finalPos[node] = positions[i];
|
|
318
|
+
});
|
|
319
|
+
return finalPos;
|
|
320
|
+
}
|
|
321
|
+
function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1, scalingRatio = 2, gravity = 1, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) {
|
|
322
|
+
const processed = _processParams(G, null, dim);
|
|
323
|
+
const graph = processed.G;
|
|
324
|
+
const nodes = getNodesFromGraph(graph);
|
|
325
|
+
if (nodes.length === 0) {
|
|
326
|
+
return {};
|
|
327
|
+
}
|
|
328
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
329
|
+
let posArray;
|
|
330
|
+
if (pos === null) {
|
|
331
|
+
pos = {};
|
|
332
|
+
posArray = new Array(nodes.length);
|
|
333
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
334
|
+
posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
335
|
+
pos[nodes[i]] = posArray[i];
|
|
241
336
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
337
|
+
} else if (Object.keys(pos).length === nodes.length) {
|
|
338
|
+
posArray = new Array(nodes.length);
|
|
339
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
340
|
+
const nodePos = pos[nodes[i]];
|
|
341
|
+
posArray[i] = new Array(dim);
|
|
342
|
+
for (let d = 0; d < dim; d++) {
|
|
343
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
344
|
+
}
|
|
247
345
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
346
|
+
} else {
|
|
347
|
+
const minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
348
|
+
const maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
349
|
+
for (const node in pos) {
|
|
350
|
+
const nodePos = pos[node];
|
|
351
|
+
for (let d = 0; d < dim; d++) {
|
|
352
|
+
if (d < nodePos.length) {
|
|
353
|
+
minPos[d] = Math.min(minPos[d], nodePos[d]);
|
|
354
|
+
maxPos[d] = Math.max(maxPos[d], nodePos[d]);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
258
357
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const sum = flatArr.reduce((a, b) => a + b, 0);
|
|
265
|
-
return sum / flatArr.length;
|
|
358
|
+
for (let d = 0; d < dim; d++) {
|
|
359
|
+
if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
|
|
360
|
+
minPos[d] = -1;
|
|
361
|
+
maxPos[d] = 1;
|
|
362
|
+
}
|
|
266
363
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
364
|
+
posArray = new Array(nodes.length);
|
|
365
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
366
|
+
const node = nodes[i];
|
|
367
|
+
if (pos[node]) {
|
|
368
|
+
const nodePos = pos[node];
|
|
369
|
+
posArray[i] = new Array(dim);
|
|
370
|
+
for (let d = 0; d < dim; d++) {
|
|
371
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
274
372
|
}
|
|
275
|
-
|
|
373
|
+
} else {
|
|
374
|
+
posArray[i] = Array(dim).fill(0).map((_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d]));
|
|
375
|
+
pos[node] = posArray[i];
|
|
276
376
|
}
|
|
277
|
-
return result;
|
|
278
377
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
378
|
+
}
|
|
379
|
+
const mass = new Array(nodes.length).fill(0);
|
|
380
|
+
const size = new Array(nodes.length).fill(0);
|
|
381
|
+
const adjustSizes = nodeSize !== null;
|
|
382
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
383
|
+
const node = nodes[i];
|
|
384
|
+
mass[i] = nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1;
|
|
385
|
+
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
386
|
+
}
|
|
387
|
+
const n = nodes.length;
|
|
388
|
+
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
389
|
+
const edges = Array.isArray(graph) ? [] : graph.edges();
|
|
390
|
+
const nodeIndices = {};
|
|
391
|
+
nodes.forEach((node, i) => {
|
|
392
|
+
nodeIndices[node] = i;
|
|
393
|
+
});
|
|
394
|
+
for (const [source, target] of edges) {
|
|
395
|
+
const i = nodeIndices[source];
|
|
396
|
+
const j = nodeIndices[target];
|
|
397
|
+
let edgeWeight = 1;
|
|
398
|
+
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
399
|
+
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
284
400
|
}
|
|
285
|
-
|
|
286
|
-
|
|
401
|
+
A[i][j] = edgeWeight;
|
|
402
|
+
A[j][i] = edgeWeight;
|
|
403
|
+
}
|
|
404
|
+
const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
405
|
+
const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
406
|
+
const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
407
|
+
let speed = 1;
|
|
408
|
+
let speedEfficiency = 1;
|
|
409
|
+
function estimateFactor(n2, swing2, traction2, speed2, speedEfficiency2, jitterTolerance2) {
|
|
410
|
+
const optJitter = 0.05 * Math.sqrt(n2);
|
|
411
|
+
const minJitter = Math.sqrt(optJitter);
|
|
412
|
+
const maxJitter = 10;
|
|
413
|
+
const minSpeedEfficiency = 0.05;
|
|
414
|
+
const other = Math.min(maxJitter, optJitter * traction2 / (n2 * n2));
|
|
415
|
+
let jitter = jitterTolerance2 * Math.max(minJitter, other);
|
|
416
|
+
if (swing2 / traction2 > 2) {
|
|
417
|
+
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
418
|
+
speedEfficiency2 *= 0.5;
|
|
419
|
+
}
|
|
420
|
+
jitter = Math.max(jitter, jitterTolerance2);
|
|
287
421
|
}
|
|
288
|
-
|
|
289
|
-
|
|
422
|
+
const targetSpeed = swing2 === 0 ? Number.POSITIVE_INFINITY : jitter * speedEfficiency2 * traction2 / swing2;
|
|
423
|
+
if (swing2 > jitter * traction2) {
|
|
424
|
+
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
425
|
+
speedEfficiency2 *= 0.7;
|
|
426
|
+
}
|
|
427
|
+
} else if (speed2 < 1e3) {
|
|
428
|
+
speedEfficiency2 *= 1.3;
|
|
290
429
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
430
|
+
const maxRise = 0.5;
|
|
431
|
+
speed2 = speed2 + Math.min(targetSpeed - speed2, maxRise * speed2);
|
|
432
|
+
return [speed2, speedEfficiency2];
|
|
433
|
+
}
|
|
434
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
435
|
+
for (let i = 0; i < n; i++) {
|
|
436
|
+
for (let d = 0; d < dim; d++) {
|
|
437
|
+
attraction[i][d] = 0;
|
|
438
|
+
repulsion[i][d] = 0;
|
|
439
|
+
gravities[i][d] = 0;
|
|
440
|
+
}
|
|
296
441
|
}
|
|
297
|
-
|
|
298
|
-
|
|
442
|
+
const diff = Array(n).fill(0).map(
|
|
443
|
+
() => Array(n).fill(0).map(() => Array(dim).fill(0))
|
|
444
|
+
);
|
|
445
|
+
const distance = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
446
|
+
for (let i = 0; i < n; i++) {
|
|
447
|
+
for (let j = 0; j < n; j++) {
|
|
448
|
+
if (i === j) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
for (let d = 0; d < dim; d++) {
|
|
452
|
+
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
453
|
+
}
|
|
454
|
+
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
455
|
+
if (distance[i][j] < 0.01) {
|
|
456
|
+
distance[i][j] = 0.01;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
299
459
|
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
460
|
+
if (linlog) {
|
|
461
|
+
for (let i = 0; i < n; i++) {
|
|
462
|
+
for (let j = 0; j < n; j++) {
|
|
463
|
+
if (i === j || A[i][j] === 0) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const dist = distance[i][j];
|
|
467
|
+
const factor = -Math.log(1 + dist) / dist * A[i][j];
|
|
468
|
+
for (let d = 0; d < dim; d++) {
|
|
469
|
+
const force = factor * diff[i][j][d];
|
|
470
|
+
attraction[i][d] += force;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
for (let i = 0; i < n; i++) {
|
|
476
|
+
for (let j = 0; j < n; j++) {
|
|
477
|
+
if (i === j || A[i][j] === 0) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
for (let d = 0; d < dim; d++) {
|
|
481
|
+
const force = -diff[i][j][d] * A[i][j];
|
|
482
|
+
attraction[i][d] += force;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
302
486
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
min: function(arr) {
|
|
310
|
-
if (!Array.isArray(arr)) return arr;
|
|
311
|
-
return Math.min(...arr.flat(Infinity));
|
|
312
|
-
},
|
|
313
|
-
norm: function(arr) {
|
|
314
|
-
return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
function circularLayout(G, scale = 1, center = null, dim = 2) {
|
|
318
|
-
if (dim < 2) {
|
|
319
|
-
throw new Error("cannot handle dimensions < 2");
|
|
320
|
-
}
|
|
321
|
-
const processed = _processParams(G, center, dim);
|
|
322
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
323
|
-
center = processed.center;
|
|
324
|
-
const pos = {};
|
|
325
|
-
if (nodes.length === 0) {
|
|
326
|
-
return pos;
|
|
327
|
-
}
|
|
328
|
-
if (nodes.length === 1) {
|
|
329
|
-
pos[nodes[0]] = center;
|
|
330
|
-
return pos;
|
|
331
|
-
}
|
|
332
|
-
if (dim === 2) {
|
|
333
|
-
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
334
|
-
nodes.forEach((node, i) => {
|
|
335
|
-
const x = Math.cos(theta[i]) * scale + center[0];
|
|
336
|
-
const y = Math.sin(theta[i]) * scale + center[1];
|
|
337
|
-
pos[node] = [x, y];
|
|
338
|
-
});
|
|
339
|
-
} else if (dim === 3) {
|
|
340
|
-
const n = nodes.length;
|
|
341
|
-
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
342
|
-
nodes.forEach((node, i) => {
|
|
343
|
-
const theta = 2 * Math.PI * i / goldenRatio;
|
|
344
|
-
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
345
|
-
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
346
|
-
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
347
|
-
const z = Math.cos(phi) * scale + center[2];
|
|
348
|
-
pos[node] = [x, y, z];
|
|
349
|
-
});
|
|
350
|
-
} else {
|
|
351
|
-
const rng = new RandomNumberGenerator();
|
|
352
|
-
nodes.forEach((node) => {
|
|
353
|
-
const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
354
|
-
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
355
|
-
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
return pos;
|
|
359
|
-
}
|
|
360
|
-
function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
|
|
361
|
-
if (dim !== 2) {
|
|
362
|
-
throw new Error("can only handle 2 dimensions");
|
|
363
|
-
}
|
|
364
|
-
const processed = _processParams(G, center, dim);
|
|
365
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
366
|
-
center = processed.center;
|
|
367
|
-
const pos = {};
|
|
368
|
-
if (nodes.length === 0) {
|
|
369
|
-
return pos;
|
|
370
|
-
}
|
|
371
|
-
if (nodes.length === 1) {
|
|
372
|
-
pos[nodes[0]] = center;
|
|
373
|
-
return pos;
|
|
374
|
-
}
|
|
375
|
-
if (!nlist) {
|
|
376
|
-
nlist = [nodes];
|
|
377
|
-
}
|
|
378
|
-
const radiusBump = scale / nlist.length;
|
|
379
|
-
let radius;
|
|
380
|
-
if (nlist[0].length === 1) {
|
|
381
|
-
radius = 0;
|
|
382
|
-
pos[nlist[0][0]] = [...center];
|
|
383
|
-
radius += radiusBump;
|
|
384
|
-
} else {
|
|
385
|
-
radius = radiusBump;
|
|
386
|
-
}
|
|
387
|
-
for (let i = 0; i < nlist.length; i++) {
|
|
388
|
-
const shell = nlist[i];
|
|
389
|
-
if (shell.length === 0) continue;
|
|
390
|
-
if (shell.length === 1 && i === 0) {
|
|
391
|
-
continue;
|
|
487
|
+
if (distributedAction) {
|
|
488
|
+
for (let i = 0; i < n; i++) {
|
|
489
|
+
for (let d = 0; d < dim; d++) {
|
|
490
|
+
attraction[i][d] /= mass[i];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
392
493
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
494
|
+
for (let i = 0; i < n; i++) {
|
|
495
|
+
for (let j = 0; j < n; j++) {
|
|
496
|
+
if (i === j) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
let dist = distance[i][j];
|
|
500
|
+
if (adjustSizes) {
|
|
501
|
+
dist -= size[i] - size[j];
|
|
502
|
+
dist = Math.max(dist, 0.01);
|
|
503
|
+
}
|
|
504
|
+
const distSquared = dist * dist;
|
|
505
|
+
const massProduct = mass[i] * mass[j];
|
|
506
|
+
const factor = massProduct / distSquared * scalingRatio;
|
|
507
|
+
for (let d = 0; d < dim; d++) {
|
|
508
|
+
const direction = diff[i][j][d] / dist;
|
|
509
|
+
repulsion[i][d] += direction * factor;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const centerOfMass = Array(dim).fill(0);
|
|
514
|
+
for (let i = 0; i < n; i++) {
|
|
515
|
+
for (let d = 0; d < dim; d++) {
|
|
516
|
+
centerOfMass[d] += posArray[i][d] / n;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (let i = 0; i < n; i++) {
|
|
520
|
+
const posCentered = Array(dim);
|
|
521
|
+
for (let d = 0; d < dim; d++) {
|
|
522
|
+
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
523
|
+
}
|
|
524
|
+
if (strongGravity) {
|
|
525
|
+
for (let d = 0; d < dim; d++) {
|
|
526
|
+
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
530
|
+
if (dist > 0.01) {
|
|
531
|
+
for (let d = 0; d < dim; d++) {
|
|
532
|
+
const direction = posCentered[d] / dist;
|
|
533
|
+
gravities[i][d] = -gravity * mass[i] * direction;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const update = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
539
|
+
let totalSwing = 0;
|
|
540
|
+
let totalTraction = 0;
|
|
541
|
+
for (let i = 0; i < n; i++) {
|
|
542
|
+
for (let d = 0; d < dim; d++) {
|
|
543
|
+
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
544
|
+
}
|
|
545
|
+
const oldPos = [...posArray[i]];
|
|
546
|
+
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
547
|
+
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
548
|
+
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
549
|
+
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
550
|
+
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
551
|
+
totalSwing += mass[i] * swingMagnitude;
|
|
552
|
+
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
553
|
+
}
|
|
554
|
+
[speed, speedEfficiency] = estimateFactor(
|
|
555
|
+
n,
|
|
556
|
+
totalSwing,
|
|
557
|
+
totalTraction,
|
|
558
|
+
speed,
|
|
559
|
+
speedEfficiency,
|
|
560
|
+
jitterTolerance
|
|
561
|
+
);
|
|
562
|
+
let totalMovement = 0;
|
|
563
|
+
for (let i = 0; i < n; i++) {
|
|
564
|
+
let factor;
|
|
565
|
+
if (adjustSizes) {
|
|
566
|
+
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
567
|
+
const swinging = mass[i] * df;
|
|
568
|
+
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
569
|
+
factor = Math.min(factor * df, 10) / df;
|
|
570
|
+
} else {
|
|
571
|
+
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
572
|
+
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
573
|
+
}
|
|
574
|
+
for (let d = 0; d < dim; d++) {
|
|
575
|
+
const movement = update[i][d] * factor;
|
|
576
|
+
posArray[i][d] += movement;
|
|
577
|
+
totalMovement += Math.abs(movement);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (totalMovement < 1e-10) {
|
|
581
|
+
break;
|
|
428
582
|
}
|
|
429
|
-
} else {
|
|
430
|
-
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
431
|
-
const angle = dist.map((d) => resolution * d);
|
|
432
|
-
positions = dist.map((d, i) => [
|
|
433
|
-
Math.cos(angle[i]) * d,
|
|
434
|
-
Math.sin(angle[i]) * d
|
|
435
|
-
]);
|
|
436
|
-
}
|
|
437
|
-
const posArray = [];
|
|
438
|
-
for (let i = 0; i < positions.length; i++) {
|
|
439
|
-
posArray.push(positions[i]);
|
|
440
|
-
}
|
|
441
|
-
const scaledPositions = rescaleLayout(posArray, scale);
|
|
442
|
-
for (let i = 0; i < scaledPositions.length; i++) {
|
|
443
|
-
scaledPositions[i][0] += center[0];
|
|
444
|
-
scaledPositions[i][1] += center[1];
|
|
445
583
|
}
|
|
446
|
-
|
|
447
|
-
|
|
584
|
+
const positions = {};
|
|
585
|
+
for (let i = 0; i < n; i++) {
|
|
586
|
+
positions[nodes[i]] = posArray[i];
|
|
448
587
|
}
|
|
449
|
-
return
|
|
588
|
+
return rescaleLayout(positions);
|
|
589
|
+
}
|
|
590
|
+
function getNodeDegree(graph, node) {
|
|
591
|
+
return graph.edges().filter((edge) => edge[0] === node || edge[1] === node).length;
|
|
450
592
|
}
|
|
451
593
|
function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
452
594
|
const processed = _processParams(G, center, dim);
|
|
453
|
-
|
|
595
|
+
const graph = processed.G;
|
|
454
596
|
center = processed.center;
|
|
455
597
|
const nodes = getNodesFromGraph(graph);
|
|
456
598
|
const edges = getEdgesFromGraph(graph);
|
|
@@ -514,7 +656,9 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
|
|
|
514
656
|
}
|
|
515
657
|
}
|
|
516
658
|
for (const node of nodes) {
|
|
517
|
-
if (fixedNodes.has(node))
|
|
659
|
+
if (fixedNodes.has(node)) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
518
662
|
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
|
|
519
663
|
const limitedMagnitude = Math.min(magnitude, t);
|
|
520
664
|
for (let j = 0; j < dim; j++) {
|
|
@@ -529,9 +673,6 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
|
|
|
529
673
|
}
|
|
530
674
|
return positions;
|
|
531
675
|
}
|
|
532
|
-
function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
533
|
-
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
534
|
-
}
|
|
535
676
|
function _lbfgsDirection(grad, sList, yList, m) {
|
|
536
677
|
if (sList.length === 0) {
|
|
537
678
|
return grad.map((g) => -g);
|
|
@@ -622,10 +763,8 @@ function _computeShortestPathDistances(G, weight) {
|
|
|
622
763
|
function _kamadaKawaiSolve(distMatrix, positions, dim) {
|
|
623
764
|
const nNodes = positions.length;
|
|
624
765
|
const meanWeight = 1e-3;
|
|
625
|
-
const invDistMatrix = distMatrix.map(
|
|
626
|
-
|
|
627
|
-
);
|
|
628
|
-
let posVec = positions.flat();
|
|
766
|
+
const invDistMatrix = distMatrix.map((row) => row.map((d) => d === 0 ? 0 : 1 / (d + 1e-3)));
|
|
767
|
+
const posVec = positions.flat();
|
|
629
768
|
const maxIter = 500;
|
|
630
769
|
const gtol = 1e-5;
|
|
631
770
|
const m = 10;
|
|
@@ -710,443 +849,296 @@ function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
|
|
|
710
849
|
}
|
|
711
850
|
return [cost, grad];
|
|
712
851
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const nodes = getNodesFromGraph(graph);
|
|
718
|
-
if (nodes.length === 0) {
|
|
719
|
-
return {};
|
|
720
|
-
}
|
|
721
|
-
if (nodes.length === 1) {
|
|
722
|
-
return { [nodes[0]]: center };
|
|
723
|
-
}
|
|
724
|
-
if (!dist) {
|
|
725
|
-
if (Array.isArray(graph)) {
|
|
726
|
-
throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes");
|
|
727
|
-
}
|
|
728
|
-
dist = _computeShortestPathDistances(graph, weight);
|
|
729
|
-
}
|
|
730
|
-
const nodesArray = Array.from(nodes);
|
|
731
|
-
const nNodes = nodesArray.length;
|
|
732
|
-
const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
|
|
733
|
-
for (let i = 0; i < nNodes; i++) {
|
|
734
|
-
const nodeI = nodesArray[i];
|
|
735
|
-
distMatrix[i][i] = 0;
|
|
736
|
-
if (!dist[nodeI]) continue;
|
|
737
|
-
for (let j = 0; j < nNodes; j++) {
|
|
738
|
-
const nodeJ = nodesArray[j];
|
|
739
|
-
if (dist[nodeI][nodeJ] !== void 0) {
|
|
740
|
-
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (!pos) {
|
|
745
|
-
if (dim >= 2) {
|
|
746
|
-
pos = circularLayout(G, 1, center, dim);
|
|
747
|
-
} else {
|
|
748
|
-
const posArray2 = {};
|
|
749
|
-
nodesArray.forEach((node, i) => {
|
|
750
|
-
posArray2[node] = [i / (nNodes - 1 || 1)];
|
|
751
|
-
});
|
|
752
|
-
pos = posArray2;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
const posArray = new Array(nNodes);
|
|
756
|
-
for (let i = 0; i < nNodes; i++) {
|
|
757
|
-
const node = nodesArray[i];
|
|
758
|
-
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
759
|
-
while (posArray[i].length < dim) {
|
|
760
|
-
posArray[i].push(0);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
764
|
-
const finalPos = {};
|
|
765
|
-
for (let i = 0; i < nNodes; i++) {
|
|
766
|
-
finalPos[nodesArray[i]] = newPositions[i];
|
|
767
|
-
}
|
|
768
|
-
return rescaleLayout(finalPos, scale, center);
|
|
769
|
-
}
|
|
770
|
-
function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1, scalingRatio = 2, gravity = 1, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) {
|
|
771
|
-
const processed = _processParams(G, null, dim);
|
|
772
|
-
const graph = processed.G;
|
|
773
|
-
const nodes = getNodesFromGraph(graph);
|
|
774
|
-
if (nodes.length === 0) {
|
|
775
|
-
return {};
|
|
776
|
-
}
|
|
777
|
-
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
778
|
-
let posArray;
|
|
779
|
-
if (pos === null) {
|
|
780
|
-
pos = {};
|
|
781
|
-
posArray = new Array(nodes.length);
|
|
782
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
783
|
-
posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
784
|
-
pos[nodes[i]] = posArray[i];
|
|
785
|
-
}
|
|
786
|
-
} else if (Object.keys(pos).length === nodes.length) {
|
|
787
|
-
posArray = new Array(nodes.length);
|
|
788
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
789
|
-
const nodePos = pos[nodes[i]];
|
|
790
|
-
posArray[i] = new Array(dim);
|
|
791
|
-
for (let d = 0; d < dim; d++) {
|
|
792
|
-
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} else {
|
|
796
|
-
let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
797
|
-
let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
798
|
-
for (const node in pos) {
|
|
799
|
-
const nodePos = pos[node];
|
|
800
|
-
for (let d = 0; d < dim; d++) {
|
|
801
|
-
if (d < nodePos.length) {
|
|
802
|
-
minPos[d] = Math.min(minPos[d], nodePos[d]);
|
|
803
|
-
maxPos[d] = Math.max(maxPos[d], nodePos[d]);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
for (let d = 0; d < dim; d++) {
|
|
808
|
-
if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
|
|
809
|
-
minPos[d] = -1;
|
|
810
|
-
maxPos[d] = 1;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
posArray = new Array(nodes.length);
|
|
814
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
815
|
-
const node = nodes[i];
|
|
816
|
-
if (pos[node]) {
|
|
817
|
-
const nodePos = pos[node];
|
|
818
|
-
posArray[i] = new Array(dim);
|
|
819
|
-
for (let d = 0; d < dim; d++) {
|
|
820
|
-
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
821
|
-
}
|
|
822
|
-
} else {
|
|
823
|
-
posArray[i] = Array(dim).fill(0).map(
|
|
824
|
-
(_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d])
|
|
825
|
-
);
|
|
826
|
-
pos[node] = posArray[i];
|
|
827
|
-
}
|
|
852
|
+
const np = {
|
|
853
|
+
zeros: function(shape) {
|
|
854
|
+
if (typeof shape === "number") {
|
|
855
|
+
return Array(shape).fill(0);
|
|
828
856
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const size = new Array(nodes.length).fill(0);
|
|
832
|
-
const adjustSizes = nodeSize !== null;
|
|
833
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
834
|
-
const node = nodes[i];
|
|
835
|
-
mass[i] = nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1;
|
|
836
|
-
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
837
|
-
}
|
|
838
|
-
const n = nodes.length;
|
|
839
|
-
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
840
|
-
const edges = Array.isArray(graph) ? [] : graph.edges();
|
|
841
|
-
const nodeIndices = {};
|
|
842
|
-
nodes.forEach((node, i) => {
|
|
843
|
-
nodeIndices[node] = i;
|
|
844
|
-
});
|
|
845
|
-
for (const [source, target] of edges) {
|
|
846
|
-
const i = nodeIndices[source];
|
|
847
|
-
const j = nodeIndices[target];
|
|
848
|
-
let edgeWeight = 1;
|
|
849
|
-
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
850
|
-
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
857
|
+
if (shape.length === 1) {
|
|
858
|
+
return Array(shape[0]).fill(0);
|
|
851
859
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
858
|
-
let speed = 1;
|
|
859
|
-
let speedEfficiency = 1;
|
|
860
|
-
function estimateFactor(n2, swing2, traction2, speed2, speedEfficiency2, jitterTolerance2) {
|
|
861
|
-
const optJitter = 0.05 * Math.sqrt(n2);
|
|
862
|
-
const minJitter = Math.sqrt(optJitter);
|
|
863
|
-
const maxJitter = 10;
|
|
864
|
-
const minSpeedEfficiency = 0.05;
|
|
865
|
-
const other = Math.min(maxJitter, optJitter * traction2 / (n2 * n2));
|
|
866
|
-
let jitter = jitterTolerance2 * Math.max(minJitter, other);
|
|
867
|
-
if (swing2 / traction2 > 2) {
|
|
868
|
-
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
869
|
-
speedEfficiency2 *= 0.5;
|
|
870
|
-
}
|
|
871
|
-
jitter = Math.max(jitter, jitterTolerance2);
|
|
860
|
+
return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
|
|
861
|
+
},
|
|
862
|
+
ones: function(shape) {
|
|
863
|
+
if (typeof shape === "number") {
|
|
864
|
+
return Array(shape).fill(1);
|
|
872
865
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
876
|
-
speedEfficiency2 *= 0.7;
|
|
877
|
-
}
|
|
878
|
-
} else if (speed2 < 1e3) {
|
|
879
|
-
speedEfficiency2 *= 1.3;
|
|
866
|
+
if (shape.length === 1) {
|
|
867
|
+
return Array(shape[0]).fill(1);
|
|
880
868
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
for (let i = 0; i < n; i++) {
|
|
887
|
-
for (let d = 0; d < dim; d++) {
|
|
888
|
-
attraction[i][d] = 0;
|
|
889
|
-
repulsion[i][d] = 0;
|
|
890
|
-
gravities[i][d] = 0;
|
|
891
|
-
}
|
|
869
|
+
return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
|
|
870
|
+
},
|
|
871
|
+
linspace: function(start, stop, num) {
|
|
872
|
+
if (num === 1) {
|
|
873
|
+
return [start];
|
|
892
874
|
}
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
904
|
-
if (distance[i][j] < 0.01) distance[i][j] = 0.01;
|
|
905
|
-
}
|
|
875
|
+
const step = (stop - start) / (num - 1);
|
|
876
|
+
return Array.from({ length: num }, (_, i) => start + i * step);
|
|
877
|
+
},
|
|
878
|
+
array: function(arr) {
|
|
879
|
+
return Array.isArray(arr) ? [...arr] : [arr];
|
|
880
|
+
},
|
|
881
|
+
repeat: function(a, repeats) {
|
|
882
|
+
const result = [];
|
|
883
|
+
for (let i = 0; i < repeats; i++) {
|
|
884
|
+
result.push(...np.array(a));
|
|
906
885
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const force = factor * diff[i][j][d];
|
|
915
|
-
attraction[i][d] += force;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
} else {
|
|
920
|
-
for (let i = 0; i < n; i++) {
|
|
921
|
-
for (let j = 0; j < n; j++) {
|
|
922
|
-
if (i === j || A[i][j] === 0) continue;
|
|
923
|
-
for (let d = 0; d < dim; d++) {
|
|
924
|
-
const force = -diff[i][j][d] * A[i][j];
|
|
925
|
-
attraction[i][d] += force;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
886
|
+
return result;
|
|
887
|
+
},
|
|
888
|
+
mean: function(arr, axis = null) {
|
|
889
|
+
if (axis === null) {
|
|
890
|
+
const flatArr = Array.isArray(arr[0]) ? arr.flat(Infinity) : arr;
|
|
891
|
+
const sum = flatArr.reduce((a, b) => a + b, 0);
|
|
892
|
+
return sum / flatArr.length;
|
|
929
893
|
}
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
894
|
+
if (axis === 0) {
|
|
895
|
+
const result = [];
|
|
896
|
+
const matrix = arr;
|
|
897
|
+
for (let i = 0; i < matrix[0].length; i++) {
|
|
898
|
+
let sum = 0;
|
|
899
|
+
for (let j = 0; j < matrix.length; j++) {
|
|
900
|
+
sum += matrix[j][i];
|
|
934
901
|
}
|
|
902
|
+
result.push(sum / matrix.length);
|
|
935
903
|
}
|
|
904
|
+
return result;
|
|
936
905
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
dist -= size[i] - size[j];
|
|
943
|
-
dist = Math.max(dist, 0.01);
|
|
944
|
-
}
|
|
945
|
-
const distSquared = dist * dist;
|
|
946
|
-
const massProduct = mass[i] * mass[j];
|
|
947
|
-
const factor = massProduct / distSquared * scalingRatio;
|
|
948
|
-
for (let d = 0; d < dim; d++) {
|
|
949
|
-
const direction = diff[i][j][d] / dist;
|
|
950
|
-
repulsion[i][d] += direction * factor;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
906
|
+
return arr.map((row) => np.mean(row));
|
|
907
|
+
},
|
|
908
|
+
add: function(a, b) {
|
|
909
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
910
|
+
return a + b;
|
|
953
911
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
for (let d = 0; d < dim; d++) {
|
|
957
|
-
centerOfMass[d] += posArray[i][d] / n;
|
|
958
|
-
}
|
|
912
|
+
if (!Array.isArray(a)) {
|
|
913
|
+
return b.map((val) => a + val);
|
|
959
914
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
for (let d = 0; d < dim; d++) {
|
|
963
|
-
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
964
|
-
}
|
|
965
|
-
if (strongGravity) {
|
|
966
|
-
for (let d = 0; d < dim; d++) {
|
|
967
|
-
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
968
|
-
}
|
|
969
|
-
} else {
|
|
970
|
-
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
971
|
-
if (dist > 0.01) {
|
|
972
|
-
for (let d = 0; d < dim; d++) {
|
|
973
|
-
const direction = posCentered[d] / dist;
|
|
974
|
-
gravities[i][d] = -gravity * mass[i] * direction;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
915
|
+
if (!Array.isArray(b)) {
|
|
916
|
+
return a.map((val) => val + b);
|
|
978
917
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
985
|
-
}
|
|
986
|
-
const oldPos = [...posArray[i]];
|
|
987
|
-
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
988
|
-
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
989
|
-
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
990
|
-
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
991
|
-
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
992
|
-
totalSwing += mass[i] * swingMagnitude;
|
|
993
|
-
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
918
|
+
return a.map((val, i) => val + b[i]);
|
|
919
|
+
},
|
|
920
|
+
subtract: function(a, b) {
|
|
921
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
922
|
+
return a - b;
|
|
994
923
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
924
|
+
if (!Array.isArray(a)) {
|
|
925
|
+
return b.map((val) => a - val);
|
|
926
|
+
}
|
|
927
|
+
if (!Array.isArray(b)) {
|
|
928
|
+
return a.map((val) => val - b);
|
|
929
|
+
}
|
|
930
|
+
return a.map((val, i) => val - b[i]);
|
|
931
|
+
},
|
|
932
|
+
max: function(arr) {
|
|
933
|
+
if (!Array.isArray(arr)) {
|
|
934
|
+
return arr;
|
|
935
|
+
}
|
|
936
|
+
return Math.max(...arr.flat(Infinity));
|
|
937
|
+
},
|
|
938
|
+
min: function(arr) {
|
|
939
|
+
if (!Array.isArray(arr)) {
|
|
940
|
+
return arr;
|
|
941
|
+
}
|
|
942
|
+
return Math.min(...arr.flat(Infinity));
|
|
943
|
+
},
|
|
944
|
+
norm: function(arr) {
|
|
945
|
+
return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
function circularLayout(G, scale = 1, center = null, dim = 2) {
|
|
949
|
+
if (dim < 2) {
|
|
950
|
+
throw new Error("cannot handle dimensions < 2");
|
|
951
|
+
}
|
|
952
|
+
const processed = _processParams(G, center, dim);
|
|
953
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
954
|
+
center = processed.center;
|
|
955
|
+
const pos = {};
|
|
956
|
+
if (nodes.length === 0) {
|
|
957
|
+
return pos;
|
|
958
|
+
}
|
|
959
|
+
if (nodes.length === 1) {
|
|
960
|
+
pos[nodes[0]] = center;
|
|
961
|
+
return pos;
|
|
962
|
+
}
|
|
963
|
+
if (dim === 2) {
|
|
964
|
+
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
965
|
+
nodes.forEach((node, i) => {
|
|
966
|
+
const x = Math.cos(theta[i]) * scale + center[0];
|
|
967
|
+
const y = Math.sin(theta[i]) * scale + center[1];
|
|
968
|
+
pos[node] = [x, y];
|
|
969
|
+
});
|
|
970
|
+
} else if (dim === 3) {
|
|
971
|
+
const n = nodes.length;
|
|
972
|
+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
973
|
+
nodes.forEach((node, i) => {
|
|
974
|
+
const theta = 2 * Math.PI * i / goldenRatio;
|
|
975
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
976
|
+
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
977
|
+
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
978
|
+
const z = Math.cos(phi) * scale + center[2];
|
|
979
|
+
pos[node] = [x, y, z];
|
|
980
|
+
});
|
|
981
|
+
} else {
|
|
982
|
+
const rng = new RandomNumberGenerator();
|
|
983
|
+
nodes.forEach((node) => {
|
|
984
|
+
const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
985
|
+
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
986
|
+
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
return pos;
|
|
990
|
+
}
|
|
991
|
+
function kamadaKawaiLayout(G, dist = null, pos = null, weight = "weight", scale = 1, center = null, dim = 2) {
|
|
992
|
+
const processed = _processParams(G, center, dim);
|
|
993
|
+
const graph = processed.G;
|
|
994
|
+
center = processed.center;
|
|
995
|
+
const nodes = getNodesFromGraph(graph);
|
|
996
|
+
if (nodes.length === 0) {
|
|
997
|
+
return {};
|
|
998
|
+
}
|
|
999
|
+
if (nodes.length === 1) {
|
|
1000
|
+
return { [nodes[0]]: center };
|
|
1001
|
+
}
|
|
1002
|
+
if (!dist) {
|
|
1003
|
+
if (Array.isArray(graph)) {
|
|
1004
|
+
throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes");
|
|
1005
|
+
}
|
|
1006
|
+
dist = _computeShortestPathDistances(graph, weight);
|
|
1007
|
+
}
|
|
1008
|
+
const nodesArray = Array.from(nodes);
|
|
1009
|
+
const nNodes = nodesArray.length;
|
|
1010
|
+
const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
|
|
1011
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1012
|
+
const nodeI = nodesArray[i];
|
|
1013
|
+
distMatrix[i][i] = 0;
|
|
1014
|
+
if (!dist[nodeI]) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
for (let j = 0; j < nNodes; j++) {
|
|
1018
|
+
const nodeJ = nodesArray[j];
|
|
1019
|
+
if (dist[nodeI][nodeJ] !== void 0) {
|
|
1020
|
+
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
1019
1021
|
}
|
|
1020
1022
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
+
}
|
|
1024
|
+
if (!pos) {
|
|
1025
|
+
if (dim >= 2) {
|
|
1026
|
+
pos = circularLayout(G, 1, center, dim);
|
|
1027
|
+
} else {
|
|
1028
|
+
const posArray2 = {};
|
|
1029
|
+
nodesArray.forEach((node, i) => {
|
|
1030
|
+
posArray2[node] = [i / (nNodes - 1 || 1)];
|
|
1031
|
+
});
|
|
1032
|
+
pos = posArray2;
|
|
1023
1033
|
}
|
|
1024
1034
|
}
|
|
1025
|
-
const
|
|
1026
|
-
for (let i = 0; i <
|
|
1027
|
-
|
|
1035
|
+
const posArray = new Array(nNodes);
|
|
1036
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1037
|
+
const node = nodesArray[i];
|
|
1038
|
+
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
1039
|
+
while (posArray[i].length < dim) {
|
|
1040
|
+
posArray[i].push(0);
|
|
1041
|
+
}
|
|
1028
1042
|
}
|
|
1029
|
-
|
|
1043
|
+
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
1044
|
+
const finalPos = {};
|
|
1045
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1046
|
+
finalPos[nodesArray[i]] = newPositions[i];
|
|
1047
|
+
}
|
|
1048
|
+
return rescaleLayout(finalPos, scale, center);
|
|
1030
1049
|
}
|
|
1031
|
-
function
|
|
1032
|
-
return
|
|
1033
|
-
(edge) => edge[0] === node || edge[1] === node
|
|
1034
|
-
).length;
|
|
1050
|
+
function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
1051
|
+
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
1035
1052
|
}
|
|
1036
|
-
function
|
|
1037
|
-
if (
|
|
1038
|
-
throw new Error("
|
|
1053
|
+
function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
|
|
1054
|
+
if (dim !== 2) {
|
|
1055
|
+
throw new Error("can only handle 2 dimensions");
|
|
1039
1056
|
}
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1057
|
+
const processed = _processParams(G, center, dim);
|
|
1058
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
1059
|
+
center = processed.center;
|
|
1060
|
+
const pos = {};
|
|
1042
1061
|
if (nodes.length === 0) {
|
|
1043
|
-
return
|
|
1062
|
+
return pos;
|
|
1044
1063
|
}
|
|
1045
|
-
if (
|
|
1046
|
-
pos =
|
|
1047
|
-
|
|
1048
|
-
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
1049
|
-
const defaultPos = {};
|
|
1050
|
-
nodes.forEach((node) => {
|
|
1051
|
-
if (!pos[node]) {
|
|
1052
|
-
defaultPos[node] = [rng.rand(), rng.rand()];
|
|
1053
|
-
}
|
|
1054
|
-
});
|
|
1055
|
-
pos = { ...pos, ...defaultPos };
|
|
1064
|
+
if (nodes.length === 1) {
|
|
1065
|
+
pos[nodes[0]] = center;
|
|
1066
|
+
return pos;
|
|
1056
1067
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
nodeIndex[node] = i;
|
|
1060
|
-
});
|
|
1061
|
-
const positions = nodes.map((node) => [...pos[node]]);
|
|
1062
|
-
const N = nodes.length;
|
|
1063
|
-
const K = Array(N).fill(0).map(() => Array(N).fill(1));
|
|
1064
|
-
for (let i = 0; i < N; i++) {
|
|
1065
|
-
K[i][i] = 0;
|
|
1068
|
+
if (!nlist) {
|
|
1069
|
+
nlist = [nodes];
|
|
1066
1070
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1071
|
+
const radiusBump = scale / nlist.length;
|
|
1072
|
+
let radius;
|
|
1073
|
+
if (nlist[0].length === 1) {
|
|
1074
|
+
radius = 0;
|
|
1075
|
+
pos[nlist[0][0]] = [...center];
|
|
1076
|
+
radius += radiusBump;
|
|
1077
|
+
} else {
|
|
1078
|
+
radius = radiusBump;
|
|
1073
1079
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
let nIter = 0;
|
|
1079
|
-
while (error > etol && nIter < maxIter) {
|
|
1080
|
-
const change = Array(N).fill(0).map(() => [0, 0]);
|
|
1081
|
-
for (let i = 0; i < N; i++) {
|
|
1082
|
-
for (let j = 0; j < N; j++) {
|
|
1083
|
-
if (i === j) continue;
|
|
1084
|
-
const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
|
|
1085
|
-
const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
|
|
1086
|
-
for (let d = 0; d < diff.length; d++) {
|
|
1087
|
-
change[i][d] += K[i][j] * diff[d] - rho / dist * diff[d];
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1080
|
+
for (let i = 0; i < nlist.length; i++) {
|
|
1081
|
+
const shell = nlist[i];
|
|
1082
|
+
if (shell.length === 0) {
|
|
1083
|
+
continue;
|
|
1090
1084
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
positions[i][d] += change[i][d] * dt;
|
|
1094
|
-
}
|
|
1085
|
+
if (shell.length === 1 && i === 0) {
|
|
1086
|
+
continue;
|
|
1095
1087
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1088
|
+
const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
|
|
1089
|
+
shell.forEach((node, j) => {
|
|
1090
|
+
const x = Math.cos(theta[j]) * radius + center[0];
|
|
1091
|
+
const y = Math.sin(theta[j]) * radius + center[1];
|
|
1092
|
+
pos[node] = [x, y];
|
|
1093
|
+
});
|
|
1094
|
+
radius += radiusBump;
|
|
1098
1095
|
}
|
|
1099
|
-
|
|
1100
|
-
nodes.forEach((node, i) => {
|
|
1101
|
-
finalPos[node] = positions[i];
|
|
1102
|
-
});
|
|
1103
|
-
return finalPos;
|
|
1096
|
+
return pos;
|
|
1104
1097
|
}
|
|
1105
|
-
function
|
|
1106
|
-
if (
|
|
1107
|
-
throw new Error("
|
|
1098
|
+
function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
|
|
1099
|
+
if (dim !== 2) {
|
|
1100
|
+
throw new Error("can only handle 2 dimensions");
|
|
1108
1101
|
}
|
|
1109
|
-
const processed = _processParams(G, center || [0, 0],
|
|
1110
|
-
const
|
|
1102
|
+
const processed = _processParams(G, center || [0, 0], dim);
|
|
1103
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
1111
1104
|
center = processed.center;
|
|
1112
|
-
const allNodes = getNodesFromGraph(graph);
|
|
1113
|
-
if (allNodes.length === 0) {
|
|
1114
|
-
return {};
|
|
1115
|
-
}
|
|
1116
|
-
if (!nodes) {
|
|
1117
|
-
nodes = allNodes.filter((_, i) => i % 2 === 0);
|
|
1118
|
-
}
|
|
1119
|
-
const left = new Set(nodes);
|
|
1120
|
-
const right = new Set(allNodes.filter((n) => !left.has(n)));
|
|
1121
|
-
const height = 1;
|
|
1122
|
-
const width = aspectRatio * height;
|
|
1123
|
-
const offset = [width / 2, height / 2];
|
|
1124
1105
|
const pos = {};
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const x = 0;
|
|
1128
|
-
const y = i * height / (leftNodes.length || 1);
|
|
1129
|
-
pos[node] = [x, y];
|
|
1130
|
-
});
|
|
1131
|
-
const rightNodes = [...right];
|
|
1132
|
-
rightNodes.forEach((node, i) => {
|
|
1133
|
-
const x = width;
|
|
1134
|
-
const y = i * height / (rightNodes.length || 1);
|
|
1135
|
-
pos[node] = [x, y];
|
|
1136
|
-
});
|
|
1137
|
-
for (const node in pos) {
|
|
1138
|
-
pos[node][0] -= offset[0];
|
|
1139
|
-
pos[node][1] -= offset[1];
|
|
1106
|
+
if (nodes.length === 0) {
|
|
1107
|
+
return pos;
|
|
1140
1108
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1109
|
+
if (nodes.length === 1) {
|
|
1110
|
+
pos[nodes[0]] = [...center];
|
|
1111
|
+
return pos;
|
|
1112
|
+
}
|
|
1113
|
+
let positions = [];
|
|
1114
|
+
if (equidistant) {
|
|
1115
|
+
const chord = 1;
|
|
1116
|
+
const step = 0.5;
|
|
1117
|
+
let theta = resolution;
|
|
1118
|
+
theta += chord / (step * theta);
|
|
1119
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1120
|
+
const r = step * theta;
|
|
1121
|
+
theta += chord / r;
|
|
1122
|
+
positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
|
|
1147
1123
|
}
|
|
1124
|
+
} else {
|
|
1125
|
+
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
1126
|
+
const angle = dist.map((d) => resolution * d);
|
|
1127
|
+
positions = dist.map((d, i) => [Math.cos(angle[i]) * d, Math.sin(angle[i]) * d]);
|
|
1148
1128
|
}
|
|
1149
|
-
|
|
1129
|
+
const posArray = [];
|
|
1130
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1131
|
+
posArray.push(positions[i]);
|
|
1132
|
+
}
|
|
1133
|
+
const scaledPositions = rescaleLayout(posArray, scale);
|
|
1134
|
+
for (let i = 0; i < scaledPositions.length; i++) {
|
|
1135
|
+
scaledPositions[i][0] += center[0];
|
|
1136
|
+
scaledPositions[i][1] += center[1];
|
|
1137
|
+
}
|
|
1138
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1139
|
+
pos[nodes[i]] = scaledPositions[i];
|
|
1140
|
+
}
|
|
1141
|
+
return pos;
|
|
1150
1142
|
}
|
|
1151
1143
|
function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale = 1, center = null) {
|
|
1152
1144
|
if (align !== "vertical" && align !== "horizontal") {
|
|
@@ -1218,161 +1210,66 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
|
|
|
1218
1210
|
if (!visited.has(neighbor)) {
|
|
1219
1211
|
nextLayer.push(neighbor);
|
|
1220
1212
|
visited.add(neighbor);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (nextLayer.length === 0) {
|
|
1225
|
-
const unvisited = allNodes.filter((node) => !visited.has(node));
|
|
1226
|
-
if (unvisited.length > 0) {
|
|
1227
|
-
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
1228
|
-
}
|
|
1229
|
-
break;
|
|
1230
|
-
}
|
|
1231
|
-
currentLayer++;
|
|
1232
|
-
layers[currentLayer] = nextLayer;
|
|
1233
|
-
}
|
|
1234
|
-
return multipartiteLayout(graph, layers, align, scale, center);
|
|
1235
|
-
}
|
|
1236
|
-
function spectralLayout(G, scale = 1, center = null, dim = 2) {
|
|
1237
|
-
const processed = _processParams(G, center, dim);
|
|
1238
|
-
const graph = processed.G;
|
|
1239
|
-
center = processed.center;
|
|
1240
|
-
const nodes = getNodesFromGraph(graph);
|
|
1241
|
-
if (nodes.length <= 2) {
|
|
1242
|
-
if (nodes.length === 0) {
|
|
1243
|
-
return {};
|
|
1244
|
-
} else if (nodes.length === 1) {
|
|
1245
|
-
return { [nodes[0]]: center };
|
|
1246
|
-
} else {
|
|
1247
|
-
return {
|
|
1248
|
-
[nodes[0]]: center.map((v) => v - scale),
|
|
1249
|
-
[nodes[1]]: center.map((v) => v + scale)
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
const N = nodes.length;
|
|
1254
|
-
const nodeIndices = {};
|
|
1255
|
-
nodes.forEach((node, i) => {
|
|
1256
|
-
nodeIndices[node] = i;
|
|
1257
|
-
});
|
|
1258
|
-
const A = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1259
|
-
const edges = getEdgesFromGraph(graph);
|
|
1260
|
-
for (const [source, target] of edges) {
|
|
1261
|
-
const i = nodeIndices[source];
|
|
1262
|
-
const j = nodeIndices[target];
|
|
1263
|
-
A[i][j] = 1;
|
|
1264
|
-
A[j][i] = 1;
|
|
1265
|
-
}
|
|
1266
|
-
const L = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1267
|
-
for (let i = 0; i < N; i++) {
|
|
1268
|
-
L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
|
|
1269
|
-
for (let j = 0; j < N; j++) {
|
|
1270
|
-
L[i][j] -= A[i][j];
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
const eigenvectors = [];
|
|
1274
|
-
for (let d = 0; d < dim; d++) {
|
|
1275
|
-
let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
|
|
1276
|
-
for (const ev of eigenvectors) {
|
|
1277
|
-
const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
|
|
1278
|
-
vector = vector.map((val, idx) => val - dot * ev[idx]);
|
|
1279
|
-
}
|
|
1280
|
-
const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
|
|
1281
|
-
vector = vector.map((val) => val / norm);
|
|
1282
|
-
for (let iter = 0; iter < 100; iter++) {
|
|
1283
|
-
const newVec = Array(N).fill(0);
|
|
1284
|
-
for (let i = 0; i < N; i++) {
|
|
1285
|
-
for (let j = 0; j < N; j++) {
|
|
1286
|
-
newVec[i] += L[i][j] * vector[j];
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
|
|
1290
|
-
newVec.forEach((val, idx, arr) => {
|
|
1291
|
-
arr[idx] = val - mean;
|
|
1292
|
-
});
|
|
1293
|
-
const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
|
|
1294
|
-
if (newNorm < 1e-10) continue;
|
|
1295
|
-
vector = newVec.map((val) => val / newNorm);
|
|
1296
|
-
}
|
|
1297
|
-
eigenvectors.push(vector);
|
|
1298
|
-
}
|
|
1299
|
-
const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
|
|
1300
|
-
for (let i = 0; i < N; i++) {
|
|
1301
|
-
for (let d = 0; d < dim; d++) {
|
|
1302
|
-
positions[i][d] = eigenvectors[d][i];
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
const scaledPositions = rescaleLayout(positions, scale);
|
|
1306
|
-
const pos = {};
|
|
1307
|
-
nodes.forEach((node, i) => {
|
|
1308
|
-
pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
|
|
1309
|
-
});
|
|
1310
|
-
return pos;
|
|
1311
|
-
}
|
|
1312
|
-
function isK5(nodes, edges) {
|
|
1313
|
-
if (nodes.length !== 5) return false;
|
|
1314
|
-
if (edges.length !== 10) return false;
|
|
1315
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1316
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
1317
|
-
const hasEdge = edges.some(
|
|
1318
|
-
(e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
|
|
1319
|
-
);
|
|
1320
|
-
if (!hasEdge) return false;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
return true;
|
|
1324
|
-
}
|
|
1325
|
-
function isK33(nodes, edges) {
|
|
1326
|
-
if (nodes.length !== 6) return false;
|
|
1327
|
-
if (edges.length !== 9) return false;
|
|
1328
|
-
const nodePartitions = tryFindBipartitePartition(nodes, edges);
|
|
1329
|
-
if (!nodePartitions) return false;
|
|
1330
|
-
const [part1, part2] = nodePartitions;
|
|
1331
|
-
if (part1.length !== 3 || part2.length !== 3) return false;
|
|
1332
|
-
for (const n1 of part1) {
|
|
1333
|
-
for (const n2 of part2) {
|
|
1334
|
-
const hasEdge = edges.some(
|
|
1335
|
-
(e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1
|
|
1336
|
-
);
|
|
1337
|
-
if (!hasEdge) return false;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
return true;
|
|
1341
|
-
}
|
|
1342
|
-
function tryFindBipartitePartition(nodes, edges) {
|
|
1343
|
-
const colorMap = {};
|
|
1344
|
-
const adjList = {};
|
|
1345
|
-
for (const node of nodes) {
|
|
1346
|
-
adjList[node] = [];
|
|
1347
|
-
}
|
|
1348
|
-
for (const [u, v] of edges) {
|
|
1349
|
-
adjList[u].push(v);
|
|
1350
|
-
adjList[v].push(u);
|
|
1351
|
-
}
|
|
1352
|
-
const queue = [nodes[0]];
|
|
1353
|
-
colorMap[nodes[0]] = 0;
|
|
1354
|
-
while (queue.length > 0) {
|
|
1355
|
-
const node = queue.shift();
|
|
1356
|
-
const nodeColor = colorMap[node];
|
|
1357
|
-
for (const neighbor of adjList[node]) {
|
|
1358
|
-
if (colorMap[neighbor] === void 0) {
|
|
1359
|
-
colorMap[neighbor] = 1 - nodeColor;
|
|
1360
|
-
queue.push(neighbor);
|
|
1361
|
-
} else if (colorMap[neighbor] === nodeColor) {
|
|
1362
|
-
return null;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (nextLayer.length === 0) {
|
|
1217
|
+
const unvisited = allNodes.filter((node) => !visited.has(node));
|
|
1218
|
+
if (unvisited.length > 0) {
|
|
1219
|
+
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
1363
1220
|
}
|
|
1221
|
+
break;
|
|
1364
1222
|
}
|
|
1223
|
+
currentLayer++;
|
|
1224
|
+
layers[currentLayer] = nextLayer;
|
|
1365
1225
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1226
|
+
return multipartiteLayout(graph, layers, align, scale, center);
|
|
1227
|
+
}
|
|
1228
|
+
function bipartiteLayout(G, nodes = null, align = "vertical", scale = 1, center = null, aspectRatio = 4 / 3) {
|
|
1229
|
+
if (align !== "vertical" && align !== "horizontal") {
|
|
1230
|
+
throw new Error("align must be either vertical or horizontal");
|
|
1231
|
+
}
|
|
1232
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
1233
|
+
const graph = processed.G;
|
|
1234
|
+
center = processed.center;
|
|
1235
|
+
const allNodes = getNodesFromGraph(graph);
|
|
1236
|
+
if (allNodes.length === 0) {
|
|
1237
|
+
return {};
|
|
1238
|
+
}
|
|
1239
|
+
if (!nodes) {
|
|
1240
|
+
nodes = allNodes.filter((_, i) => i % 2 === 0);
|
|
1241
|
+
}
|
|
1242
|
+
const left = new Set(nodes);
|
|
1243
|
+
const right = new Set(allNodes.filter((n) => !left.has(n)));
|
|
1244
|
+
const height = 1;
|
|
1245
|
+
const width = aspectRatio * height;
|
|
1246
|
+
const offset = [width / 2, height / 2];
|
|
1247
|
+
const pos = {};
|
|
1248
|
+
const leftNodes = [...left];
|
|
1249
|
+
leftNodes.forEach((node, i) => {
|
|
1250
|
+
const x = 0;
|
|
1251
|
+
const y = i * height / (leftNodes.length || 1);
|
|
1252
|
+
pos[node] = [x, y];
|
|
1253
|
+
});
|
|
1254
|
+
const rightNodes = [...right];
|
|
1255
|
+
rightNodes.forEach((node, i) => {
|
|
1256
|
+
const x = width;
|
|
1257
|
+
const y = i * height / (rightNodes.length || 1);
|
|
1258
|
+
pos[node] = [x, y];
|
|
1259
|
+
});
|
|
1260
|
+
for (const node in pos) {
|
|
1261
|
+
pos[node][0] -= offset[0];
|
|
1262
|
+
pos[node][1] -= offset[1];
|
|
1263
|
+
}
|
|
1264
|
+
const scaledPos = rescaleLayout(pos, scale, center);
|
|
1265
|
+
if (align === "horizontal") {
|
|
1266
|
+
for (const node in scaledPos) {
|
|
1267
|
+
const temp = scaledPos[node][0];
|
|
1268
|
+
scaledPos[node][0] = scaledPos[node][1];
|
|
1269
|
+
scaledPos[node][1] = temp;
|
|
1373
1270
|
}
|
|
1374
1271
|
}
|
|
1375
|
-
return
|
|
1272
|
+
return scaledPos;
|
|
1376
1273
|
}
|
|
1377
1274
|
function createTriangulationEmbedding(nodes, edges, seed = null) {
|
|
1378
1275
|
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
@@ -1426,8 +1323,12 @@ function createTriangulationEmbedding(nodes, edges, seed = null) {
|
|
|
1426
1323
|
return embedding;
|
|
1427
1324
|
}
|
|
1428
1325
|
function findCycle(nodes, edges, adjMap) {
|
|
1429
|
-
if (nodes.length === 0)
|
|
1430
|
-
|
|
1326
|
+
if (nodes.length === 0) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
if (nodes.length <= 2) {
|
|
1330
|
+
return nodes;
|
|
1331
|
+
}
|
|
1431
1332
|
if (nodes.length <= 8) {
|
|
1432
1333
|
let hamiltonianCycleDFS = function(node) {
|
|
1433
1334
|
path.push(node);
|
|
@@ -1463,7 +1364,9 @@ function findCycle(nodes, edges, adjMap) {
|
|
|
1463
1364
|
function findCycleDFS(node, parentNode) {
|
|
1464
1365
|
visited.add(node);
|
|
1465
1366
|
for (const neighbor of adjMap[node]) {
|
|
1466
|
-
if (neighbor === parentNode)
|
|
1367
|
+
if (neighbor === parentNode) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1467
1370
|
if (visited.has(neighbor)) {
|
|
1468
1371
|
cycleFound = constructCycle(node, neighbor, parent);
|
|
1469
1372
|
return true;
|
|
@@ -1535,6 +1438,85 @@ function lrPlanarityTest(nodes, edges, seed = null) {
|
|
|
1535
1438
|
const embedding = createTriangulationEmbedding(nodes, edges, seed);
|
|
1536
1439
|
return { isPlanar: true, embedding };
|
|
1537
1440
|
}
|
|
1441
|
+
function isK5(nodes, edges) {
|
|
1442
|
+
if (nodes.length !== 5) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
if (edges.length !== 10) {
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1449
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1450
|
+
const hasEdge = edges.some(
|
|
1451
|
+
(e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
|
|
1452
|
+
);
|
|
1453
|
+
if (!hasEdge) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
function isK33(nodes, edges) {
|
|
1461
|
+
if (nodes.length !== 6) {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
if (edges.length !== 9) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
const nodePartitions = tryFindBipartitePartition(nodes, edges);
|
|
1468
|
+
if (!nodePartitions) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
const [part1, part2] = nodePartitions;
|
|
1472
|
+
if (part1.length !== 3 || part2.length !== 3) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
for (const n1 of part1) {
|
|
1476
|
+
for (const n2 of part2) {
|
|
1477
|
+
const hasEdge = edges.some((e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1);
|
|
1478
|
+
if (!hasEdge) {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
function tryFindBipartitePartition(nodes, edges) {
|
|
1486
|
+
const colorMap = {};
|
|
1487
|
+
const adjList = {};
|
|
1488
|
+
for (const node of nodes) {
|
|
1489
|
+
adjList[node] = [];
|
|
1490
|
+
}
|
|
1491
|
+
for (const [u, v] of edges) {
|
|
1492
|
+
adjList[u].push(v);
|
|
1493
|
+
adjList[v].push(u);
|
|
1494
|
+
}
|
|
1495
|
+
const queue = [nodes[0]];
|
|
1496
|
+
colorMap[nodes[0]] = 0;
|
|
1497
|
+
while (queue.length > 0) {
|
|
1498
|
+
const node = queue.shift();
|
|
1499
|
+
const nodeColor = colorMap[node];
|
|
1500
|
+
for (const neighbor of adjList[node]) {
|
|
1501
|
+
if (colorMap[neighbor] === void 0) {
|
|
1502
|
+
colorMap[neighbor] = 1 - nodeColor;
|
|
1503
|
+
queue.push(neighbor);
|
|
1504
|
+
} else if (colorMap[neighbor] === nodeColor) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const part0 = [];
|
|
1510
|
+
const part1 = [];
|
|
1511
|
+
for (const node of nodes) {
|
|
1512
|
+
if (colorMap[node] === 0) {
|
|
1513
|
+
part0.push(node);
|
|
1514
|
+
} else {
|
|
1515
|
+
part1.push(node);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return [part0, part1];
|
|
1519
|
+
}
|
|
1538
1520
|
function checkPlanarity(G, nodes, edges, seed = null) {
|
|
1539
1521
|
if (nodes.length <= 4) {
|
|
1540
1522
|
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
|
|
@@ -1571,6 +1553,83 @@ function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
|
|
|
1571
1553
|
pos = rescaleLayout(pos, scale, center);
|
|
1572
1554
|
return pos;
|
|
1573
1555
|
}
|
|
1556
|
+
function spectralLayout(G, scale = 1, center = null, dim = 2) {
|
|
1557
|
+
const processed = _processParams(G, center, dim);
|
|
1558
|
+
const graph = processed.G;
|
|
1559
|
+
center = processed.center;
|
|
1560
|
+
const nodes = getNodesFromGraph(graph);
|
|
1561
|
+
if (nodes.length <= 2) {
|
|
1562
|
+
if (nodes.length === 0) {
|
|
1563
|
+
return {};
|
|
1564
|
+
} else if (nodes.length === 1) {
|
|
1565
|
+
return { [nodes[0]]: center };
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
[nodes[0]]: center.map((v) => v - scale),
|
|
1569
|
+
[nodes[1]]: center.map((v) => v + scale)
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const N = nodes.length;
|
|
1573
|
+
const nodeIndices = {};
|
|
1574
|
+
nodes.forEach((node, i) => {
|
|
1575
|
+
nodeIndices[node] = i;
|
|
1576
|
+
});
|
|
1577
|
+
const A = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1578
|
+
const edges = getEdgesFromGraph(graph);
|
|
1579
|
+
for (const [source, target] of edges) {
|
|
1580
|
+
const i = nodeIndices[source];
|
|
1581
|
+
const j = nodeIndices[target];
|
|
1582
|
+
A[i][j] = 1;
|
|
1583
|
+
A[j][i] = 1;
|
|
1584
|
+
}
|
|
1585
|
+
const L = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1586
|
+
for (let i = 0; i < N; i++) {
|
|
1587
|
+
L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
|
|
1588
|
+
for (let j = 0; j < N; j++) {
|
|
1589
|
+
L[i][j] -= A[i][j];
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const eigenvectors = [];
|
|
1593
|
+
for (let d = 0; d < dim; d++) {
|
|
1594
|
+
let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
|
|
1595
|
+
for (const ev of eigenvectors) {
|
|
1596
|
+
const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
|
|
1597
|
+
vector = vector.map((val, idx) => val - dot * ev[idx]);
|
|
1598
|
+
}
|
|
1599
|
+
const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
|
|
1600
|
+
vector = vector.map((val) => val / norm);
|
|
1601
|
+
for (let iter = 0; iter < 100; iter++) {
|
|
1602
|
+
const newVec = Array(N).fill(0);
|
|
1603
|
+
for (let i = 0; i < N; i++) {
|
|
1604
|
+
for (let j = 0; j < N; j++) {
|
|
1605
|
+
newVec[i] += L[i][j] * vector[j];
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
|
|
1609
|
+
newVec.forEach((val, idx, arr) => {
|
|
1610
|
+
arr[idx] = val - mean;
|
|
1611
|
+
});
|
|
1612
|
+
const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
|
|
1613
|
+
if (newNorm < 1e-10) {
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
vector = newVec.map((val) => val / newNorm);
|
|
1617
|
+
}
|
|
1618
|
+
eigenvectors.push(vector);
|
|
1619
|
+
}
|
|
1620
|
+
const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
|
|
1621
|
+
for (let i = 0; i < N; i++) {
|
|
1622
|
+
for (let d = 0; d < dim; d++) {
|
|
1623
|
+
positions[i][d] = eigenvectors[d][i];
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const scaledPositions = rescaleLayout(positions, scale);
|
|
1627
|
+
const pos = {};
|
|
1628
|
+
nodes.forEach((node, i) => {
|
|
1629
|
+
pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
|
|
1630
|
+
});
|
|
1631
|
+
return pos;
|
|
1632
|
+
}
|
|
1574
1633
|
function completeGraph(n) {
|
|
1575
1634
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1576
1635
|
const edges = [];
|
|
@@ -1623,6 +1682,30 @@ function wheelGraph(n) {
|
|
|
1623
1682
|
edges: () => edges
|
|
1624
1683
|
};
|
|
1625
1684
|
}
|
|
1685
|
+
function bipartiteGraph(n1, n2, p, seed) {
|
|
1686
|
+
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
1687
|
+
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
1688
|
+
const nodes = [...setA, ...setB];
|
|
1689
|
+
const edges = [];
|
|
1690
|
+
let currentSeed = seed;
|
|
1691
|
+
const random = seed !== void 0 ? () => {
|
|
1692
|
+
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1693
|
+
return currentSeed / 233280;
|
|
1694
|
+
} : Math.random;
|
|
1695
|
+
for (const a of setA) {
|
|
1696
|
+
for (const b of setB) {
|
|
1697
|
+
if (random() < p) {
|
|
1698
|
+
edges.push([a, b]);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
nodes: () => nodes,
|
|
1704
|
+
edges: () => edges,
|
|
1705
|
+
setA,
|
|
1706
|
+
setB
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1626
1709
|
function gridGraph(rows, cols) {
|
|
1627
1710
|
const nodes = [];
|
|
1628
1711
|
const edges = [];
|
|
@@ -1650,7 +1733,7 @@ function randomGraph(n, p, seed) {
|
|
|
1650
1733
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1651
1734
|
const edges = [];
|
|
1652
1735
|
let currentSeed = seed;
|
|
1653
|
-
|
|
1736
|
+
const random = seed !== void 0 ? () => {
|
|
1654
1737
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1655
1738
|
return currentSeed / 233280;
|
|
1656
1739
|
} : Math.random;
|
|
@@ -1666,30 +1749,6 @@ function randomGraph(n, p, seed) {
|
|
|
1666
1749
|
edges: () => edges
|
|
1667
1750
|
};
|
|
1668
1751
|
}
|
|
1669
|
-
function bipartiteGraph(n1, n2, p, seed) {
|
|
1670
|
-
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
1671
|
-
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
1672
|
-
const nodes = [...setA, ...setB];
|
|
1673
|
-
const edges = [];
|
|
1674
|
-
let currentSeed = seed;
|
|
1675
|
-
let random = seed !== void 0 ? () => {
|
|
1676
|
-
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1677
|
-
return currentSeed / 233280;
|
|
1678
|
-
} : Math.random;
|
|
1679
|
-
for (const a of setA) {
|
|
1680
|
-
for (const b of setB) {
|
|
1681
|
-
if (random() < p) {
|
|
1682
|
-
edges.push([a, b]);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
return {
|
|
1687
|
-
nodes: () => nodes,
|
|
1688
|
-
edges: () => edges,
|
|
1689
|
-
setA,
|
|
1690
|
-
setB
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
1752
|
function scaleFreeGraph(n, m, seed) {
|
|
1694
1753
|
if (m >= n) {
|
|
1695
1754
|
throw new Error("m must be less than n");
|
|
@@ -1698,7 +1757,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1698
1757
|
const edges = [];
|
|
1699
1758
|
const degrees = new Array(n).fill(0);
|
|
1700
1759
|
let currentSeed = seed;
|
|
1701
|
-
|
|
1760
|
+
const random = seed !== void 0 ? () => {
|
|
1702
1761
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1703
1762
|
return currentSeed / 233280;
|
|
1704
1763
|
} : Math.random;
|
|
@@ -1713,7 +1772,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1713
1772
|
const targets = /* @__PURE__ */ new Set();
|
|
1714
1773
|
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
1715
1774
|
while (targets.size < m) {
|
|
1716
|
-
|
|
1775
|
+
const r = random() * totalDegree;
|
|
1717
1776
|
let cumSum = 0;
|
|
1718
1777
|
for (let j = 0; j < i; j++) {
|
|
1719
1778
|
cumSum += degrees[j];
|