@graphty/layout 1.2.8 → 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 +394 -404
- package/dist/layout.js +881 -821
- 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 +3 -3
- package/dist/src/algorithms/planarity/check.js +7 -7
- package/dist/src/algorithms/planarity/check.js.map +1 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +3 -5
- package/dist/src/algorithms/planarity/embedding.js +20 -16
- 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 +3 -3
- package/dist/src/algorithms/planarity/lr-test.js +5 -5
- 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 +3 -3
- package/dist/src/layouts/specialized/planar.js +8 -8
- 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 -21
- package/src/algorithms/planarity/embedding.ts +159 -165
- package/src/algorithms/planarity/index.ts +4 -8
- package/src/algorithms/planarity/lr-test.ts +46 -45
- 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 +42 -41
- 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") {
|
|
@@ -1221,160 +1213,66 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
|
|
|
1221
1213
|
}
|
|
1222
1214
|
}
|
|
1223
1215
|
}
|
|
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;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
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.");
|
|
1220
|
+
}
|
|
1221
|
+
break;
|
|
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
|
-
function createTriangulationEmbedding(nodes, edges) {
|
|
1274
|
+
function createTriangulationEmbedding(nodes, edges, seed = null) {
|
|
1275
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
1378
1276
|
const embedding = {
|
|
1379
1277
|
nodeOrder: [...nodes],
|
|
1380
1278
|
faceList: [],
|
|
@@ -1410,14 +1308,14 @@ function createTriangulationEmbedding(nodes, edges) {
|
|
|
1410
1308
|
}
|
|
1411
1309
|
}
|
|
1412
1310
|
if (count > 0) {
|
|
1413
|
-
const jitter = 0.1 *
|
|
1311
|
+
const jitter = 0.1 * rng.rand();
|
|
1414
1312
|
embedding.nodePositions[node] = [
|
|
1415
|
-
xSum / count + jitter * (
|
|
1416
|
-
ySum / count + jitter * (
|
|
1313
|
+
xSum / count + jitter * (rng.rand() - 0.5),
|
|
1314
|
+
ySum / count + jitter * (rng.rand() - 0.5)
|
|
1417
1315
|
];
|
|
1418
1316
|
} else {
|
|
1419
|
-
const r = 0.5 *
|
|
1420
|
-
const angle = 2 * Math.PI *
|
|
1317
|
+
const r = 0.5 * rng.rand();
|
|
1318
|
+
const angle = 2 * Math.PI * rng.rand();
|
|
1421
1319
|
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
1422
1320
|
}
|
|
1423
1321
|
}
|
|
@@ -1425,8 +1323,12 @@ function createTriangulationEmbedding(nodes, edges) {
|
|
|
1425
1323
|
return embedding;
|
|
1426
1324
|
}
|
|
1427
1325
|
function findCycle(nodes, edges, adjMap) {
|
|
1428
|
-
if (nodes.length === 0)
|
|
1429
|
-
|
|
1326
|
+
if (nodes.length === 0) {
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
if (nodes.length <= 2) {
|
|
1330
|
+
return nodes;
|
|
1331
|
+
}
|
|
1430
1332
|
if (nodes.length <= 8) {
|
|
1431
1333
|
let hamiltonianCycleDFS = function(node) {
|
|
1432
1334
|
path.push(node);
|
|
@@ -1462,7 +1364,9 @@ function findCycle(nodes, edges, adjMap) {
|
|
|
1462
1364
|
function findCycleDFS(node, parentNode) {
|
|
1463
1365
|
visited.add(node);
|
|
1464
1366
|
for (const neighbor of adjMap[node]) {
|
|
1465
|
-
if (neighbor === parentNode)
|
|
1367
|
+
if (neighbor === parentNode) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1466
1370
|
if (visited.has(neighbor)) {
|
|
1467
1371
|
cycleFound = constructCycle(node, neighbor, parent);
|
|
1468
1372
|
return true;
|
|
@@ -1504,7 +1408,7 @@ function combinatorialEmbeddingToPos(embedding, nodes) {
|
|
|
1504
1408
|
}
|
|
1505
1409
|
return pos;
|
|
1506
1410
|
}
|
|
1507
|
-
function lrPlanarityTest(nodes, edges) {
|
|
1411
|
+
function lrPlanarityTest(nodes, edges, seed = null) {
|
|
1508
1412
|
const adjList = {};
|
|
1509
1413
|
for (const node of nodes) {
|
|
1510
1414
|
adjList[node] = [];
|
|
@@ -1526,25 +1430,104 @@ function lrPlanarityTest(nodes, edges) {
|
|
|
1526
1430
|
}
|
|
1527
1431
|
dfs(nodes[0]);
|
|
1528
1432
|
if (ordering.length < nodes.length) {
|
|
1529
|
-
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
1433
|
+
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
|
|
1530
1434
|
}
|
|
1531
1435
|
if (edges.length > 3 * nodes.length - 6) {
|
|
1532
1436
|
return { isPlanar: false, embedding: null };
|
|
1533
1437
|
}
|
|
1534
|
-
const embedding = createTriangulationEmbedding(nodes, edges);
|
|
1438
|
+
const embedding = createTriangulationEmbedding(nodes, edges, seed);
|
|
1535
1439
|
return { isPlanar: true, embedding };
|
|
1536
1440
|
}
|
|
1537
|
-
function
|
|
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
|
+
}
|
|
1520
|
+
function checkPlanarity(G, nodes, edges, seed = null) {
|
|
1538
1521
|
if (nodes.length <= 4) {
|
|
1539
|
-
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
1522
|
+
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
|
|
1540
1523
|
}
|
|
1541
1524
|
if (isK5(nodes, edges) || isK33(nodes, edges)) {
|
|
1542
1525
|
return { isPlanar: false, embedding: null };
|
|
1543
1526
|
}
|
|
1544
|
-
const result = lrPlanarityTest(nodes, edges);
|
|
1527
|
+
const result = lrPlanarityTest(nodes, edges, seed);
|
|
1545
1528
|
return result;
|
|
1546
1529
|
}
|
|
1547
|
-
function planarLayout(G, scale = 1, center = null, dim = 2) {
|
|
1530
|
+
function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
|
|
1548
1531
|
if (dim !== 2) {
|
|
1549
1532
|
throw new Error("can only handle 2 dimensions");
|
|
1550
1533
|
}
|
|
@@ -1559,7 +1542,7 @@ function planarLayout(G, scale = 1, center = null, dim = 2) {
|
|
|
1559
1542
|
if (nodes.length === 0) {
|
|
1560
1543
|
return {};
|
|
1561
1544
|
}
|
|
1562
|
-
const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
|
|
1545
|
+
const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges, seed);
|
|
1563
1546
|
if (!isPlanar) {
|
|
1564
1547
|
throw new Error("G is not planar.");
|
|
1565
1548
|
}
|
|
@@ -1570,6 +1553,83 @@ function planarLayout(G, scale = 1, center = null, dim = 2) {
|
|
|
1570
1553
|
pos = rescaleLayout(pos, scale, center);
|
|
1571
1554
|
return pos;
|
|
1572
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
|
+
}
|
|
1573
1633
|
function completeGraph(n) {
|
|
1574
1634
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1575
1635
|
const edges = [];
|
|
@@ -1622,6 +1682,30 @@ function wheelGraph(n) {
|
|
|
1622
1682
|
edges: () => edges
|
|
1623
1683
|
};
|
|
1624
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
|
+
}
|
|
1625
1709
|
function gridGraph(rows, cols) {
|
|
1626
1710
|
const nodes = [];
|
|
1627
1711
|
const edges = [];
|
|
@@ -1649,7 +1733,7 @@ function randomGraph(n, p, seed) {
|
|
|
1649
1733
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1650
1734
|
const edges = [];
|
|
1651
1735
|
let currentSeed = seed;
|
|
1652
|
-
|
|
1736
|
+
const random = seed !== void 0 ? () => {
|
|
1653
1737
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1654
1738
|
return currentSeed / 233280;
|
|
1655
1739
|
} : Math.random;
|
|
@@ -1665,30 +1749,6 @@ function randomGraph(n, p, seed) {
|
|
|
1665
1749
|
edges: () => edges
|
|
1666
1750
|
};
|
|
1667
1751
|
}
|
|
1668
|
-
function bipartiteGraph(n1, n2, p, seed) {
|
|
1669
|
-
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
1670
|
-
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
1671
|
-
const nodes = [...setA, ...setB];
|
|
1672
|
-
const edges = [];
|
|
1673
|
-
let currentSeed = seed;
|
|
1674
|
-
let random = seed !== void 0 ? () => {
|
|
1675
|
-
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1676
|
-
return currentSeed / 233280;
|
|
1677
|
-
} : Math.random;
|
|
1678
|
-
for (const a of setA) {
|
|
1679
|
-
for (const b of setB) {
|
|
1680
|
-
if (random() < p) {
|
|
1681
|
-
edges.push([a, b]);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
return {
|
|
1686
|
-
nodes: () => nodes,
|
|
1687
|
-
edges: () => edges,
|
|
1688
|
-
setA,
|
|
1689
|
-
setB
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
1752
|
function scaleFreeGraph(n, m, seed) {
|
|
1693
1753
|
if (m >= n) {
|
|
1694
1754
|
throw new Error("m must be less than n");
|
|
@@ -1697,7 +1757,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1697
1757
|
const edges = [];
|
|
1698
1758
|
const degrees = new Array(n).fill(0);
|
|
1699
1759
|
let currentSeed = seed;
|
|
1700
|
-
|
|
1760
|
+
const random = seed !== void 0 ? () => {
|
|
1701
1761
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1702
1762
|
return currentSeed / 233280;
|
|
1703
1763
|
} : Math.random;
|
|
@@ -1712,7 +1772,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1712
1772
|
const targets = /* @__PURE__ */ new Set();
|
|
1713
1773
|
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
1714
1774
|
while (targets.size < m) {
|
|
1715
|
-
|
|
1775
|
+
const r = random() * totalDegree;
|
|
1716
1776
|
let cumSum = 0;
|
|
1717
1777
|
for (let j = 0; j < i; j++) {
|
|
1718
1778
|
cumSum += degrees[j];
|