@graphty/layout 1.2.8 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +394 -404
  2. package/dist/layout.js +881 -821
  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 +3 -3
  21. package/dist/src/algorithms/planarity/check.js +7 -7
  22. package/dist/src/algorithms/planarity/check.js.map +1 -1
  23. package/dist/src/algorithms/planarity/embedding.d.ts +3 -5
  24. package/dist/src/algorithms/planarity/embedding.js +20 -16
  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 +3 -3
  30. package/dist/src/algorithms/planarity/lr-test.js +5 -5
  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 +3 -3
  108. package/dist/src/layouts/specialized/planar.js +8 -8
  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 -21
  145. package/src/algorithms/planarity/embedding.ts +159 -165
  146. package/src/algorithms/planarity/index.ts +4 -8
  147. package/src/algorithms/planarity/lr-test.ts +46 -45
  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 +42 -41
  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") {
@@ -1221,160 +1213,66 @@ function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
1221
1213
  }
1222
1214
  }
1223
1215
  }
1224
- if (nextLayer.length === 0) {
1225
- const unvisited = allNodes.filter((node) => !visited.has(node));
1226
- if (unvisited.length > 0) {
1227
- throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
1228
- }
1229
- break;
1230
- }
1231
- currentLayer++;
1232
- layers[currentLayer] = nextLayer;
1233
- }
1234
- return multipartiteLayout(graph, layers, align, scale, center);
1235
- }
1236
- function spectralLayout(G, scale = 1, center = null, dim = 2) {
1237
- const processed = _processParams(G, center, dim);
1238
- const graph = processed.G;
1239
- center = processed.center;
1240
- const nodes = getNodesFromGraph(graph);
1241
- if (nodes.length <= 2) {
1242
- if (nodes.length === 0) {
1243
- return {};
1244
- } else if (nodes.length === 1) {
1245
- return { [nodes[0]]: center };
1246
- } else {
1247
- return {
1248
- [nodes[0]]: center.map((v) => v - scale),
1249
- [nodes[1]]: center.map((v) => v + scale)
1250
- };
1251
- }
1252
- }
1253
- const N = nodes.length;
1254
- const nodeIndices = {};
1255
- nodes.forEach((node, i) => {
1256
- nodeIndices[node] = i;
1257
- });
1258
- const A = Array(N).fill(0).map(() => Array(N).fill(0));
1259
- const edges = getEdgesFromGraph(graph);
1260
- for (const [source, target] of edges) {
1261
- const i = nodeIndices[source];
1262
- const j = nodeIndices[target];
1263
- A[i][j] = 1;
1264
- A[j][i] = 1;
1265
- }
1266
- const L = Array(N).fill(0).map(() => Array(N).fill(0));
1267
- for (let i = 0; i < N; i++) {
1268
- L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
1269
- for (let j = 0; j < N; j++) {
1270
- L[i][j] -= A[i][j];
1271
- }
1272
- }
1273
- const eigenvectors = [];
1274
- for (let d = 0; d < dim; d++) {
1275
- let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
1276
- for (const ev of eigenvectors) {
1277
- const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
1278
- vector = vector.map((val, idx) => val - dot * ev[idx]);
1279
- }
1280
- const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
1281
- vector = vector.map((val) => val / norm);
1282
- for (let iter = 0; iter < 100; iter++) {
1283
- const newVec = Array(N).fill(0);
1284
- for (let i = 0; i < N; i++) {
1285
- for (let j = 0; j < N; j++) {
1286
- newVec[i] += L[i][j] * vector[j];
1287
- }
1288
- }
1289
- const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
1290
- newVec.forEach((val, idx, arr) => {
1291
- arr[idx] = val - mean;
1292
- });
1293
- const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
1294
- if (newNorm < 1e-10) continue;
1295
- vector = newVec.map((val) => val / newNorm);
1296
- }
1297
- eigenvectors.push(vector);
1298
- }
1299
- const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
1300
- for (let i = 0; i < N; i++) {
1301
- for (let d = 0; d < dim; d++) {
1302
- positions[i][d] = eigenvectors[d][i];
1303
- }
1304
- }
1305
- const scaledPositions = rescaleLayout(positions, scale);
1306
- const pos = {};
1307
- nodes.forEach((node, i) => {
1308
- pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
1309
- });
1310
- return pos;
1311
- }
1312
- function isK5(nodes, edges) {
1313
- if (nodes.length !== 5) return false;
1314
- if (edges.length !== 10) return false;
1315
- for (let i = 0; i < nodes.length; i++) {
1316
- for (let j = i + 1; j < nodes.length; j++) {
1317
- const hasEdge = edges.some(
1318
- (e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
1319
- );
1320
- if (!hasEdge) return false;
1321
- }
1322
- }
1323
- return true;
1324
- }
1325
- function isK33(nodes, edges) {
1326
- if (nodes.length !== 6) return false;
1327
- if (edges.length !== 9) return false;
1328
- const nodePartitions = tryFindBipartitePartition(nodes, edges);
1329
- if (!nodePartitions) return false;
1330
- const [part1, part2] = nodePartitions;
1331
- if (part1.length !== 3 || part2.length !== 3) return false;
1332
- for (const n1 of part1) {
1333
- for (const n2 of part2) {
1334
- const hasEdge = edges.some(
1335
- (e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1
1336
- );
1337
- if (!hasEdge) return false;
1338
- }
1339
- }
1340
- return true;
1341
- }
1342
- function tryFindBipartitePartition(nodes, edges) {
1343
- const colorMap = {};
1344
- const adjList = {};
1345
- for (const node of nodes) {
1346
- adjList[node] = [];
1347
- }
1348
- for (const [u, v] of edges) {
1349
- adjList[u].push(v);
1350
- adjList[v].push(u);
1351
- }
1352
- const queue = [nodes[0]];
1353
- colorMap[nodes[0]] = 0;
1354
- while (queue.length > 0) {
1355
- const node = queue.shift();
1356
- const nodeColor = colorMap[node];
1357
- for (const neighbor of adjList[node]) {
1358
- if (colorMap[neighbor] === void 0) {
1359
- colorMap[neighbor] = 1 - nodeColor;
1360
- queue.push(neighbor);
1361
- } else if (colorMap[neighbor] === nodeColor) {
1362
- return null;
1363
- }
1364
- }
1216
+ if (nextLayer.length === 0) {
1217
+ const unvisited = allNodes.filter((node) => !visited.has(node));
1218
+ if (unvisited.length > 0) {
1219
+ throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
1220
+ }
1221
+ break;
1222
+ }
1223
+ currentLayer++;
1224
+ layers[currentLayer] = nextLayer;
1365
1225
  }
1366
- 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
- function createTriangulationEmbedding(nodes, edges) {
1274
+ function createTriangulationEmbedding(nodes, edges, seed = null) {
1275
+ const rng = new RandomNumberGenerator(seed ?? void 0);
1378
1276
  const embedding = {
1379
1277
  nodeOrder: [...nodes],
1380
1278
  faceList: [],
@@ -1410,14 +1308,14 @@ function createTriangulationEmbedding(nodes, edges) {
1410
1308
  }
1411
1309
  }
1412
1310
  if (count > 0) {
1413
- const jitter = 0.1 * Math.random();
1311
+ const jitter = 0.1 * rng.rand();
1414
1312
  embedding.nodePositions[node] = [
1415
- xSum / count + jitter * (Math.random() - 0.5),
1416
- ySum / count + jitter * (Math.random() - 0.5)
1313
+ xSum / count + jitter * (rng.rand() - 0.5),
1314
+ ySum / count + jitter * (rng.rand() - 0.5)
1417
1315
  ];
1418
1316
  } else {
1419
- const r = 0.5 * Math.random();
1420
- const angle = 2 * Math.PI * Math.random();
1317
+ const r = 0.5 * rng.rand();
1318
+ const angle = 2 * Math.PI * rng.rand();
1421
1319
  embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
1422
1320
  }
1423
1321
  }
@@ -1425,8 +1323,12 @@ function createTriangulationEmbedding(nodes, edges) {
1425
1323
  return embedding;
1426
1324
  }
1427
1325
  function findCycle(nodes, edges, adjMap) {
1428
- if (nodes.length === 0) return null;
1429
- if (nodes.length <= 2) return nodes;
1326
+ if (nodes.length === 0) {
1327
+ return null;
1328
+ }
1329
+ if (nodes.length <= 2) {
1330
+ return nodes;
1331
+ }
1430
1332
  if (nodes.length <= 8) {
1431
1333
  let hamiltonianCycleDFS = function(node) {
1432
1334
  path.push(node);
@@ -1462,7 +1364,9 @@ function findCycle(nodes, edges, adjMap) {
1462
1364
  function findCycleDFS(node, parentNode) {
1463
1365
  visited.add(node);
1464
1366
  for (const neighbor of adjMap[node]) {
1465
- if (neighbor === parentNode) continue;
1367
+ if (neighbor === parentNode) {
1368
+ continue;
1369
+ }
1466
1370
  if (visited.has(neighbor)) {
1467
1371
  cycleFound = constructCycle(node, neighbor, parent);
1468
1372
  return true;
@@ -1504,7 +1408,7 @@ function combinatorialEmbeddingToPos(embedding, nodes) {
1504
1408
  }
1505
1409
  return pos;
1506
1410
  }
1507
- function lrPlanarityTest(nodes, edges) {
1411
+ function lrPlanarityTest(nodes, edges, seed = null) {
1508
1412
  const adjList = {};
1509
1413
  for (const node of nodes) {
1510
1414
  adjList[node] = [];
@@ -1526,25 +1430,104 @@ function lrPlanarityTest(nodes, edges) {
1526
1430
  }
1527
1431
  dfs(nodes[0]);
1528
1432
  if (ordering.length < nodes.length) {
1529
- return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
1433
+ return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
1530
1434
  }
1531
1435
  if (edges.length > 3 * nodes.length - 6) {
1532
1436
  return { isPlanar: false, embedding: null };
1533
1437
  }
1534
- const embedding = createTriangulationEmbedding(nodes, edges);
1438
+ const embedding = createTriangulationEmbedding(nodes, edges, seed);
1535
1439
  return { isPlanar: true, embedding };
1536
1440
  }
1537
- function checkPlanarity(G, nodes, edges) {
1441
+ function isK5(nodes, edges) {
1442
+ if (nodes.length !== 5) {
1443
+ return false;
1444
+ }
1445
+ if (edges.length !== 10) {
1446
+ return false;
1447
+ }
1448
+ for (let i = 0; i < nodes.length; i++) {
1449
+ for (let j = i + 1; j < nodes.length; j++) {
1450
+ const hasEdge = edges.some(
1451
+ (e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
1452
+ );
1453
+ if (!hasEdge) {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ }
1458
+ return true;
1459
+ }
1460
+ function isK33(nodes, edges) {
1461
+ if (nodes.length !== 6) {
1462
+ return false;
1463
+ }
1464
+ if (edges.length !== 9) {
1465
+ return false;
1466
+ }
1467
+ const nodePartitions = tryFindBipartitePartition(nodes, edges);
1468
+ if (!nodePartitions) {
1469
+ return false;
1470
+ }
1471
+ const [part1, part2] = nodePartitions;
1472
+ if (part1.length !== 3 || part2.length !== 3) {
1473
+ return false;
1474
+ }
1475
+ for (const n1 of part1) {
1476
+ for (const n2 of part2) {
1477
+ const hasEdge = edges.some((e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1);
1478
+ if (!hasEdge) {
1479
+ return false;
1480
+ }
1481
+ }
1482
+ }
1483
+ return true;
1484
+ }
1485
+ function tryFindBipartitePartition(nodes, edges) {
1486
+ const colorMap = {};
1487
+ const adjList = {};
1488
+ for (const node of nodes) {
1489
+ adjList[node] = [];
1490
+ }
1491
+ for (const [u, v] of edges) {
1492
+ adjList[u].push(v);
1493
+ adjList[v].push(u);
1494
+ }
1495
+ const queue = [nodes[0]];
1496
+ colorMap[nodes[0]] = 0;
1497
+ while (queue.length > 0) {
1498
+ const node = queue.shift();
1499
+ const nodeColor = colorMap[node];
1500
+ for (const neighbor of adjList[node]) {
1501
+ if (colorMap[neighbor] === void 0) {
1502
+ colorMap[neighbor] = 1 - nodeColor;
1503
+ queue.push(neighbor);
1504
+ } else if (colorMap[neighbor] === nodeColor) {
1505
+ return null;
1506
+ }
1507
+ }
1508
+ }
1509
+ const part0 = [];
1510
+ const part1 = [];
1511
+ for (const node of nodes) {
1512
+ if (colorMap[node] === 0) {
1513
+ part0.push(node);
1514
+ } else {
1515
+ part1.push(node);
1516
+ }
1517
+ }
1518
+ return [part0, part1];
1519
+ }
1520
+ function checkPlanarity(G, nodes, edges, seed = null) {
1538
1521
  if (nodes.length <= 4) {
1539
- return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
1522
+ return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
1540
1523
  }
1541
1524
  if (isK5(nodes, edges) || isK33(nodes, edges)) {
1542
1525
  return { isPlanar: false, embedding: null };
1543
1526
  }
1544
- const result = lrPlanarityTest(nodes, edges);
1527
+ const result = lrPlanarityTest(nodes, edges, seed);
1545
1528
  return result;
1546
1529
  }
1547
- function planarLayout(G, scale = 1, center = null, dim = 2) {
1530
+ function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) {
1548
1531
  if (dim !== 2) {
1549
1532
  throw new Error("can only handle 2 dimensions");
1550
1533
  }
@@ -1559,7 +1542,7 @@ function planarLayout(G, scale = 1, center = null, dim = 2) {
1559
1542
  if (nodes.length === 0) {
1560
1543
  return {};
1561
1544
  }
1562
- const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
1545
+ const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges, seed);
1563
1546
  if (!isPlanar) {
1564
1547
  throw new Error("G is not planar.");
1565
1548
  }
@@ -1570,6 +1553,83 @@ function planarLayout(G, scale = 1, center = null, dim = 2) {
1570
1553
  pos = rescaleLayout(pos, scale, center);
1571
1554
  return pos;
1572
1555
  }
1556
+ function spectralLayout(G, scale = 1, center = null, dim = 2) {
1557
+ const processed = _processParams(G, center, dim);
1558
+ const graph = processed.G;
1559
+ center = processed.center;
1560
+ const nodes = getNodesFromGraph(graph);
1561
+ if (nodes.length <= 2) {
1562
+ if (nodes.length === 0) {
1563
+ return {};
1564
+ } else if (nodes.length === 1) {
1565
+ return { [nodes[0]]: center };
1566
+ }
1567
+ return {
1568
+ [nodes[0]]: center.map((v) => v - scale),
1569
+ [nodes[1]]: center.map((v) => v + scale)
1570
+ };
1571
+ }
1572
+ const N = nodes.length;
1573
+ const nodeIndices = {};
1574
+ nodes.forEach((node, i) => {
1575
+ nodeIndices[node] = i;
1576
+ });
1577
+ const A = Array(N).fill(0).map(() => Array(N).fill(0));
1578
+ const edges = getEdgesFromGraph(graph);
1579
+ for (const [source, target] of edges) {
1580
+ const i = nodeIndices[source];
1581
+ const j = nodeIndices[target];
1582
+ A[i][j] = 1;
1583
+ A[j][i] = 1;
1584
+ }
1585
+ const L = Array(N).fill(0).map(() => Array(N).fill(0));
1586
+ for (let i = 0; i < N; i++) {
1587
+ L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
1588
+ for (let j = 0; j < N; j++) {
1589
+ L[i][j] -= A[i][j];
1590
+ }
1591
+ }
1592
+ const eigenvectors = [];
1593
+ for (let d = 0; d < dim; d++) {
1594
+ let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
1595
+ for (const ev of eigenvectors) {
1596
+ const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
1597
+ vector = vector.map((val, idx) => val - dot * ev[idx]);
1598
+ }
1599
+ const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
1600
+ vector = vector.map((val) => val / norm);
1601
+ for (let iter = 0; iter < 100; iter++) {
1602
+ const newVec = Array(N).fill(0);
1603
+ for (let i = 0; i < N; i++) {
1604
+ for (let j = 0; j < N; j++) {
1605
+ newVec[i] += L[i][j] * vector[j];
1606
+ }
1607
+ }
1608
+ const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
1609
+ newVec.forEach((val, idx, arr) => {
1610
+ arr[idx] = val - mean;
1611
+ });
1612
+ const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
1613
+ if (newNorm < 1e-10) {
1614
+ continue;
1615
+ }
1616
+ vector = newVec.map((val) => val / newNorm);
1617
+ }
1618
+ eigenvectors.push(vector);
1619
+ }
1620
+ const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
1621
+ for (let i = 0; i < N; i++) {
1622
+ for (let d = 0; d < dim; d++) {
1623
+ positions[i][d] = eigenvectors[d][i];
1624
+ }
1625
+ }
1626
+ const scaledPositions = rescaleLayout(positions, scale);
1627
+ const pos = {};
1628
+ nodes.forEach((node, i) => {
1629
+ pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
1630
+ });
1631
+ return pos;
1632
+ }
1573
1633
  function completeGraph(n) {
1574
1634
  const nodes = Array.from({ length: n }, (_, i) => i);
1575
1635
  const edges = [];
@@ -1622,6 +1682,30 @@ function wheelGraph(n) {
1622
1682
  edges: () => edges
1623
1683
  };
1624
1684
  }
1685
+ function bipartiteGraph(n1, n2, p, seed) {
1686
+ const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
1687
+ const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
1688
+ const nodes = [...setA, ...setB];
1689
+ const edges = [];
1690
+ let currentSeed = seed;
1691
+ const random = seed !== void 0 ? () => {
1692
+ currentSeed = (currentSeed * 9301 + 49297) % 233280;
1693
+ return currentSeed / 233280;
1694
+ } : Math.random;
1695
+ for (const a of setA) {
1696
+ for (const b of setB) {
1697
+ if (random() < p) {
1698
+ edges.push([a, b]);
1699
+ }
1700
+ }
1701
+ }
1702
+ return {
1703
+ nodes: () => nodes,
1704
+ edges: () => edges,
1705
+ setA,
1706
+ setB
1707
+ };
1708
+ }
1625
1709
  function gridGraph(rows, cols) {
1626
1710
  const nodes = [];
1627
1711
  const edges = [];
@@ -1649,7 +1733,7 @@ function randomGraph(n, p, seed) {
1649
1733
  const nodes = Array.from({ length: n }, (_, i) => i);
1650
1734
  const edges = [];
1651
1735
  let currentSeed = seed;
1652
- let random = seed !== void 0 ? () => {
1736
+ const random = seed !== void 0 ? () => {
1653
1737
  currentSeed = (currentSeed * 9301 + 49297) % 233280;
1654
1738
  return currentSeed / 233280;
1655
1739
  } : Math.random;
@@ -1665,30 +1749,6 @@ function randomGraph(n, p, seed) {
1665
1749
  edges: () => edges
1666
1750
  };
1667
1751
  }
1668
- function bipartiteGraph(n1, n2, p, seed) {
1669
- const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
1670
- const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
1671
- const nodes = [...setA, ...setB];
1672
- const edges = [];
1673
- let currentSeed = seed;
1674
- let random = seed !== void 0 ? () => {
1675
- currentSeed = (currentSeed * 9301 + 49297) % 233280;
1676
- return currentSeed / 233280;
1677
- } : Math.random;
1678
- for (const a of setA) {
1679
- for (const b of setB) {
1680
- if (random() < p) {
1681
- edges.push([a, b]);
1682
- }
1683
- }
1684
- }
1685
- return {
1686
- nodes: () => nodes,
1687
- edges: () => edges,
1688
- setA,
1689
- setB
1690
- };
1691
- }
1692
1752
  function scaleFreeGraph(n, m, seed) {
1693
1753
  if (m >= n) {
1694
1754
  throw new Error("m must be less than n");
@@ -1697,7 +1757,7 @@ function scaleFreeGraph(n, m, seed) {
1697
1757
  const edges = [];
1698
1758
  const degrees = new Array(n).fill(0);
1699
1759
  let currentSeed = seed;
1700
- let random = seed !== void 0 ? () => {
1760
+ const random = seed !== void 0 ? () => {
1701
1761
  currentSeed = (currentSeed * 9301 + 49297) % 233280;
1702
1762
  return currentSeed / 233280;
1703
1763
  } : Math.random;
@@ -1712,7 +1772,7 @@ function scaleFreeGraph(n, m, seed) {
1712
1772
  const targets = /* @__PURE__ */ new Set();
1713
1773
  const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
1714
1774
  while (targets.size < m) {
1715
- let r = random() * totalDegree;
1775
+ const r = random() * totalDegree;
1716
1776
  let cumSum = 0;
1717
1777
  for (let j = 0; j < i; j++) {
1718
1778
  cumSum += degrees[j];