@graphty/layout 1.2.9 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +400 -409
- package/dist/layout.js +884 -817
- package/dist/layout.js.map +1 -1
- package/dist/src/algorithms/index.d.ts +3 -2
- package/dist/src/algorithms/index.d.ts.map +1 -0
- 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 +5 -4
- package/dist/src/algorithms/optimization/index.d.ts.map +1 -0
- 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 +3 -5
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts.map +1 -0
- 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 +3 -3
- package/dist/src/algorithms/optimization/lbfgs.d.ts.map +1 -0
- package/dist/src/algorithms/optimization/lbfgs.js +5 -7
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -1
- package/dist/src/algorithms/optimization/line-search.d.ts +1 -1
- package/dist/src/algorithms/optimization/line-search.d.ts.map +1 -0
- 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 +2 -1
- package/dist/src/algorithms/optimization/types.d.ts.map +1 -0
- package/dist/src/algorithms/planarity/check.d.ts +5 -4
- package/dist/src/algorithms/planarity/check.d.ts.map +1 -0
- package/dist/src/algorithms/planarity/check.js +6 -6
- package/dist/src/algorithms/planarity/check.js.map +1 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +5 -6
- package/dist/src/algorithms/planarity/embedding.d.ts.map +1 -0
- package/dist/src/algorithms/planarity/embedding.js +15 -14
- package/dist/src/algorithms/planarity/embedding.js.map +1 -1
- package/dist/src/algorithms/planarity/index.d.ts +5 -4
- package/dist/src/algorithms/planarity/index.d.ts.map +1 -0
- 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 -2
- package/dist/src/algorithms/planarity/lr-test.d.ts.map +1 -0
- 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 +2 -4
- package/dist/src/algorithms/planarity/special-graphs.d.ts.map +1 -0
- 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 +2 -1
- package/dist/src/generators/basic.d.ts.map +1 -0
- package/dist/src/generators/basic.js +4 -4
- package/dist/src/generators/basic.js.map +1 -1
- package/dist/src/generators/bipartite.d.ts +2 -1
- package/dist/src/generators/bipartite.d.ts.map +1 -0
- package/dist/src/generators/bipartite.js +2 -2
- package/dist/src/generators/bipartite.js.map +1 -1
- package/dist/src/generators/grid.d.ts +2 -1
- package/dist/src/generators/grid.d.ts.map +1 -0
- package/dist/src/generators/grid.js +1 -1
- package/dist/src/generators/grid.js.map +1 -1
- package/dist/src/generators/index.d.ts +6 -5
- package/dist/src/generators/index.d.ts.map +1 -0
- package/dist/src/generators/index.js +5 -5
- package/dist/src/generators/index.js.map +1 -1
- package/dist/src/generators/random.d.ts +2 -1
- package/dist/src/generators/random.d.ts.map +1 -0
- 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 +2 -1
- package/dist/src/generators/scale-free.d.ts.map +1 -0
- 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 +5 -4
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -4
- package/dist/src/layouts/basic/index.d.ts +2 -1
- package/dist/src/layouts/basic/index.d.ts.map +1 -0
- package/dist/src/layouts/basic/index.js +1 -1
- package/dist/src/layouts/basic/random.d.ts +2 -2
- package/dist/src/layouts/basic/random.d.ts.map +1 -0
- package/dist/src/layouts/basic/random.js +4 -5
- package/dist/src/layouts/basic/random.js.map +1 -1
- package/dist/src/layouts/force-directed/arf.d.ts +2 -2
- package/dist/src/layouts/force-directed/arf.d.ts.map +1 -0
- 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 +4 -4
- package/dist/src/layouts/force-directed/forceatlas2.d.ts.map +1 -0
- package/dist/src/layouts/force-directed/forceatlas2.js +83 -51
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -1
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +12 -12
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts.map +1 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +18 -18
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -1
- package/dist/src/layouts/force-directed/index.d.ts +6 -5
- package/dist/src/layouts/force-directed/index.d.ts.map +1 -0
- 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 +3 -3
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts.map +1 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js +13 -11
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -1
- package/dist/src/layouts/force-directed/spring.d.ts +12 -12
- package/dist/src/layouts/force-directed/spring.d.ts.map +1 -0
- 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 +2 -2
- package/dist/src/layouts/geometric/circular.d.ts.map +1 -0
- package/dist/src/layouts/geometric/circular.js +11 -10
- package/dist/src/layouts/geometric/circular.js.map +1 -1
- package/dist/src/layouts/geometric/index.d.ts +4 -3
- package/dist/src/layouts/geometric/index.d.ts.map +1 -0
- package/dist/src/layouts/geometric/index.js +3 -3
- package/dist/src/layouts/geometric/shell.d.ts +2 -2
- package/dist/src/layouts/geometric/shell.d.ts.map +1 -0
- package/dist/src/layouts/geometric/shell.js +6 -6
- package/dist/src/layouts/geometric/shell.js.map +1 -1
- package/dist/src/layouts/geometric/spiral.d.ts +2 -2
- package/dist/src/layouts/geometric/spiral.d.ts.map +1 -0
- package/dist/src/layouts/geometric/spiral.js +6 -10
- package/dist/src/layouts/geometric/spiral.js.map +1 -1
- package/dist/src/layouts/hierarchical/bfs.d.ts +3 -3
- package/dist/src/layouts/hierarchical/bfs.d.ts.map +1 -0
- package/dist/src/layouts/hierarchical/bfs.js +6 -7
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -1
- package/dist/src/layouts/hierarchical/bipartite.d.ts +3 -3
- package/dist/src/layouts/hierarchical/bipartite.d.ts.map +1 -0
- package/dist/src/layouts/hierarchical/bipartite.js +9 -10
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -1
- package/dist/src/layouts/hierarchical/index.d.ts +4 -3
- package/dist/src/layouts/hierarchical/index.d.ts.map +1 -0
- 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 +3 -3
- package/dist/src/layouts/hierarchical/multipartite.d.ts.map +1 -0
- package/dist/src/layouts/hierarchical/multipartite.js +9 -10
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -1
- package/dist/src/layouts/index.d.ts +6 -5
- package/dist/src/layouts/index.d.ts.map +1 -0
- 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 +3 -2
- package/dist/src/layouts/specialized/index.d.ts.map +1 -0
- 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 +2 -2
- package/dist/src/layouts/specialized/planar.d.ts.map +1 -0
- package/dist/src/layouts/specialized/planar.js +6 -7
- package/dist/src/layouts/specialized/planar.js.map +1 -1
- package/dist/src/layouts/specialized/spectral.d.ts +2 -2
- package/dist/src/layouts/specialized/spectral.d.ts.map +1 -0
- package/dist/src/layouts/specialized/spectral.js +31 -21
- package/dist/src/layouts/specialized/spectral.js.map +1 -1
- package/dist/src/types/embedding.d.ts +2 -1
- package/dist/src/types/embedding.d.ts.map +1 -0
- package/dist/src/types/graph.d.ts +2 -1
- package/dist/src/types/graph.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +4 -3
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +3 -3
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/layout.d.ts +2 -1
- package/dist/src/types/layout.d.ts.map +1 -0
- package/dist/src/utils/graph.d.ts +2 -5
- package/dist/src/utils/graph.d.ts.map +1 -0
- package/dist/src/utils/graph.js +4 -6
- package/dist/src/utils/graph.js.map +1 -1
- package/dist/src/utils/index.d.ts +6 -5
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -5
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/numpy.d.ts +5 -4
- package/dist/src/utils/numpy.d.ts.map +1 -0
- package/dist/src/utils/numpy.js +22 -16
- package/dist/src/utils/numpy.js.map +1 -1
- package/dist/src/utils/params.d.ts +2 -2
- package/dist/src/utils/params.d.ts.map +1 -0
- package/dist/src/utils/params.js +0 -1
- package/dist/src/utils/params.js.map +1 -1
- package/dist/src/utils/random.d.ts +18 -0
- package/dist/src/utils/random.d.ts.map +1 -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 +2 -3
- package/dist/src/utils/rescale.d.ts.map +1 -0
- package/dist/src/utils/rescale.js +12 -10
- package/dist/src/utils/rescale.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -34
- 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 +42 -49
- package/src/algorithms/optimization/line-search.ts +27 -28
- package/src/algorithms/optimization/types.ts +2 -2
- package/src/algorithms/planarity/check.ts +23 -23
- package/src/algorithms/planarity/embedding.ts +160 -171
- 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 +374 -352
- 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 +77 -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/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -30
- package/dist/vitest.config.js.map +0 -1
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();
|
|
@@ -214,7 +237,7 @@ class RandomNumberGenerator {
|
|
|
214
237
|
function randomLayout(G, center = null, dim = 2, seed = null) {
|
|
215
238
|
const processed = _processParams(G, center, dim);
|
|
216
239
|
const nodes = getNodesFromGraph(processed.G);
|
|
217
|
-
center = processed
|
|
240
|
+
({ center } = processed);
|
|
218
241
|
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
219
242
|
const pos = {};
|
|
220
243
|
nodes.forEach((node) => {
|
|
@@ -222,236 +245,363 @@ function randomLayout(G, center = null, dim = 2, seed = null) {
|
|
|
222
245
|
});
|
|
223
246
|
return pos;
|
|
224
247
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
238
282
|
}
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
306
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return [start];
|
|
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
|
+
}
|
|
247
311
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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];
|
|
258
336
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
266
345
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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]);
|
|
274
355
|
}
|
|
275
|
-
result.push(sum / matrix.length);
|
|
276
356
|
}
|
|
277
|
-
return result;
|
|
278
357
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
363
|
}
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
posArray[i] = Array(dim).fill(0).map((_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d]));
|
|
375
|
+
pos[node] = posArray[i];
|
|
376
|
+
}
|
|
287
377
|
}
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
if (nodeMass && nodeMass[node]) {
|
|
385
|
+
mass[i] = nodeMass[node];
|
|
386
|
+
} else if (Array.isArray(graph)) {
|
|
387
|
+
mass[i] = 1;
|
|
388
|
+
} else {
|
|
389
|
+
mass[i] = getNodeDegree(graph, node) + 1;
|
|
290
390
|
}
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
391
|
+
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
392
|
+
}
|
|
393
|
+
const n = nodes.length;
|
|
394
|
+
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
395
|
+
const edges = Array.isArray(graph) ? [] : graph.edges();
|
|
396
|
+
const nodeIndices = {};
|
|
397
|
+
nodes.forEach((node, i) => {
|
|
398
|
+
nodeIndices[node] = i;
|
|
399
|
+
});
|
|
400
|
+
for (const [source, target] of edges) {
|
|
401
|
+
const i = nodeIndices[source];
|
|
402
|
+
const j = nodeIndices[target];
|
|
403
|
+
let edgeWeight = 1;
|
|
404
|
+
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
405
|
+
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
296
406
|
}
|
|
297
|
-
|
|
298
|
-
|
|
407
|
+
A[i][j] = edgeWeight;
|
|
408
|
+
A[j][i] = edgeWeight;
|
|
409
|
+
}
|
|
410
|
+
const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
411
|
+
const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
412
|
+
const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
413
|
+
let speed = 1;
|
|
414
|
+
let speedEfficiency = 1;
|
|
415
|
+
function estimateFactor(nodeCount, swingValue, tractionValue, currentSpeed, currentSpeedEfficiency, jitterToleranceParam) {
|
|
416
|
+
let resultSpeed = currentSpeed;
|
|
417
|
+
let resultSpeedEfficiency = currentSpeedEfficiency;
|
|
418
|
+
const optJitter = 0.05 * Math.sqrt(nodeCount);
|
|
419
|
+
const minJitter = Math.sqrt(optJitter);
|
|
420
|
+
const maxJitter = 10;
|
|
421
|
+
const minSpeedEfficiency = 0.05;
|
|
422
|
+
const other = Math.min(maxJitter, optJitter * tractionValue / (nodeCount * nodeCount));
|
|
423
|
+
let jitter = jitterToleranceParam * Math.max(minJitter, other);
|
|
424
|
+
if (swingValue / tractionValue > 2) {
|
|
425
|
+
if (resultSpeedEfficiency > minSpeedEfficiency) {
|
|
426
|
+
resultSpeedEfficiency *= 0.5;
|
|
427
|
+
}
|
|
428
|
+
jitter = Math.max(jitter, jitterToleranceParam);
|
|
299
429
|
}
|
|
300
|
-
|
|
301
|
-
|
|
430
|
+
const targetSpeed = swingValue === 0 ? Number.POSITIVE_INFINITY : jitter * resultSpeedEfficiency * tractionValue / swingValue;
|
|
431
|
+
if (swingValue > jitter * tractionValue) {
|
|
432
|
+
if (resultSpeedEfficiency > minSpeedEfficiency) {
|
|
433
|
+
resultSpeedEfficiency *= 0.7;
|
|
434
|
+
}
|
|
435
|
+
} else if (resultSpeed < 1e3) {
|
|
436
|
+
resultSpeedEfficiency *= 1.3;
|
|
302
437
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!Array.isArray(arr)) return arr;
|
|
307
|
-
return Math.max(...arr.flat(Infinity));
|
|
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");
|
|
438
|
+
const maxRise = 0.5;
|
|
439
|
+
resultSpeed = resultSpeed + Math.min(targetSpeed - resultSpeed, maxRise * resultSpeed);
|
|
440
|
+
return [resultSpeed, resultSpeedEfficiency];
|
|
320
441
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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;
|
|
442
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
443
|
+
for (let i = 0; i < n; i++) {
|
|
444
|
+
for (let d = 0; d < dim; d++) {
|
|
445
|
+
attraction[i][d] = 0;
|
|
446
|
+
repulsion[i][d] = 0;
|
|
447
|
+
gravities[i][d] = 0;
|
|
448
|
+
}
|
|
392
449
|
}
|
|
393
|
-
const
|
|
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
|
-
|
|
450
|
+
const diff = Array(n).fill(0).map(
|
|
451
|
+
() => Array(n).fill(0).map(() => Array(dim).fill(0))
|
|
452
|
+
);
|
|
453
|
+
const distance = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
454
|
+
for (let i = 0; i < n; i++) {
|
|
455
|
+
for (let j = 0; j < n; j++) {
|
|
456
|
+
if (i === j) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (let d = 0; d < dim; d++) {
|
|
460
|
+
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
461
|
+
}
|
|
462
|
+
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
463
|
+
if (distance[i][j] < 0.01) {
|
|
464
|
+
distance[i][j] = 0.01;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (linlog) {
|
|
469
|
+
for (let i = 0; i < n; i++) {
|
|
470
|
+
for (let j = 0; j < n; j++) {
|
|
471
|
+
if (i === j || A[i][j] === 0) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const dist = distance[i][j];
|
|
475
|
+
const factor = -Math.log(1 + dist) / dist * A[i][j];
|
|
476
|
+
for (let d = 0; d < dim; d++) {
|
|
477
|
+
const force = factor * diff[i][j][d];
|
|
478
|
+
attraction[i][d] += force;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
for (let i = 0; i < n; i++) {
|
|
484
|
+
for (let j = 0; j < n; j++) {
|
|
485
|
+
if (i === j || A[i][j] === 0) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
for (let d = 0; d < dim; d++) {
|
|
489
|
+
const force = -diff[i][j][d] * A[i][j];
|
|
490
|
+
attraction[i][d] += force;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (distributedAction) {
|
|
496
|
+
for (let i = 0; i < n; i++) {
|
|
497
|
+
for (let d = 0; d < dim; d++) {
|
|
498
|
+
attraction[i][d] /= mass[i];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (let i = 0; i < n; i++) {
|
|
503
|
+
for (let j = 0; j < n; j++) {
|
|
504
|
+
if (i === j) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
let dist = distance[i][j];
|
|
508
|
+
if (adjustSizes) {
|
|
509
|
+
dist -= size[i] - size[j];
|
|
510
|
+
dist = Math.max(dist, 0.01);
|
|
511
|
+
}
|
|
512
|
+
const distSquared = dist * dist;
|
|
513
|
+
const massProduct = mass[i] * mass[j];
|
|
514
|
+
const factor = massProduct / distSquared * scalingRatio;
|
|
515
|
+
for (let d = 0; d < dim; d++) {
|
|
516
|
+
const direction = diff[i][j][d] / dist;
|
|
517
|
+
repulsion[i][d] += direction * factor;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const centerOfMass = Array(dim).fill(0);
|
|
522
|
+
for (let i = 0; i < n; i++) {
|
|
523
|
+
for (let d = 0; d < dim; d++) {
|
|
524
|
+
centerOfMass[d] += posArray[i][d] / n;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (let i = 0; i < n; i++) {
|
|
528
|
+
const posCentered = Array(dim);
|
|
529
|
+
for (let d = 0; d < dim; d++) {
|
|
530
|
+
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
531
|
+
}
|
|
532
|
+
if (strongGravity) {
|
|
533
|
+
for (let d = 0; d < dim; d++) {
|
|
534
|
+
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
538
|
+
if (dist > 0.01) {
|
|
539
|
+
for (let d = 0; d < dim; d++) {
|
|
540
|
+
const direction = posCentered[d] / dist;
|
|
541
|
+
gravities[i][d] = -gravity * mass[i] * direction;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const update = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
547
|
+
let totalSwing = 0;
|
|
548
|
+
let totalTraction = 0;
|
|
549
|
+
for (let i = 0; i < n; i++) {
|
|
550
|
+
for (let d = 0; d < dim; d++) {
|
|
551
|
+
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
552
|
+
}
|
|
553
|
+
const oldPos = [...posArray[i]];
|
|
554
|
+
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
555
|
+
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
556
|
+
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
557
|
+
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
558
|
+
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
559
|
+
totalSwing += mass[i] * swingMagnitude;
|
|
560
|
+
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
561
|
+
}
|
|
562
|
+
[speed, speedEfficiency] = estimateFactor(
|
|
563
|
+
n,
|
|
564
|
+
totalSwing,
|
|
565
|
+
totalTraction,
|
|
566
|
+
speed,
|
|
567
|
+
speedEfficiency,
|
|
568
|
+
jitterTolerance
|
|
569
|
+
);
|
|
570
|
+
let totalMovement = 0;
|
|
571
|
+
for (let i = 0; i < n; i++) {
|
|
572
|
+
let factor;
|
|
573
|
+
if (adjustSizes) {
|
|
574
|
+
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
575
|
+
const swinging = mass[i] * df;
|
|
576
|
+
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
577
|
+
factor = Math.min(factor * df, 10) / df;
|
|
578
|
+
} else {
|
|
579
|
+
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
580
|
+
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
581
|
+
}
|
|
582
|
+
for (let d = 0; d < dim; d++) {
|
|
583
|
+
const movement = update[i][d] * factor;
|
|
584
|
+
posArray[i][d] += movement;
|
|
585
|
+
totalMovement += Math.abs(movement);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (totalMovement < 1e-10) {
|
|
589
|
+
break;
|
|
428
590
|
}
|
|
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
591
|
}
|
|
446
|
-
|
|
447
|
-
|
|
592
|
+
const positions = {};
|
|
593
|
+
for (let i = 0; i < n; i++) {
|
|
594
|
+
positions[nodes[i]] = posArray[i];
|
|
448
595
|
}
|
|
449
|
-
return
|
|
596
|
+
return rescaleLayout(positions);
|
|
597
|
+
}
|
|
598
|
+
function getNodeDegree(graph, node) {
|
|
599
|
+
return graph.edges().filter((edge) => edge[0] === node || edge[1] === node).length;
|
|
450
600
|
}
|
|
451
601
|
function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
452
602
|
const processed = _processParams(G, center, dim);
|
|
453
|
-
|
|
454
|
-
center = processed
|
|
603
|
+
const graph = processed.G;
|
|
604
|
+
({ center } = processed);
|
|
455
605
|
const nodes = getNodesFromGraph(graph);
|
|
456
606
|
const edges = getEdgesFromGraph(graph);
|
|
457
607
|
if (nodes.length === 0) {
|
|
@@ -514,7 +664,9 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
|
|
|
514
664
|
}
|
|
515
665
|
}
|
|
516
666
|
for (const node of nodes) {
|
|
517
|
-
if (fixedNodes.has(node))
|
|
667
|
+
if (fixedNodes.has(node)) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
518
670
|
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
|
|
519
671
|
const limitedMagnitude = Math.min(magnitude, t);
|
|
520
672
|
for (let j = 0; j < dim; j++) {
|
|
@@ -529,10 +681,7 @@ function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterat
|
|
|
529
681
|
}
|
|
530
682
|
return positions;
|
|
531
683
|
}
|
|
532
|
-
function
|
|
533
|
-
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
534
|
-
}
|
|
535
|
-
function _lbfgsDirection(grad, sList, yList, m) {
|
|
684
|
+
function _lbfgsDirection(grad, sList, yList, _m) {
|
|
536
685
|
if (sList.length === 0) {
|
|
537
686
|
return grad.map((g) => -g);
|
|
538
687
|
}
|
|
@@ -622,10 +771,8 @@ function _computeShortestPathDistances(G, weight) {
|
|
|
622
771
|
function _kamadaKawaiSolve(distMatrix, positions, dim) {
|
|
623
772
|
const nNodes = positions.length;
|
|
624
773
|
const meanWeight = 1e-3;
|
|
625
|
-
const invDistMatrix = distMatrix.map(
|
|
626
|
-
|
|
627
|
-
);
|
|
628
|
-
let posVec = positions.flat();
|
|
774
|
+
const invDistMatrix = distMatrix.map((row) => row.map((d) => d === 0 ? 0 : 1 / (d + 1e-3)));
|
|
775
|
+
const posVec = positions.flat();
|
|
629
776
|
const maxIter = 500;
|
|
630
777
|
const gtol = 1e-5;
|
|
631
778
|
const m = 10;
|
|
@@ -710,443 +857,296 @@ function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
|
|
|
710
857
|
}
|
|
711
858
|
return [cost, grad];
|
|
712
859
|
}
|
|
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
|
-
}
|
|
860
|
+
const np = {
|
|
861
|
+
zeros: function(shape) {
|
|
862
|
+
if (typeof shape === "number") {
|
|
863
|
+
return Array(shape).fill(0);
|
|
812
864
|
}
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
865
|
+
if (shape.length === 1) {
|
|
866
|
+
return Array(shape[0]).fill(0);
|
|
828
867
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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;
|
|
868
|
+
return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
|
|
869
|
+
},
|
|
870
|
+
ones: function(shape) {
|
|
871
|
+
if (typeof shape === "number") {
|
|
872
|
+
return Array(shape).fill(1);
|
|
851
873
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
856
|
-
const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
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);
|
|
874
|
+
if (shape.length === 1) {
|
|
875
|
+
return Array(shape[0]).fill(1);
|
|
872
876
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
} else if (speed2 < 1e3) {
|
|
879
|
-
speedEfficiency2 *= 1.3;
|
|
877
|
+
return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
|
|
878
|
+
},
|
|
879
|
+
linspace: function(start, stop, num) {
|
|
880
|
+
if (num === 1) {
|
|
881
|
+
return [start];
|
|
880
882
|
}
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
}
|
|
883
|
+
const step = (stop - start) / (num - 1);
|
|
884
|
+
return Array.from({ length: num }, (_, i) => start + i * step);
|
|
885
|
+
},
|
|
886
|
+
array: function(arr) {
|
|
887
|
+
return Array.isArray(arr) ? [...arr] : [arr];
|
|
888
|
+
},
|
|
889
|
+
repeat: function(a, repeats) {
|
|
890
|
+
const result = [];
|
|
891
|
+
for (let i = 0; i < repeats; i++) {
|
|
892
|
+
result.push(...np.array(a));
|
|
892
893
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
for (let d = 0; d < dim; d++) {
|
|
901
|
-
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
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
|
-
}
|
|
894
|
+
return result;
|
|
895
|
+
},
|
|
896
|
+
mean: function(arr, axis = null) {
|
|
897
|
+
if (axis === null) {
|
|
898
|
+
const flatArr = Array.isArray(arr[0]) ? arr.flat(Infinity) : arr;
|
|
899
|
+
const sum = flatArr.reduce((a, b) => a + b, 0);
|
|
900
|
+
return sum / flatArr.length;
|
|
906
901
|
}
|
|
907
|
-
if (
|
|
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
|
-
}
|
|
902
|
+
if (axis === 0) {
|
|
903
|
+
const result = [];
|
|
904
|
+
const matrix = arr;
|
|
905
|
+
for (let i = 0; i < matrix[0].length; i++) {
|
|
906
|
+
let sum = 0;
|
|
907
|
+
for (let j = 0; j < matrix.length; j++) {
|
|
908
|
+
sum += matrix[j][i];
|
|
927
909
|
}
|
|
910
|
+
result.push(sum / matrix.length);
|
|
928
911
|
}
|
|
912
|
+
return result;
|
|
929
913
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
914
|
+
return arr.map((row) => np.mean(row));
|
|
915
|
+
},
|
|
916
|
+
add: function(a, b) {
|
|
917
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
918
|
+
return a + b;
|
|
936
919
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (i === j) continue;
|
|
940
|
-
let dist = distance[i][j];
|
|
941
|
-
if (adjustSizes) {
|
|
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
|
-
}
|
|
920
|
+
if (!Array.isArray(a)) {
|
|
921
|
+
return b.map((val) => a + val);
|
|
953
922
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
for (let d = 0; d < dim; d++) {
|
|
957
|
-
centerOfMass[d] += posArray[i][d] / n;
|
|
958
|
-
}
|
|
923
|
+
if (!Array.isArray(b)) {
|
|
924
|
+
return a.map((val) => val + b);
|
|
959
925
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
}
|
|
926
|
+
return a.map((val, i) => val + b[i]);
|
|
927
|
+
},
|
|
928
|
+
subtract: function(a, b) {
|
|
929
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
930
|
+
return a - b;
|
|
978
931
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
let totalTraction = 0;
|
|
982
|
-
for (let i = 0; i < n; i++) {
|
|
983
|
-
for (let d = 0; d < dim; d++) {
|
|
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;
|
|
932
|
+
if (!Array.isArray(a)) {
|
|
933
|
+
return b.map((val) => a - val);
|
|
994
934
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
totalSwing,
|
|
998
|
-
totalTraction,
|
|
999
|
-
speed,
|
|
1000
|
-
speedEfficiency,
|
|
1001
|
-
jitterTolerance
|
|
1002
|
-
);
|
|
1003
|
-
let totalMovement = 0;
|
|
1004
|
-
for (let i = 0; i < n; i++) {
|
|
1005
|
-
let factor;
|
|
1006
|
-
if (adjustSizes) {
|
|
1007
|
-
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1008
|
-
const swinging = mass[i] * df;
|
|
1009
|
-
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
1010
|
-
factor = Math.min(factor * df, 10) / df;
|
|
1011
|
-
} else {
|
|
1012
|
-
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1013
|
-
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
1014
|
-
}
|
|
1015
|
-
for (let d = 0; d < dim; d++) {
|
|
1016
|
-
const movement = update[i][d] * factor;
|
|
1017
|
-
posArray[i][d] += movement;
|
|
1018
|
-
totalMovement += Math.abs(movement);
|
|
1019
|
-
}
|
|
935
|
+
if (!Array.isArray(b)) {
|
|
936
|
+
return a.map((val) => val - b);
|
|
1020
937
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
938
|
+
return a.map((val, i) => val - b[i]);
|
|
939
|
+
},
|
|
940
|
+
max: function(arr) {
|
|
941
|
+
if (!Array.isArray(arr)) {
|
|
942
|
+
return arr;
|
|
1023
943
|
}
|
|
944
|
+
return Math.max(...arr.flat(Infinity));
|
|
945
|
+
},
|
|
946
|
+
min: function(arr) {
|
|
947
|
+
if (!Array.isArray(arr)) {
|
|
948
|
+
return arr;
|
|
949
|
+
}
|
|
950
|
+
return Math.min(...arr.flat(Infinity));
|
|
951
|
+
},
|
|
952
|
+
norm: function(arr) {
|
|
953
|
+
return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
1024
954
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
return rescaleLayout(positions);
|
|
1030
|
-
}
|
|
1031
|
-
function getNodeDegree(graph, node) {
|
|
1032
|
-
return graph.edges().filter(
|
|
1033
|
-
(edge) => edge[0] === node || edge[1] === node
|
|
1034
|
-
).length;
|
|
1035
|
-
}
|
|
1036
|
-
function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1e3, seed = null) {
|
|
1037
|
-
if (a <= 1) {
|
|
1038
|
-
throw new Error("The parameter a should be larger than 1");
|
|
955
|
+
};
|
|
956
|
+
function circularLayout(G, scale = 1, center = null, dim = 2) {
|
|
957
|
+
if (dim < 2) {
|
|
958
|
+
throw new Error("cannot handle dimensions < 2");
|
|
1039
959
|
}
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
960
|
+
const processed = _processParams(G, center, dim);
|
|
961
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
962
|
+
({ center } = processed);
|
|
963
|
+
const pos = {};
|
|
1042
964
|
if (nodes.length === 0) {
|
|
1043
|
-
return
|
|
965
|
+
return pos;
|
|
1044
966
|
}
|
|
1045
|
-
if (
|
|
1046
|
-
pos =
|
|
967
|
+
if (nodes.length === 1) {
|
|
968
|
+
pos[nodes[0]] = center;
|
|
969
|
+
return pos;
|
|
970
|
+
}
|
|
971
|
+
if (dim === 2) {
|
|
972
|
+
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
973
|
+
nodes.forEach((node, i) => {
|
|
974
|
+
const x = Math.cos(theta[i]) * scale + center[0];
|
|
975
|
+
const y = Math.sin(theta[i]) * scale + center[1];
|
|
976
|
+
pos[node] = [x, y];
|
|
977
|
+
});
|
|
978
|
+
} else if (dim === 3) {
|
|
979
|
+
const n = nodes.length;
|
|
980
|
+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
981
|
+
nodes.forEach((node, i) => {
|
|
982
|
+
const theta = 2 * Math.PI * i / goldenRatio;
|
|
983
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
984
|
+
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
985
|
+
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
986
|
+
const z = Math.cos(phi) * scale + center[2];
|
|
987
|
+
pos[node] = [x, y, z];
|
|
988
|
+
});
|
|
1047
989
|
} else {
|
|
1048
|
-
const rng = new RandomNumberGenerator(
|
|
1049
|
-
const defaultPos = {};
|
|
990
|
+
const rng = new RandomNumberGenerator();
|
|
1050
991
|
nodes.forEach((node) => {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
992
|
+
const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
993
|
+
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
994
|
+
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
1054
995
|
});
|
|
1055
|
-
pos = { ...pos, ...defaultPos };
|
|
1056
996
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
997
|
+
return pos;
|
|
998
|
+
}
|
|
999
|
+
function kamadaKawaiLayout(G, dist = null, pos = null, weight = "weight", scale = 1, center = null, dim = 2) {
|
|
1000
|
+
const processed = _processParams(G, center, dim);
|
|
1001
|
+
const graph = processed.G;
|
|
1002
|
+
({ center } = processed);
|
|
1003
|
+
const nodes = getNodesFromGraph(graph);
|
|
1004
|
+
if (nodes.length === 0) {
|
|
1005
|
+
return {};
|
|
1066
1006
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const i = nodeIndex[source];
|
|
1070
|
-
const j = nodeIndex[target];
|
|
1071
|
-
K[i][j] = a;
|
|
1072
|
-
K[j][i] = a;
|
|
1007
|
+
if (nodes.length === 1) {
|
|
1008
|
+
return { [nodes[0]]: center };
|
|
1073
1009
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
let error = etol + 1;
|
|
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
|
-
}
|
|
1010
|
+
if (!dist) {
|
|
1011
|
+
if (Array.isArray(graph)) {
|
|
1012
|
+
throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes");
|
|
1090
1013
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1014
|
+
dist = _computeShortestPathDistances(graph, weight);
|
|
1015
|
+
}
|
|
1016
|
+
const nodesArray = Array.from(nodes);
|
|
1017
|
+
const nNodes = nodesArray.length;
|
|
1018
|
+
const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
|
|
1019
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1020
|
+
const nodeI = nodesArray[i];
|
|
1021
|
+
distMatrix[i][i] = 0;
|
|
1022
|
+
if (!dist[nodeI]) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
for (let j = 0; j < nNodes; j++) {
|
|
1026
|
+
const nodeJ = nodesArray[j];
|
|
1027
|
+
if (dist[nodeI][nodeJ] !== void 0) {
|
|
1028
|
+
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
1094
1029
|
}
|
|
1095
1030
|
}
|
|
1096
|
-
error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
|
|
1097
|
-
nIter++;
|
|
1098
1031
|
}
|
|
1032
|
+
if (!pos) {
|
|
1033
|
+
if (dim >= 2) {
|
|
1034
|
+
pos = circularLayout(G, 1, center, dim);
|
|
1035
|
+
} else {
|
|
1036
|
+
const posArray2 = {};
|
|
1037
|
+
nodesArray.forEach((node, i) => {
|
|
1038
|
+
posArray2[node] = [i / (nNodes - 1 || 1)];
|
|
1039
|
+
});
|
|
1040
|
+
pos = posArray2;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const posArray = new Array(nNodes);
|
|
1044
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1045
|
+
const node = nodesArray[i];
|
|
1046
|
+
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
1047
|
+
while (posArray[i].length < dim) {
|
|
1048
|
+
posArray[i].push(0);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
1099
1052
|
const finalPos = {};
|
|
1100
|
-
|
|
1101
|
-
finalPos[
|
|
1102
|
-
}
|
|
1103
|
-
return finalPos;
|
|
1053
|
+
for (let i = 0; i < nNodes; i++) {
|
|
1054
|
+
finalPos[nodesArray[i]] = newPositions[i];
|
|
1055
|
+
}
|
|
1056
|
+
return rescaleLayout(finalPos, scale, center);
|
|
1104
1057
|
}
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1058
|
+
function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
1059
|
+
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
1060
|
+
}
|
|
1061
|
+
function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
|
|
1062
|
+
if (dim !== 2) {
|
|
1063
|
+
throw new Error("can only handle 2 dimensions");
|
|
1108
1064
|
}
|
|
1109
|
-
const processed = _processParams(G, center
|
|
1110
|
-
const
|
|
1111
|
-
center = processed
|
|
1112
|
-
const
|
|
1113
|
-
if (
|
|
1114
|
-
return
|
|
1065
|
+
const processed = _processParams(G, center, dim);
|
|
1066
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
1067
|
+
({ center } = processed);
|
|
1068
|
+
const pos = {};
|
|
1069
|
+
if (nodes.length === 0) {
|
|
1070
|
+
return pos;
|
|
1115
1071
|
}
|
|
1116
|
-
if (
|
|
1117
|
-
nodes =
|
|
1072
|
+
if (nodes.length === 1) {
|
|
1073
|
+
pos[nodes[0]] = center;
|
|
1074
|
+
return pos;
|
|
1118
1075
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1076
|
+
if (!nlist) {
|
|
1077
|
+
nlist = [nodes];
|
|
1078
|
+
}
|
|
1079
|
+
const radiusBump = scale / nlist.length;
|
|
1080
|
+
let radius;
|
|
1081
|
+
if (nlist[0].length === 1) {
|
|
1082
|
+
radius = 0;
|
|
1083
|
+
pos[nlist[0][0]] = [...center];
|
|
1084
|
+
radius += radiusBump;
|
|
1085
|
+
} else {
|
|
1086
|
+
radius = radiusBump;
|
|
1087
|
+
}
|
|
1088
|
+
for (let i = 0; i < nlist.length; i++) {
|
|
1089
|
+
const shell = nlist[i];
|
|
1090
|
+
if (shell.length === 0) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (shell.length === 1 && i === 0) {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
|
|
1097
|
+
shell.forEach((node, j) => {
|
|
1098
|
+
const x = Math.cos(theta[j]) * radius + center[0];
|
|
1099
|
+
const y = Math.sin(theta[j]) * radius + center[1];
|
|
1100
|
+
pos[node] = [x, y];
|
|
1101
|
+
});
|
|
1102
|
+
radius += radiusBump;
|
|
1103
|
+
}
|
|
1104
|
+
return pos;
|
|
1105
|
+
}
|
|
1106
|
+
function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
|
|
1107
|
+
if (dim !== 2) {
|
|
1108
|
+
throw new Error("can only handle 2 dimensions");
|
|
1109
|
+
}
|
|
1110
|
+
const processed = _processParams(G, center || [0, 0], dim);
|
|
1111
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
1112
|
+
({ center } = processed);
|
|
1124
1113
|
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];
|
|
1114
|
+
if (nodes.length === 0) {
|
|
1115
|
+
return pos;
|
|
1140
1116
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1117
|
+
if (nodes.length === 1) {
|
|
1118
|
+
pos[nodes[0]] = [...center];
|
|
1119
|
+
return pos;
|
|
1120
|
+
}
|
|
1121
|
+
let positions = [];
|
|
1122
|
+
if (equidistant) {
|
|
1123
|
+
const chord = 1;
|
|
1124
|
+
const step = 0.5;
|
|
1125
|
+
let theta = resolution;
|
|
1126
|
+
theta += chord / (step * theta);
|
|
1127
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1128
|
+
const r = step * theta;
|
|
1129
|
+
theta += chord / r;
|
|
1130
|
+
positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
|
|
1147
1131
|
}
|
|
1132
|
+
} else {
|
|
1133
|
+
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
1134
|
+
const angle = dist.map((d) => resolution * d);
|
|
1135
|
+
positions = dist.map((d, i) => [Math.cos(angle[i]) * d, Math.sin(angle[i]) * d]);
|
|
1148
1136
|
}
|
|
1149
|
-
|
|
1137
|
+
const posArray = [];
|
|
1138
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1139
|
+
posArray.push(positions[i]);
|
|
1140
|
+
}
|
|
1141
|
+
const scaledPositions = rescaleLayout(posArray, scale);
|
|
1142
|
+
for (let i = 0; i < scaledPositions.length; i++) {
|
|
1143
|
+
scaledPositions[i][0] += center[0];
|
|
1144
|
+
scaledPositions[i][1] += center[1];
|
|
1145
|
+
}
|
|
1146
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1147
|
+
pos[nodes[i]] = scaledPositions[i];
|
|
1148
|
+
}
|
|
1149
|
+
return pos;
|
|
1150
1150
|
}
|
|
1151
1151
|
function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale = 1, center = null) {
|
|
1152
1152
|
if (align !== "vertical" && align !== "horizontal") {
|
|
@@ -1154,7 +1154,7 @@ function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale =
|
|
|
1154
1154
|
}
|
|
1155
1155
|
const processed = _processParams(G, center || [0, 0], 2);
|
|
1156
1156
|
const graph = processed.G;
|
|
1157
|
-
center = processed
|
|
1157
|
+
({ center } = processed);
|
|
1158
1158
|
const allNodes = getNodesFromGraph(graph);
|
|
1159
1159
|
if (allNodes.length === 0) {
|
|
1160
1160
|
return {};
|
|
@@ -1174,7 +1174,7 @@ function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale =
|
|
|
1174
1174
|
}
|
|
1175
1175
|
const layerCount = Object.keys(layers).length;
|
|
1176
1176
|
let pos = {};
|
|
1177
|
-
Object.entries(layers).forEach(([
|
|
1177
|
+
Object.entries(layers).forEach(([_layer, nodes], layerIdx) => {
|
|
1178
1178
|
const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
1179
1179
|
const layerSize = layerNodes.length;
|
|
1180
1180
|
layerNodes.forEach((node, nodeIdx) => {
|
|
@@ -1199,7 +1199,7 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
|
|
|
1199
1199
|
throw new Error("BFS layout requires a Graph with edges, not just a list of nodes");
|
|
1200
1200
|
}
|
|
1201
1201
|
const graph = processed.G;
|
|
1202
|
-
center = processed
|
|
1202
|
+
({ center } = processed);
|
|
1203
1203
|
const allNodes = getNodesFromGraph(graph);
|
|
1204
1204
|
if (allNodes.length === 0) {
|
|
1205
1205
|
return {};
|
|
@@ -1216,163 +1216,68 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
|
|
|
1216
1216
|
const neighbors = getNeighbors(graph, node);
|
|
1217
1217
|
for (const neighbor of neighbors) {
|
|
1218
1218
|
if (!visited.has(neighbor)) {
|
|
1219
|
-
nextLayer.push(neighbor);
|
|
1220
|
-
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;
|
|
1219
|
+
nextLayer.push(neighbor);
|
|
1220
|
+
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.");
|
|
1363
1228
|
}
|
|
1229
|
+
break;
|
|
1364
1230
|
}
|
|
1231
|
+
currentLayer++;
|
|
1232
|
+
layers[currentLayer] = nextLayer;
|
|
1365
1233
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1234
|
+
return multipartiteLayout(graph, layers, align, scale, center);
|
|
1235
|
+
}
|
|
1236
|
+
function bipartiteLayout(G, nodes = null, align = "vertical", scale = 1, center = null, aspectRatio = 4 / 3) {
|
|
1237
|
+
if (align !== "vertical" && align !== "horizontal") {
|
|
1238
|
+
throw new Error("align must be either vertical or horizontal");
|
|
1239
|
+
}
|
|
1240
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
1241
|
+
const graph = processed.G;
|
|
1242
|
+
({ center } = processed);
|
|
1243
|
+
const allNodes = getNodesFromGraph(graph);
|
|
1244
|
+
if (allNodes.length === 0) {
|
|
1245
|
+
return {};
|
|
1246
|
+
}
|
|
1247
|
+
if (!nodes) {
|
|
1248
|
+
nodes = allNodes.filter((_, i) => i % 2 === 0);
|
|
1249
|
+
}
|
|
1250
|
+
const left = new Set(nodes);
|
|
1251
|
+
const right = new Set(allNodes.filter((n) => !left.has(n)));
|
|
1252
|
+
const height = 1;
|
|
1253
|
+
const width = aspectRatio * height;
|
|
1254
|
+
const offset = [width / 2, height / 2];
|
|
1255
|
+
const pos = {};
|
|
1256
|
+
const leftNodes = [...left];
|
|
1257
|
+
leftNodes.forEach((node, i) => {
|
|
1258
|
+
const x = 0;
|
|
1259
|
+
const y = i * height / (leftNodes.length || 1);
|
|
1260
|
+
pos[node] = [x, y];
|
|
1261
|
+
});
|
|
1262
|
+
const rightNodes = [...right];
|
|
1263
|
+
rightNodes.forEach((node, i) => {
|
|
1264
|
+
const x = width;
|
|
1265
|
+
const y = i * height / (rightNodes.length || 1);
|
|
1266
|
+
pos[node] = [x, y];
|
|
1267
|
+
});
|
|
1268
|
+
for (const node in pos) {
|
|
1269
|
+
pos[node][0] -= offset[0];
|
|
1270
|
+
pos[node][1] -= offset[1];
|
|
1271
|
+
}
|
|
1272
|
+
const scaledPos = rescaleLayout(pos, scale, center);
|
|
1273
|
+
if (align === "horizontal") {
|
|
1274
|
+
for (const node in scaledPos) {
|
|
1275
|
+
const temp = scaledPos[node][0];
|
|
1276
|
+
scaledPos[node][0] = scaledPos[node][1];
|
|
1277
|
+
scaledPos[node][1] = temp;
|
|
1373
1278
|
}
|
|
1374
1279
|
}
|
|
1375
|
-
return
|
|
1280
|
+
return scaledPos;
|
|
1376
1281
|
}
|
|
1377
1282
|
function createTriangulationEmbedding(nodes, edges, seed = null) {
|
|
1378
1283
|
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
@@ -1425,9 +1330,13 @@ function createTriangulationEmbedding(nodes, edges, seed = null) {
|
|
|
1425
1330
|
}
|
|
1426
1331
|
return embedding;
|
|
1427
1332
|
}
|
|
1428
|
-
function findCycle(nodes,
|
|
1429
|
-
if (nodes.length === 0)
|
|
1430
|
-
|
|
1333
|
+
function findCycle(nodes, _edges, adjMap) {
|
|
1334
|
+
if (nodes.length === 0) {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
if (nodes.length <= 2) {
|
|
1338
|
+
return nodes;
|
|
1339
|
+
}
|
|
1431
1340
|
if (nodes.length <= 8) {
|
|
1432
1341
|
let hamiltonianCycleDFS = function(node) {
|
|
1433
1342
|
path.push(node);
|
|
@@ -1463,7 +1372,9 @@ function findCycle(nodes, edges, adjMap) {
|
|
|
1463
1372
|
function findCycleDFS(node, parentNode) {
|
|
1464
1373
|
visited.add(node);
|
|
1465
1374
|
for (const neighbor of adjMap[node]) {
|
|
1466
|
-
if (neighbor === parentNode)
|
|
1375
|
+
if (neighbor === parentNode) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1467
1378
|
if (visited.has(neighbor)) {
|
|
1468
1379
|
cycleFound = constructCycle(node, neighbor, parent);
|
|
1469
1380
|
return true;
|
|
@@ -1535,7 +1446,86 @@ function lrPlanarityTest(nodes, edges, seed = null) {
|
|
|
1535
1446
|
const embedding = createTriangulationEmbedding(nodes, edges, seed);
|
|
1536
1447
|
return { isPlanar: true, embedding };
|
|
1537
1448
|
}
|
|
1538
|
-
function
|
|
1449
|
+
function isK5(nodes, edges) {
|
|
1450
|
+
if (nodes.length !== 5) {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
if (edges.length !== 10) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1457
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1458
|
+
const hasEdge = edges.some(
|
|
1459
|
+
(e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
|
|
1460
|
+
);
|
|
1461
|
+
if (!hasEdge) {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
function isK33(nodes, edges) {
|
|
1469
|
+
if (nodes.length !== 6) {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
if (edges.length !== 9) {
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
const nodePartitions = tryFindBipartitePartition(nodes, edges);
|
|
1476
|
+
if (!nodePartitions) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
const [part1, part2] = nodePartitions;
|
|
1480
|
+
if (part1.length !== 3 || part2.length !== 3) {
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
for (const n1 of part1) {
|
|
1484
|
+
for (const n2 of part2) {
|
|
1485
|
+
const hasEdge = edges.some((e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1);
|
|
1486
|
+
if (!hasEdge) {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
function tryFindBipartitePartition(nodes, edges) {
|
|
1494
|
+
const colorMap = {};
|
|
1495
|
+
const adjList = {};
|
|
1496
|
+
for (const node of nodes) {
|
|
1497
|
+
adjList[node] = [];
|
|
1498
|
+
}
|
|
1499
|
+
for (const [u, v] of edges) {
|
|
1500
|
+
adjList[u].push(v);
|
|
1501
|
+
adjList[v].push(u);
|
|
1502
|
+
}
|
|
1503
|
+
const queue = [nodes[0]];
|
|
1504
|
+
colorMap[nodes[0]] = 0;
|
|
1505
|
+
while (queue.length > 0) {
|
|
1506
|
+
const node = queue.shift();
|
|
1507
|
+
const nodeColor = colorMap[node];
|
|
1508
|
+
for (const neighbor of adjList[node]) {
|
|
1509
|
+
if (colorMap[neighbor] === void 0) {
|
|
1510
|
+
colorMap[neighbor] = 1 - nodeColor;
|
|
1511
|
+
queue.push(neighbor);
|
|
1512
|
+
} else if (colorMap[neighbor] === nodeColor) {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const part0 = [];
|
|
1518
|
+
const part1 = [];
|
|
1519
|
+
for (const node of nodes) {
|
|
1520
|
+
if (colorMap[node] === 0) {
|
|
1521
|
+
part0.push(node);
|
|
1522
|
+
} else {
|
|
1523
|
+
part1.push(node);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return [part0, part1];
|
|
1527
|
+
}
|
|
1528
|
+
function checkPlanarity(_G, nodes, edges, seed = null) {
|
|
1539
1529
|
if (nodes.length <= 4) {
|
|
1540
1530
|
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
|
|
1541
1531
|
}
|
|
@@ -1554,7 +1544,7 @@ function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
|
|
|
1554
1544
|
throw new Error("Planar layout requires a Graph with edges, not just a list of nodes");
|
|
1555
1545
|
}
|
|
1556
1546
|
const graph = processed.G;
|
|
1557
|
-
center = processed
|
|
1547
|
+
({ center } = processed);
|
|
1558
1548
|
const nodes = getNodesFromGraph(graph);
|
|
1559
1549
|
const edges = getEdgesFromGraph(graph);
|
|
1560
1550
|
if (nodes.length === 0) {
|
|
@@ -1571,6 +1561,83 @@ function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
|
|
|
1571
1561
|
pos = rescaleLayout(pos, scale, center);
|
|
1572
1562
|
return pos;
|
|
1573
1563
|
}
|
|
1564
|
+
function spectralLayout(G, scale = 1, center = null, dim = 2) {
|
|
1565
|
+
const processed = _processParams(G, center, dim);
|
|
1566
|
+
const graph = processed.G;
|
|
1567
|
+
({ center } = processed);
|
|
1568
|
+
const nodes = getNodesFromGraph(graph);
|
|
1569
|
+
if (nodes.length <= 2) {
|
|
1570
|
+
if (nodes.length === 0) {
|
|
1571
|
+
return {};
|
|
1572
|
+
} else if (nodes.length === 1) {
|
|
1573
|
+
return { [nodes[0]]: center };
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
[nodes[0]]: center.map((v) => v - scale),
|
|
1577
|
+
[nodes[1]]: center.map((v) => v + scale)
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const N = nodes.length;
|
|
1581
|
+
const nodeIndices = {};
|
|
1582
|
+
nodes.forEach((node, i) => {
|
|
1583
|
+
nodeIndices[node] = i;
|
|
1584
|
+
});
|
|
1585
|
+
const A = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1586
|
+
const edges = getEdgesFromGraph(graph);
|
|
1587
|
+
for (const [source, target] of edges) {
|
|
1588
|
+
const i = nodeIndices[source];
|
|
1589
|
+
const j = nodeIndices[target];
|
|
1590
|
+
A[i][j] = 1;
|
|
1591
|
+
A[j][i] = 1;
|
|
1592
|
+
}
|
|
1593
|
+
const L = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1594
|
+
for (let i = 0; i < N; i++) {
|
|
1595
|
+
L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
|
|
1596
|
+
for (let j = 0; j < N; j++) {
|
|
1597
|
+
L[i][j] -= A[i][j];
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const eigenvectors = [];
|
|
1601
|
+
for (let d = 0; d < dim; d++) {
|
|
1602
|
+
let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
|
|
1603
|
+
for (const ev of eigenvectors) {
|
|
1604
|
+
const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
|
|
1605
|
+
vector = vector.map((val, idx) => val - dot * ev[idx]);
|
|
1606
|
+
}
|
|
1607
|
+
const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
|
|
1608
|
+
vector = vector.map((val) => val / norm);
|
|
1609
|
+
for (let iter = 0; iter < 100; iter++) {
|
|
1610
|
+
const newVec = Array(N).fill(0);
|
|
1611
|
+
for (let i = 0; i < N; i++) {
|
|
1612
|
+
for (let j = 0; j < N; j++) {
|
|
1613
|
+
newVec[i] += L[i][j] * vector[j];
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
|
|
1617
|
+
newVec.forEach((val, idx, arr) => {
|
|
1618
|
+
arr[idx] = val - mean;
|
|
1619
|
+
});
|
|
1620
|
+
const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
|
|
1621
|
+
if (newNorm < 1e-10) {
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
vector = newVec.map((val) => val / newNorm);
|
|
1625
|
+
}
|
|
1626
|
+
eigenvectors.push(vector);
|
|
1627
|
+
}
|
|
1628
|
+
const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
|
|
1629
|
+
for (let i = 0; i < N; i++) {
|
|
1630
|
+
for (let d = 0; d < dim; d++) {
|
|
1631
|
+
positions[i][d] = eigenvectors[d][i];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
const scaledPositions = rescaleLayout(positions, scale);
|
|
1635
|
+
const pos = {};
|
|
1636
|
+
nodes.forEach((node, i) => {
|
|
1637
|
+
pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
|
|
1638
|
+
});
|
|
1639
|
+
return pos;
|
|
1640
|
+
}
|
|
1574
1641
|
function completeGraph(n) {
|
|
1575
1642
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1576
1643
|
const edges = [];
|
|
@@ -1623,6 +1690,30 @@ function wheelGraph(n) {
|
|
|
1623
1690
|
edges: () => edges
|
|
1624
1691
|
};
|
|
1625
1692
|
}
|
|
1693
|
+
function bipartiteGraph(n1, n2, p, seed) {
|
|
1694
|
+
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
1695
|
+
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
1696
|
+
const nodes = [...setA, ...setB];
|
|
1697
|
+
const edges = [];
|
|
1698
|
+
let currentSeed = seed;
|
|
1699
|
+
const random = seed !== void 0 ? () => {
|
|
1700
|
+
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1701
|
+
return currentSeed / 233280;
|
|
1702
|
+
} : Math.random;
|
|
1703
|
+
for (const a of setA) {
|
|
1704
|
+
for (const b of setB) {
|
|
1705
|
+
if (random() < p) {
|
|
1706
|
+
edges.push([a, b]);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
nodes: () => nodes,
|
|
1712
|
+
edges: () => edges,
|
|
1713
|
+
setA,
|
|
1714
|
+
setB
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1626
1717
|
function gridGraph(rows, cols) {
|
|
1627
1718
|
const nodes = [];
|
|
1628
1719
|
const edges = [];
|
|
@@ -1650,7 +1741,7 @@ function randomGraph(n, p, seed) {
|
|
|
1650
1741
|
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1651
1742
|
const edges = [];
|
|
1652
1743
|
let currentSeed = seed;
|
|
1653
|
-
|
|
1744
|
+
const random = seed !== void 0 ? () => {
|
|
1654
1745
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1655
1746
|
return currentSeed / 233280;
|
|
1656
1747
|
} : Math.random;
|
|
@@ -1666,30 +1757,6 @@ function randomGraph(n, p, seed) {
|
|
|
1666
1757
|
edges: () => edges
|
|
1667
1758
|
};
|
|
1668
1759
|
}
|
|
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
1760
|
function scaleFreeGraph(n, m, seed) {
|
|
1694
1761
|
if (m >= n) {
|
|
1695
1762
|
throw new Error("m must be less than n");
|
|
@@ -1698,7 +1765,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1698
1765
|
const edges = [];
|
|
1699
1766
|
const degrees = new Array(n).fill(0);
|
|
1700
1767
|
let currentSeed = seed;
|
|
1701
|
-
|
|
1768
|
+
const random = seed !== void 0 ? () => {
|
|
1702
1769
|
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1703
1770
|
return currentSeed / 233280;
|
|
1704
1771
|
} : Math.random;
|
|
@@ -1713,7 +1780,7 @@ function scaleFreeGraph(n, m, seed) {
|
|
|
1713
1780
|
const targets = /* @__PURE__ */ new Set();
|
|
1714
1781
|
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
1715
1782
|
while (targets.size < m) {
|
|
1716
|
-
|
|
1783
|
+
const r = random() * totalDegree;
|
|
1717
1784
|
let cumSum = 0;
|
|
1718
1785
|
for (let j = 0; j < i; j++) {
|
|
1719
1786
|
cumSum += degrees[j];
|