@graphty/layout 1.2.9 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +393 -403
  2. package/dist/layout.js +867 -808
  3. package/dist/layout.js.map +1 -1
  4. package/dist/src/algorithms/index.d.ts +2 -2
  5. package/dist/src/algorithms/index.js +2 -2
  6. package/dist/src/algorithms/index.js.map +1 -1
  7. package/dist/src/algorithms/optimization/index.d.ts +4 -4
  8. package/dist/src/algorithms/optimization/index.js +3 -3
  9. package/dist/src/algorithms/optimization/index.js.map +1 -1
  10. package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +2 -5
  11. package/dist/src/algorithms/optimization/kamada-kawai-solver.js +6 -9
  12. package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -1
  13. package/dist/src/algorithms/optimization/lbfgs.d.ts +0 -1
  14. package/dist/src/algorithms/optimization/lbfgs.js +3 -5
  15. package/dist/src/algorithms/optimization/lbfgs.js.map +1 -1
  16. package/dist/src/algorithms/optimization/line-search.d.ts +0 -1
  17. package/dist/src/algorithms/optimization/line-search.js +0 -1
  18. package/dist/src/algorithms/optimization/line-search.js.map +1 -1
  19. package/dist/src/algorithms/optimization/types.d.ts +1 -1
  20. package/dist/src/algorithms/planarity/check.d.ts +2 -2
  21. package/dist/src/algorithms/planarity/check.js +4 -4
  22. package/dist/src/algorithms/planarity/check.js.map +1 -1
  23. package/dist/src/algorithms/planarity/embedding.d.ts +2 -4
  24. package/dist/src/algorithms/planarity/embedding.js +13 -12
  25. package/dist/src/algorithms/planarity/embedding.js.map +1 -1
  26. package/dist/src/algorithms/planarity/index.d.ts +4 -4
  27. package/dist/src/algorithms/planarity/index.js +4 -4
  28. package/dist/src/algorithms/planarity/index.js.map +1 -1
  29. package/dist/src/algorithms/planarity/lr-test.d.ts +2 -2
  30. package/dist/src/algorithms/planarity/lr-test.js +2 -2
  31. package/dist/src/algorithms/planarity/lr-test.js.map +1 -1
  32. package/dist/src/algorithms/planarity/special-graphs.d.ts +1 -4
  33. package/dist/src/algorithms/planarity/special-graphs.js +18 -15
  34. package/dist/src/algorithms/planarity/special-graphs.js.map +1 -1
  35. package/dist/src/generators/basic.d.ts +1 -1
  36. package/dist/src/generators/basic.js +4 -4
  37. package/dist/src/generators/basic.js.map +1 -1
  38. package/dist/src/generators/bipartite.d.ts +1 -1
  39. package/dist/src/generators/bipartite.js +2 -2
  40. package/dist/src/generators/bipartite.js.map +1 -1
  41. package/dist/src/generators/grid.d.ts +1 -1
  42. package/dist/src/generators/grid.js +1 -1
  43. package/dist/src/generators/grid.js.map +1 -1
  44. package/dist/src/generators/index.d.ts +5 -5
  45. package/dist/src/generators/index.js +5 -5
  46. package/dist/src/generators/index.js.map +1 -1
  47. package/dist/src/generators/random.d.ts +1 -1
  48. package/dist/src/generators/random.js +2 -2
  49. package/dist/src/generators/random.js.map +1 -1
  50. package/dist/src/generators/scale-free.d.ts +1 -1
  51. package/dist/src/generators/scale-free.js +4 -4
  52. package/dist/src/generators/scale-free.js.map +1 -1
  53. package/dist/src/index.d.ts +4 -4
  54. package/dist/src/index.js +4 -4
  55. package/dist/src/layouts/basic/index.d.ts +1 -1
  56. package/dist/src/layouts/basic/index.js +1 -1
  57. package/dist/src/layouts/basic/random.d.ts +1 -2
  58. package/dist/src/layouts/basic/random.js +3 -4
  59. package/dist/src/layouts/basic/random.js.map +1 -1
  60. package/dist/src/layouts/force-directed/arf.d.ts +1 -2
  61. package/dist/src/layouts/force-directed/arf.js +13 -8
  62. package/dist/src/layouts/force-directed/arf.js.map +1 -1
  63. package/dist/src/layouts/force-directed/forceatlas2.d.ts +1 -2
  64. package/dist/src/layouts/force-directed/forceatlas2.js +58 -35
  65. package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -1
  66. package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +11 -12
  67. package/dist/src/layouts/force-directed/fruchterman-reingold.js +17 -17
  68. package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -1
  69. package/dist/src/layouts/force-directed/index.d.ts +5 -5
  70. package/dist/src/layouts/force-directed/index.js +5 -5
  71. package/dist/src/layouts/force-directed/index.js.map +1 -1
  72. package/dist/src/layouts/force-directed/kamada-kawai.d.ts +2 -3
  73. package/dist/src/layouts/force-directed/kamada-kawai.js +12 -10
  74. package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -1
  75. package/dist/src/layouts/force-directed/spring.d.ts +11 -12
  76. package/dist/src/layouts/force-directed/spring.js +11 -12
  77. package/dist/src/layouts/force-directed/spring.js.map +1 -1
  78. package/dist/src/layouts/geometric/circular.d.ts +1 -2
  79. package/dist/src/layouts/geometric/circular.js +10 -9
  80. package/dist/src/layouts/geometric/circular.js.map +1 -1
  81. package/dist/src/layouts/geometric/index.d.ts +3 -3
  82. package/dist/src/layouts/geometric/index.js +3 -3
  83. package/dist/src/layouts/geometric/shell.d.ts +1 -2
  84. package/dist/src/layouts/geometric/shell.js +5 -5
  85. package/dist/src/layouts/geometric/shell.js.map +1 -1
  86. package/dist/src/layouts/geometric/spiral.d.ts +1 -2
  87. package/dist/src/layouts/geometric/spiral.js +5 -9
  88. package/dist/src/layouts/geometric/spiral.js.map +1 -1
  89. package/dist/src/layouts/hierarchical/bfs.d.ts +2 -3
  90. package/dist/src/layouts/hierarchical/bfs.js +5 -6
  91. package/dist/src/layouts/hierarchical/bfs.js.map +1 -1
  92. package/dist/src/layouts/hierarchical/bipartite.d.ts +2 -3
  93. package/dist/src/layouts/hierarchical/bipartite.js +8 -9
  94. package/dist/src/layouts/hierarchical/bipartite.js.map +1 -1
  95. package/dist/src/layouts/hierarchical/index.d.ts +3 -3
  96. package/dist/src/layouts/hierarchical/index.js +3 -3
  97. package/dist/src/layouts/hierarchical/index.js.map +1 -1
  98. package/dist/src/layouts/hierarchical/multipartite.d.ts +2 -3
  99. package/dist/src/layouts/hierarchical/multipartite.js +7 -8
  100. package/dist/src/layouts/hierarchical/multipartite.js.map +1 -1
  101. package/dist/src/layouts/index.d.ts +5 -5
  102. package/dist/src/layouts/index.js +5 -5
  103. package/dist/src/layouts/index.js.map +1 -1
  104. package/dist/src/layouts/specialized/index.d.ts +2 -2
  105. package/dist/src/layouts/specialized/index.js +2 -2
  106. package/dist/src/layouts/specialized/index.js.map +1 -1
  107. package/dist/src/layouts/specialized/planar.d.ts +1 -2
  108. package/dist/src/layouts/specialized/planar.js +5 -6
  109. package/dist/src/layouts/specialized/planar.js.map +1 -1
  110. package/dist/src/layouts/specialized/spectral.d.ts +1 -2
  111. package/dist/src/layouts/specialized/spectral.js +30 -20
  112. package/dist/src/layouts/specialized/spectral.js.map +1 -1
  113. package/dist/src/types/embedding.d.ts +1 -1
  114. package/dist/src/types/index.d.ts +3 -3
  115. package/dist/src/types/index.js +3 -3
  116. package/dist/src/types/index.js.map +1 -1
  117. package/dist/src/types/layout.d.ts +1 -1
  118. package/dist/src/utils/graph.d.ts +1 -5
  119. package/dist/src/utils/graph.js +4 -6
  120. package/dist/src/utils/graph.js.map +1 -1
  121. package/dist/src/utils/index.d.ts +5 -5
  122. package/dist/src/utils/index.js +5 -5
  123. package/dist/src/utils/index.js.map +1 -1
  124. package/dist/src/utils/numpy.js +22 -16
  125. package/dist/src/utils/numpy.js.map +1 -1
  126. package/dist/src/utils/params.d.ts +1 -2
  127. package/dist/src/utils/params.js +0 -1
  128. package/dist/src/utils/params.js.map +1 -1
  129. package/dist/src/utils/random.d.ts +17 -0
  130. package/dist/src/utils/random.js +18 -1
  131. package/dist/src/utils/random.js.map +1 -1
  132. package/dist/src/utils/rescale.d.ts +1 -3
  133. package/dist/src/utils/rescale.js +12 -10
  134. package/dist/src/utils/rescale.js.map +1 -1
  135. package/dist/vitest.config.js +14 -14
  136. package/dist/vitest.config.js.map +1 -1
  137. package/package.json +29 -24
  138. package/src/algorithms/index.ts +2 -2
  139. package/src/algorithms/optimization/index.ts +4 -8
  140. package/src/algorithms/optimization/kamada-kawai-solver.ts +170 -179
  141. package/src/algorithms/optimization/lbfgs.ts +41 -48
  142. package/src/algorithms/optimization/line-search.ts +27 -28
  143. package/src/algorithms/optimization/types.ts +2 -2
  144. package/src/algorithms/planarity/check.ts +22 -22
  145. package/src/algorithms/planarity/embedding.ts +159 -170
  146. package/src/algorithms/planarity/index.ts +4 -8
  147. package/src/algorithms/planarity/lr-test.ts +46 -46
  148. package/src/algorithms/planarity/special-graphs.ts +73 -83
  149. package/src/generators/basic.ts +57 -57
  150. package/src/generators/bipartite.ts +37 -31
  151. package/src/generators/grid.ts +28 -28
  152. package/src/generators/index.ts +5 -11
  153. package/src/generators/random.ts +26 -25
  154. package/src/generators/scale-free.ts +56 -55
  155. package/src/index.ts +5 -5
  156. package/src/layouts/basic/index.ts +1 -1
  157. package/src/layouts/basic/random.ts +20 -16
  158. package/src/layouts/force-directed/arf.ts +107 -105
  159. package/src/layouts/force-directed/forceatlas2.ts +365 -351
  160. package/src/layouts/force-directed/fruchterman-reingold.ts +133 -134
  161. package/src/layouts/force-directed/index.ts +5 -5
  162. package/src/layouts/force-directed/kamada-kawai.ts +78 -80
  163. package/src/layouts/force-directed/spring.ts +24 -25
  164. package/src/layouts/geometric/circular.ts +63 -57
  165. package/src/layouts/geometric/index.ts +3 -3
  166. package/src/layouts/geometric/shell.ts +58 -53
  167. package/src/layouts/geometric/spiral.ts +60 -64
  168. package/src/layouts/hierarchical/bfs.ts +58 -59
  169. package/src/layouts/hierarchical/bipartite.ts +62 -63
  170. package/src/layouts/hierarchical/index.ts +3 -3
  171. package/src/layouts/hierarchical/multipartite.ts +62 -63
  172. package/src/layouts/index.ts +5 -5
  173. package/src/layouts/specialized/index.ts +2 -2
  174. package/src/layouts/specialized/planar.ts +41 -42
  175. package/src/layouts/specialized/spectral.ts +110 -99
  176. package/src/types/embedding.ts +5 -5
  177. package/src/types/graph.ts +4 -4
  178. package/src/types/index.ts +3 -3
  179. package/src/types/layout.ts +2 -2
  180. package/src/utils/graph.ts +31 -35
  181. package/src/utils/index.ts +5 -5
  182. package/src/utils/numpy.ts +96 -92
  183. package/src/utils/params.ts +14 -11
  184. package/src/utils/random.ts +57 -40
  185. 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) return [];
3
+ if (pos.length === 0) {
4
+ return [];
5
+ }
4
6
  } else {
5
- if (Object.keys(pos).length === 0) return {};
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)) return NaN;
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)) return NaN;
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) return [];
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
- const np = {
226
- zeros: function(shape) {
227
- if (typeof shape === "number") {
228
- return Array(shape).fill(0);
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
- if (shape.length === 1) {
231
- return Array(shape[0]).fill(0);
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
- return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
234
- },
235
- ones: function(shape) {
236
- if (typeof shape === "number") {
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
- if (shape.length === 1) {
240
- return Array(shape[0]).fill(1);
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
- return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
243
- },
244
- linspace: function(start, stop, num) {
245
- if (num === 1) {
246
- return [start];
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
- const step = (stop - start) / (num - 1);
249
- return Array.from({ length: num }, (_, i) => start + i * step);
250
- },
251
- array: function(arr) {
252
- return Array.isArray(arr) ? [...arr] : [arr];
253
- },
254
- repeat: function(a, repeats) {
255
- const result = [];
256
- for (let i = 0; i < repeats; i++) {
257
- result.push(...np.array(a));
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
- return result;
260
- },
261
- mean: function(arr, axis = null) {
262
- if (axis === null) {
263
- const flatArr = Array.isArray(arr[0]) ? arr.flat(Infinity) : arr;
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
- if (axis === 0) {
268
- const result = [];
269
- const matrix = arr;
270
- for (let i = 0; i < matrix[0].length; i++) {
271
- let sum = 0;
272
- for (let j = 0; j < matrix.length; j++) {
273
- sum += matrix[j][i];
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
- result.push(sum / matrix.length);
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
- return arr.map((row) => np.mean(row));
280
- },
281
- add: function(a, b) {
282
- if (!Array.isArray(a) && !Array.isArray(b)) {
283
- return a + b;
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
- if (!Array.isArray(a)) {
286
- return b.map((val) => a + val);
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
- if (!Array.isArray(b)) {
289
- return a.map((val) => val + b);
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
- return a.map((val, i) => val + b[i]);
292
- },
293
- subtract: function(a, b) {
294
- if (!Array.isArray(a) && !Array.isArray(b)) {
295
- return a - b;
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
- if (!Array.isArray(a)) {
298
- return b.map((val) => a - val);
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 (!Array.isArray(b)) {
301
- return a.map((val) => val - b);
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
- return a.map((val, i) => val - b[i]);
304
- },
305
- max: function(arr) {
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");
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
- const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
394
- shell.forEach((node, j) => {
395
- const x = Math.cos(theta[j]) * radius + center[0];
396
- const y = Math.sin(theta[j]) * radius + center[1];
397
- pos[node] = [x, y];
398
- });
399
- radius += radiusBump;
400
- }
401
- return pos;
402
- }
403
- function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
404
- if (dim !== 2) {
405
- throw new Error("can only handle 2 dimensions");
406
- }
407
- const processed = _processParams(G, center || [0, 0], dim);
408
- const nodes = getNodesFromGraph(processed.G);
409
- center = processed.center;
410
- const pos = {};
411
- if (nodes.length === 0) {
412
- return pos;
413
- }
414
- if (nodes.length === 1) {
415
- pos[nodes[0]] = [...center];
416
- return pos;
417
- }
418
- let positions = [];
419
- if (equidistant) {
420
- const chord = 1;
421
- const step = 0.5;
422
- let theta = resolution;
423
- theta += chord / (step * theta);
424
- for (let i = 0; i < nodes.length; i++) {
425
- const r = step * theta;
426
- theta += chord / r;
427
- positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
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
- for (let i = 0; i < nodes.length; i++) {
447
- pos[nodes[i]] = scaledPositions[i];
584
+ const positions = {};
585
+ for (let i = 0; i < n; i++) {
586
+ positions[nodes[i]] = posArray[i];
448
587
  }
449
- return pos;
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
- let graph = processed.G;
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)) continue;
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
- (row) => row.map((d) => d === 0 ? 0 : 1 / (d + 1e-3))
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
- function kamadaKawaiLayout(G, dist = null, pos = null, weight = "weight", scale = 1, center = null, dim = 2) {
714
- const processed = _processParams(G, center, dim);
715
- const graph = processed.G;
716
- center = processed.center;
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
- const mass = new Array(nodes.length).fill(0);
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
- A[i][j] = edgeWeight;
853
- A[j][i] = edgeWeight;
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);
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
- let targetSpeed = swing2 === 0 ? Number.POSITIVE_INFINITY : jitter * speedEfficiency2 * traction2 / swing2;
874
- if (swing2 > jitter * traction2) {
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
- const maxRise = 0.5;
882
- speed2 = speed2 + Math.min(targetSpeed - speed2, maxRise * speed2);
883
- return [speed2, speedEfficiency2];
884
- }
885
- for (let iter = 0; iter < maxIter; iter++) {
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 diff = Array(n).fill(0).map(
894
- () => Array(n).fill(0).map(() => Array(dim).fill(0))
895
- );
896
- const distance = Array(n).fill(0).map(() => Array(n).fill(0));
897
- for (let i = 0; i < n; i++) {
898
- for (let j = 0; j < n; j++) {
899
- if (i === j) continue;
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
- }
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
- if (linlog) {
908
- for (let i = 0; i < n; i++) {
909
- for (let j = 0; j < n; j++) {
910
- if (i === j || A[i][j] === 0) continue;
911
- const dist = distance[i][j];
912
- const factor = -Math.log(1 + dist) / dist * A[i][j];
913
- for (let d = 0; d < dim; d++) {
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 (distributedAction) {
931
- for (let i = 0; i < n; i++) {
932
- for (let d = 0; d < dim; d++) {
933
- attraction[i][d] /= mass[i];
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
- for (let i = 0; i < n; i++) {
938
- for (let j = 0; j < n; j++) {
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
- }
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
- const centerOfMass = Array(dim).fill(0);
955
- for (let i = 0; i < n; i++) {
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
- for (let i = 0; i < n; i++) {
961
- const posCentered = Array(dim);
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
- const update = Array(n).fill(0).map(() => Array(dim).fill(0));
980
- let totalSwing = 0;
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;
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
- [speed, speedEfficiency] = estimateFactor(
996
- n,
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);
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
- if (totalMovement < 1e-10) {
1022
- break;
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 positions = {};
1026
- for (let i = 0; i < n; i++) {
1027
- positions[nodes[i]] = posArray[i];
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
- return rescaleLayout(positions);
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 getNodeDegree(graph, node) {
1032
- return graph.edges().filter(
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 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");
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 nodes = getNodesFromGraph(G);
1041
- const edges = getEdgesFromGraph(G);
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 (!pos) {
1046
- pos = randomLayout(G, null, 2, seed);
1047
- } else {
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
- const nodeIndex = {};
1058
- nodes.forEach((node, i) => {
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
- for (const [source, target] of edges) {
1068
- if (source === target) continue;
1069
- const i = nodeIndex[source];
1070
- const j = nodeIndex[target];
1071
- K[i][j] = a;
1072
- K[j][i] = a;
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
- const rho = scaling * Math.sqrt(N);
1075
- const dt = 1e-3;
1076
- const etol = 1e-6;
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
- }
1080
+ for (let i = 0; i < nlist.length; i++) {
1081
+ const shell = nlist[i];
1082
+ if (shell.length === 0) {
1083
+ continue;
1090
1084
  }
1091
- for (let i = 0; i < N; i++) {
1092
- for (let d = 0; d < positions[i].length; d++) {
1093
- positions[i][d] += change[i][d] * dt;
1094
- }
1085
+ if (shell.length === 1 && i === 0) {
1086
+ continue;
1095
1087
  }
1096
- error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
1097
- nIter++;
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
- const finalPos = {};
1100
- nodes.forEach((node, i) => {
1101
- finalPos[node] = positions[i];
1102
- });
1103
- return finalPos;
1096
+ return pos;
1104
1097
  }
1105
- function bipartiteLayout(G, nodes = null, align = "vertical", scale = 1, center = null, aspectRatio = 4 / 3) {
1106
- if (align !== "vertical" && align !== "horizontal") {
1107
- throw new Error("align must be either vertical or horizontal");
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], 2);
1110
- const graph = processed.G;
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
- const leftNodes = [...left];
1126
- leftNodes.forEach((node, i) => {
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
- const scaledPos = rescaleLayout(pos, scale, center);
1142
- if (align === "horizontal") {
1143
- for (const node in scaledPos) {
1144
- const temp = scaledPos[node][0];
1145
- scaledPos[node][0] = scaledPos[node][1];
1146
- scaledPos[node][1] = temp;
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
- return scaledPos;
1129
+ const posArray = [];
1130
+ for (let i = 0; i < positions.length; i++) {
1131
+ posArray.push(positions[i]);
1132
+ }
1133
+ const scaledPositions = rescaleLayout(posArray, scale);
1134
+ for (let i = 0; i < scaledPositions.length; i++) {
1135
+ scaledPositions[i][0] += center[0];
1136
+ scaledPositions[i][1] += center[1];
1137
+ }
1138
+ for (let i = 0; i < nodes.length; i++) {
1139
+ pos[nodes[i]] = scaledPositions[i];
1140
+ }
1141
+ return pos;
1150
1142
  }
1151
1143
  function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale = 1, center = null) {
1152
1144
  if (align !== "vertical" && align !== "horizontal") {
@@ -1218,161 +1210,66 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
1218
1210
  if (!visited.has(neighbor)) {
1219
1211
  nextLayer.push(neighbor);
1220
1212
  visited.add(neighbor);
1221
- }
1222
- }
1223
- }
1224
- if (nextLayer.length === 0) {
1225
- const unvisited = allNodes.filter((node) => !visited.has(node));
1226
- if (unvisited.length > 0) {
1227
- throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
1228
- }
1229
- break;
1230
- }
1231
- currentLayer++;
1232
- layers[currentLayer] = nextLayer;
1233
- }
1234
- return multipartiteLayout(graph, layers, align, scale, center);
1235
- }
1236
- function spectralLayout(G, scale = 1, center = null, dim = 2) {
1237
- const processed = _processParams(G, center, dim);
1238
- const graph = processed.G;
1239
- center = processed.center;
1240
- const nodes = getNodesFromGraph(graph);
1241
- if (nodes.length <= 2) {
1242
- if (nodes.length === 0) {
1243
- return {};
1244
- } else if (nodes.length === 1) {
1245
- return { [nodes[0]]: center };
1246
- } else {
1247
- return {
1248
- [nodes[0]]: center.map((v) => v - scale),
1249
- [nodes[1]]: center.map((v) => v + scale)
1250
- };
1251
- }
1252
- }
1253
- const N = nodes.length;
1254
- const nodeIndices = {};
1255
- nodes.forEach((node, i) => {
1256
- nodeIndices[node] = i;
1257
- });
1258
- const A = Array(N).fill(0).map(() => Array(N).fill(0));
1259
- const edges = getEdgesFromGraph(graph);
1260
- for (const [source, target] of edges) {
1261
- const i = nodeIndices[source];
1262
- const j = nodeIndices[target];
1263
- A[i][j] = 1;
1264
- A[j][i] = 1;
1265
- }
1266
- const L = Array(N).fill(0).map(() => Array(N).fill(0));
1267
- for (let i = 0; i < N; i++) {
1268
- L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
1269
- for (let j = 0; j < N; j++) {
1270
- L[i][j] -= A[i][j];
1271
- }
1272
- }
1273
- const eigenvectors = [];
1274
- for (let d = 0; d < dim; d++) {
1275
- let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
1276
- for (const ev of eigenvectors) {
1277
- const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
1278
- vector = vector.map((val, idx) => val - dot * ev[idx]);
1279
- }
1280
- const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
1281
- vector = vector.map((val) => val / norm);
1282
- for (let iter = 0; iter < 100; iter++) {
1283
- const newVec = Array(N).fill(0);
1284
- for (let i = 0; i < N; i++) {
1285
- for (let j = 0; j < N; j++) {
1286
- newVec[i] += L[i][j] * vector[j];
1287
- }
1288
- }
1289
- const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
1290
- newVec.forEach((val, idx, arr) => {
1291
- arr[idx] = val - mean;
1292
- });
1293
- const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
1294
- if (newNorm < 1e-10) continue;
1295
- vector = newVec.map((val) => val / newNorm);
1296
- }
1297
- eigenvectors.push(vector);
1298
- }
1299
- const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
1300
- for (let i = 0; i < N; i++) {
1301
- for (let d = 0; d < dim; d++) {
1302
- positions[i][d] = eigenvectors[d][i];
1303
- }
1304
- }
1305
- const scaledPositions = rescaleLayout(positions, scale);
1306
- const pos = {};
1307
- nodes.forEach((node, i) => {
1308
- pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
1309
- });
1310
- return pos;
1311
- }
1312
- function isK5(nodes, edges) {
1313
- if (nodes.length !== 5) return false;
1314
- if (edges.length !== 10) return false;
1315
- for (let i = 0; i < nodes.length; i++) {
1316
- for (let j = i + 1; j < nodes.length; j++) {
1317
- const hasEdge = edges.some(
1318
- (e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
1319
- );
1320
- if (!hasEdge) return false;
1321
- }
1322
- }
1323
- return true;
1324
- }
1325
- function isK33(nodes, edges) {
1326
- if (nodes.length !== 6) return false;
1327
- if (edges.length !== 9) return false;
1328
- const nodePartitions = tryFindBipartitePartition(nodes, edges);
1329
- if (!nodePartitions) return false;
1330
- const [part1, part2] = nodePartitions;
1331
- if (part1.length !== 3 || part2.length !== 3) return false;
1332
- for (const n1 of part1) {
1333
- for (const n2 of part2) {
1334
- const hasEdge = edges.some(
1335
- (e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1
1336
- );
1337
- if (!hasEdge) return false;
1338
- }
1339
- }
1340
- return true;
1341
- }
1342
- function tryFindBipartitePartition(nodes, edges) {
1343
- const colorMap = {};
1344
- const adjList = {};
1345
- for (const node of nodes) {
1346
- adjList[node] = [];
1347
- }
1348
- for (const [u, v] of edges) {
1349
- adjList[u].push(v);
1350
- adjList[v].push(u);
1351
- }
1352
- const queue = [nodes[0]];
1353
- colorMap[nodes[0]] = 0;
1354
- while (queue.length > 0) {
1355
- const node = queue.shift();
1356
- const nodeColor = colorMap[node];
1357
- for (const neighbor of adjList[node]) {
1358
- if (colorMap[neighbor] === void 0) {
1359
- colorMap[neighbor] = 1 - nodeColor;
1360
- queue.push(neighbor);
1361
- } else if (colorMap[neighbor] === nodeColor) {
1362
- return null;
1213
+ }
1214
+ }
1215
+ }
1216
+ if (nextLayer.length === 0) {
1217
+ const unvisited = allNodes.filter((node) => !visited.has(node));
1218
+ if (unvisited.length > 0) {
1219
+ throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
1363
1220
  }
1221
+ break;
1364
1222
  }
1223
+ currentLayer++;
1224
+ layers[currentLayer] = nextLayer;
1365
1225
  }
1366
- const part0 = [];
1367
- const part1 = [];
1368
- for (const node of nodes) {
1369
- if (colorMap[node] === 0) {
1370
- part0.push(node);
1371
- } else {
1372
- part1.push(node);
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 [part0, part1];
1272
+ return scaledPos;
1376
1273
  }
1377
1274
  function createTriangulationEmbedding(nodes, edges, seed = null) {
1378
1275
  const rng = new RandomNumberGenerator(seed ?? void 0);
@@ -1426,8 +1323,12 @@ function createTriangulationEmbedding(nodes, edges, seed = null) {
1426
1323
  return embedding;
1427
1324
  }
1428
1325
  function findCycle(nodes, edges, adjMap) {
1429
- if (nodes.length === 0) return null;
1430
- if (nodes.length <= 2) return nodes;
1326
+ if (nodes.length === 0) {
1327
+ return null;
1328
+ }
1329
+ if (nodes.length <= 2) {
1330
+ return nodes;
1331
+ }
1431
1332
  if (nodes.length <= 8) {
1432
1333
  let hamiltonianCycleDFS = function(node) {
1433
1334
  path.push(node);
@@ -1463,7 +1364,9 @@ function findCycle(nodes, edges, adjMap) {
1463
1364
  function findCycleDFS(node, parentNode) {
1464
1365
  visited.add(node);
1465
1366
  for (const neighbor of adjMap[node]) {
1466
- if (neighbor === parentNode) continue;
1367
+ if (neighbor === parentNode) {
1368
+ continue;
1369
+ }
1467
1370
  if (visited.has(neighbor)) {
1468
1371
  cycleFound = constructCycle(node, neighbor, parent);
1469
1372
  return true;
@@ -1535,6 +1438,85 @@ function lrPlanarityTest(nodes, edges, seed = null) {
1535
1438
  const embedding = createTriangulationEmbedding(nodes, edges, seed);
1536
1439
  return { isPlanar: true, embedding };
1537
1440
  }
1441
+ function isK5(nodes, edges) {
1442
+ if (nodes.length !== 5) {
1443
+ return false;
1444
+ }
1445
+ if (edges.length !== 10) {
1446
+ return false;
1447
+ }
1448
+ for (let i = 0; i < nodes.length; i++) {
1449
+ for (let j = i + 1; j < nodes.length; j++) {
1450
+ const hasEdge = edges.some(
1451
+ (e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
1452
+ );
1453
+ if (!hasEdge) {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ }
1458
+ return true;
1459
+ }
1460
+ function isK33(nodes, edges) {
1461
+ if (nodes.length !== 6) {
1462
+ return false;
1463
+ }
1464
+ if (edges.length !== 9) {
1465
+ return false;
1466
+ }
1467
+ const nodePartitions = tryFindBipartitePartition(nodes, edges);
1468
+ if (!nodePartitions) {
1469
+ return false;
1470
+ }
1471
+ const [part1, part2] = nodePartitions;
1472
+ if (part1.length !== 3 || part2.length !== 3) {
1473
+ return false;
1474
+ }
1475
+ for (const n1 of part1) {
1476
+ for (const n2 of part2) {
1477
+ const hasEdge = edges.some((e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1);
1478
+ if (!hasEdge) {
1479
+ return false;
1480
+ }
1481
+ }
1482
+ }
1483
+ return true;
1484
+ }
1485
+ function tryFindBipartitePartition(nodes, edges) {
1486
+ const colorMap = {};
1487
+ const adjList = {};
1488
+ for (const node of nodes) {
1489
+ adjList[node] = [];
1490
+ }
1491
+ for (const [u, v] of edges) {
1492
+ adjList[u].push(v);
1493
+ adjList[v].push(u);
1494
+ }
1495
+ const queue = [nodes[0]];
1496
+ colorMap[nodes[0]] = 0;
1497
+ while (queue.length > 0) {
1498
+ const node = queue.shift();
1499
+ const nodeColor = colorMap[node];
1500
+ for (const neighbor of adjList[node]) {
1501
+ if (colorMap[neighbor] === void 0) {
1502
+ colorMap[neighbor] = 1 - nodeColor;
1503
+ queue.push(neighbor);
1504
+ } else if (colorMap[neighbor] === nodeColor) {
1505
+ return null;
1506
+ }
1507
+ }
1508
+ }
1509
+ const part0 = [];
1510
+ const part1 = [];
1511
+ for (const node of nodes) {
1512
+ if (colorMap[node] === 0) {
1513
+ part0.push(node);
1514
+ } else {
1515
+ part1.push(node);
1516
+ }
1517
+ }
1518
+ return [part0, part1];
1519
+ }
1538
1520
  function checkPlanarity(G, nodes, edges, seed = null) {
1539
1521
  if (nodes.length <= 4) {
1540
1522
  return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
@@ -1571,6 +1553,83 @@ function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
1571
1553
  pos = rescaleLayout(pos, scale, center);
1572
1554
  return pos;
1573
1555
  }
1556
+ function spectralLayout(G, scale = 1, center = null, dim = 2) {
1557
+ const processed = _processParams(G, center, dim);
1558
+ const graph = processed.G;
1559
+ center = processed.center;
1560
+ const nodes = getNodesFromGraph(graph);
1561
+ if (nodes.length <= 2) {
1562
+ if (nodes.length === 0) {
1563
+ return {};
1564
+ } else if (nodes.length === 1) {
1565
+ return { [nodes[0]]: center };
1566
+ }
1567
+ return {
1568
+ [nodes[0]]: center.map((v) => v - scale),
1569
+ [nodes[1]]: center.map((v) => v + scale)
1570
+ };
1571
+ }
1572
+ const N = nodes.length;
1573
+ const nodeIndices = {};
1574
+ nodes.forEach((node, i) => {
1575
+ nodeIndices[node] = i;
1576
+ });
1577
+ const A = Array(N).fill(0).map(() => Array(N).fill(0));
1578
+ const edges = getEdgesFromGraph(graph);
1579
+ for (const [source, target] of edges) {
1580
+ const i = nodeIndices[source];
1581
+ const j = nodeIndices[target];
1582
+ A[i][j] = 1;
1583
+ A[j][i] = 1;
1584
+ }
1585
+ const L = Array(N).fill(0).map(() => Array(N).fill(0));
1586
+ for (let i = 0; i < N; i++) {
1587
+ L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
1588
+ for (let j = 0; j < N; j++) {
1589
+ L[i][j] -= A[i][j];
1590
+ }
1591
+ }
1592
+ const eigenvectors = [];
1593
+ for (let d = 0; d < dim; d++) {
1594
+ let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
1595
+ for (const ev of eigenvectors) {
1596
+ const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
1597
+ vector = vector.map((val, idx) => val - dot * ev[idx]);
1598
+ }
1599
+ const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
1600
+ vector = vector.map((val) => val / norm);
1601
+ for (let iter = 0; iter < 100; iter++) {
1602
+ const newVec = Array(N).fill(0);
1603
+ for (let i = 0; i < N; i++) {
1604
+ for (let j = 0; j < N; j++) {
1605
+ newVec[i] += L[i][j] * vector[j];
1606
+ }
1607
+ }
1608
+ const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
1609
+ newVec.forEach((val, idx, arr) => {
1610
+ arr[idx] = val - mean;
1611
+ });
1612
+ const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
1613
+ if (newNorm < 1e-10) {
1614
+ continue;
1615
+ }
1616
+ vector = newVec.map((val) => val / newNorm);
1617
+ }
1618
+ eigenvectors.push(vector);
1619
+ }
1620
+ const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
1621
+ for (let i = 0; i < N; i++) {
1622
+ for (let d = 0; d < dim; d++) {
1623
+ positions[i][d] = eigenvectors[d][i];
1624
+ }
1625
+ }
1626
+ const scaledPositions = rescaleLayout(positions, scale);
1627
+ const pos = {};
1628
+ nodes.forEach((node, i) => {
1629
+ pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
1630
+ });
1631
+ return pos;
1632
+ }
1574
1633
  function completeGraph(n) {
1575
1634
  const nodes = Array.from({ length: n }, (_, i) => i);
1576
1635
  const edges = [];
@@ -1623,6 +1682,30 @@ function wheelGraph(n) {
1623
1682
  edges: () => edges
1624
1683
  };
1625
1684
  }
1685
+ function bipartiteGraph(n1, n2, p, seed) {
1686
+ const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
1687
+ const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
1688
+ const nodes = [...setA, ...setB];
1689
+ const edges = [];
1690
+ let currentSeed = seed;
1691
+ const random = seed !== void 0 ? () => {
1692
+ currentSeed = (currentSeed * 9301 + 49297) % 233280;
1693
+ return currentSeed / 233280;
1694
+ } : Math.random;
1695
+ for (const a of setA) {
1696
+ for (const b of setB) {
1697
+ if (random() < p) {
1698
+ edges.push([a, b]);
1699
+ }
1700
+ }
1701
+ }
1702
+ return {
1703
+ nodes: () => nodes,
1704
+ edges: () => edges,
1705
+ setA,
1706
+ setB
1707
+ };
1708
+ }
1626
1709
  function gridGraph(rows, cols) {
1627
1710
  const nodes = [];
1628
1711
  const edges = [];
@@ -1650,7 +1733,7 @@ function randomGraph(n, p, seed) {
1650
1733
  const nodes = Array.from({ length: n }, (_, i) => i);
1651
1734
  const edges = [];
1652
1735
  let currentSeed = seed;
1653
- let random = seed !== void 0 ? () => {
1736
+ const random = seed !== void 0 ? () => {
1654
1737
  currentSeed = (currentSeed * 9301 + 49297) % 233280;
1655
1738
  return currentSeed / 233280;
1656
1739
  } : Math.random;
@@ -1666,30 +1749,6 @@ function randomGraph(n, p, seed) {
1666
1749
  edges: () => edges
1667
1750
  };
1668
1751
  }
1669
- function bipartiteGraph(n1, n2, p, seed) {
1670
- const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
1671
- const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
1672
- const nodes = [...setA, ...setB];
1673
- const edges = [];
1674
- let currentSeed = seed;
1675
- let random = seed !== void 0 ? () => {
1676
- currentSeed = (currentSeed * 9301 + 49297) % 233280;
1677
- return currentSeed / 233280;
1678
- } : Math.random;
1679
- for (const a of setA) {
1680
- for (const b of setB) {
1681
- if (random() < p) {
1682
- edges.push([a, b]);
1683
- }
1684
- }
1685
- }
1686
- return {
1687
- nodes: () => nodes,
1688
- edges: () => edges,
1689
- setA,
1690
- setB
1691
- };
1692
- }
1693
1752
  function scaleFreeGraph(n, m, seed) {
1694
1753
  if (m >= n) {
1695
1754
  throw new Error("m must be less than n");
@@ -1698,7 +1757,7 @@ function scaleFreeGraph(n, m, seed) {
1698
1757
  const edges = [];
1699
1758
  const degrees = new Array(n).fill(0);
1700
1759
  let currentSeed = seed;
1701
- let random = seed !== void 0 ? () => {
1760
+ const random = seed !== void 0 ? () => {
1702
1761
  currentSeed = (currentSeed * 9301 + 49297) % 233280;
1703
1762
  return currentSeed / 233280;
1704
1763
  } : Math.random;
@@ -1713,7 +1772,7 @@ function scaleFreeGraph(n, m, seed) {
1713
1772
  const targets = /* @__PURE__ */ new Set();
1714
1773
  const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
1715
1774
  while (targets.size < m) {
1716
- let r = random() * totalDegree;
1775
+ const r = random() * totalDegree;
1717
1776
  let cumSum = 0;
1718
1777
  for (let j = 0; j < i; j++) {
1719
1778
  cumSum += degrees[j];