@graphty/layout 1.2.3 → 1.2.5

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 (203) hide show
  1. package/dist/src/algorithms/index.d.ts +5 -0
  2. package/dist/src/algorithms/index.js +6 -0
  3. package/dist/src/algorithms/index.js.map +1 -0
  4. package/dist/src/algorithms/optimization/index.d.ts +7 -0
  5. package/dist/src/algorithms/optimization/index.js +7 -0
  6. package/dist/src/algorithms/optimization/index.js.map +1 -0
  7. package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +32 -0
  8. package/dist/src/algorithms/optimization/kamada-kawai-solver.js +176 -0
  9. package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -0
  10. package/dist/src/algorithms/optimization/lbfgs.d.ts +13 -0
  11. package/dist/src/algorithms/optimization/lbfgs.js +56 -0
  12. package/dist/src/algorithms/optimization/lbfgs.js.map +1 -0
  13. package/dist/src/algorithms/optimization/line-search.d.ts +15 -0
  14. package/dist/src/algorithms/optimization/line-search.js +37 -0
  15. package/dist/src/algorithms/optimization/line-search.js.map +1 -0
  16. package/dist/src/algorithms/optimization/types.d.ts +5 -0
  17. package/dist/src/algorithms/optimization/types.js +5 -0
  18. package/dist/src/algorithms/optimization/types.js.map +1 -0
  19. package/dist/src/algorithms/planarity/check.d.ts +17 -0
  20. package/dist/src/algorithms/planarity/check.js +30 -0
  21. package/dist/src/algorithms/planarity/check.js.map +1 -0
  22. package/dist/src/algorithms/planarity/embedding.d.ts +29 -0
  23. package/dist/src/algorithms/planarity/embedding.js +179 -0
  24. package/dist/src/algorithms/planarity/embedding.js.map +1 -0
  25. package/dist/src/algorithms/planarity/index.d.ts +7 -0
  26. package/dist/src/algorithms/planarity/index.js +8 -0
  27. package/dist/src/algorithms/planarity/index.js.map +1 -0
  28. package/dist/src/algorithms/planarity/lr-test.d.ts +15 -0
  29. package/dist/src/algorithms/planarity/lr-test.js +54 -0
  30. package/dist/src/algorithms/planarity/lr-test.js.map +1 -0
  31. package/dist/src/algorithms/planarity/special-graphs.d.ts +28 -0
  32. package/dist/src/algorithms/planarity/special-graphs.js +108 -0
  33. package/dist/src/algorithms/planarity/special-graphs.js.map +1 -0
  34. package/dist/src/generators/basic.d.ts +28 -0
  35. package/dist/src/generators/basic.js +79 -0
  36. package/dist/src/generators/basic.js.map +1 -0
  37. package/dist/src/generators/bipartite.d.ts +16 -0
  38. package/dist/src/generators/bipartite.js +40 -0
  39. package/dist/src/generators/bipartite.js.map +1 -0
  40. package/dist/src/generators/grid.d.ts +11 -0
  41. package/dist/src/generators/grid.js +37 -0
  42. package/dist/src/generators/grid.js.map +1 -0
  43. package/dist/src/generators/index.d.ts +8 -0
  44. package/dist/src/generators/index.js +9 -0
  45. package/dist/src/generators/index.js.map +1 -0
  46. package/dist/src/generators/random.d.ts +12 -0
  47. package/dist/src/generators/random.js +34 -0
  48. package/dist/src/generators/random.js.map +1 -0
  49. package/dist/src/generators/scale-free.d.ts +12 -0
  50. package/dist/src/generators/scale-free.js +62 -0
  51. package/dist/src/generators/scale-free.js.map +1 -0
  52. package/dist/src/index.d.ts +10 -0
  53. package/dist/src/index.js +15 -0
  54. package/dist/src/index.js.map +1 -0
  55. package/dist/src/layouts/basic/index.d.ts +4 -0
  56. package/dist/src/layouts/basic/index.js +5 -0
  57. package/dist/src/layouts/basic/index.js.map +1 -0
  58. package/dist/src/layouts/basic/random.d.ts +14 -0
  59. package/dist/src/layouts/basic/random.js +27 -0
  60. package/dist/src/layouts/basic/random.js.map +1 -0
  61. package/dist/src/layouts/force-directed/arf.d.ts +13 -0
  62. package/dist/src/layouts/force-directed/arf.js +103 -0
  63. package/dist/src/layouts/force-directed/arf.js.map +1 -0
  64. package/dist/src/layouts/force-directed/forceatlas2.d.ts +22 -0
  65. package/dist/src/layouts/force-directed/forceatlas2.js +340 -0
  66. package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -0
  67. package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +19 -0
  68. package/dist/src/layouts/force-directed/fruchterman-reingold.js +131 -0
  69. package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -0
  70. package/dist/src/layouts/force-directed/index.d.ts +8 -0
  71. package/dist/src/layouts/force-directed/index.js +9 -0
  72. package/dist/src/layouts/force-directed/index.js.map +1 -0
  73. package/dist/src/layouts/force-directed/kamada-kawai.d.ts +15 -0
  74. package/dist/src/layouts/force-directed/kamada-kawai.js +87 -0
  75. package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -0
  76. package/dist/src/layouts/force-directed/spring.d.ts +19 -0
  77. package/dist/src/layouts/force-directed/spring.js +23 -0
  78. package/dist/src/layouts/force-directed/spring.js.map +1 -0
  79. package/dist/src/layouts/geometric/circular.d.ts +14 -0
  80. package/dist/src/layouts/geometric/circular.js +67 -0
  81. package/dist/src/layouts/geometric/circular.js.map +1 -0
  82. package/dist/src/layouts/geometric/index.d.ts +6 -0
  83. package/dist/src/layouts/geometric/index.js +7 -0
  84. package/dist/src/layouts/geometric/index.js.map +1 -0
  85. package/dist/src/layouts/geometric/shell.d.ts +15 -0
  86. package/dist/src/layouts/geometric/shell.js +67 -0
  87. package/dist/src/layouts/geometric/shell.js.map +1 -0
  88. package/dist/src/layouts/geometric/spiral.d.ts +16 -0
  89. package/dist/src/layouts/geometric/spiral.js +73 -0
  90. package/dist/src/layouts/geometric/spiral.js.map +1 -0
  91. package/dist/src/layouts/hierarchical/bfs.d.ts +12 -0
  92. package/dist/src/layouts/hierarchical/bfs.js +62 -0
  93. package/dist/src/layouts/hierarchical/bfs.js.map +1 -0
  94. package/dist/src/layouts/hierarchical/bipartite.d.ts +16 -0
  95. package/dist/src/layouts/hierarchical/bipartite.js +72 -0
  96. package/dist/src/layouts/hierarchical/bipartite.js.map +1 -0
  97. package/dist/src/layouts/hierarchical/index.d.ts +6 -0
  98. package/dist/src/layouts/hierarchical/index.js +7 -0
  99. package/dist/src/layouts/hierarchical/index.js.map +1 -0
  100. package/dist/src/layouts/hierarchical/multipartite.d.ts +12 -0
  101. package/dist/src/layouts/hierarchical/multipartite.js +73 -0
  102. package/dist/src/layouts/hierarchical/multipartite.js.map +1 -0
  103. package/dist/src/layouts/index.d.ts +8 -0
  104. package/dist/src/layouts/index.js +9 -0
  105. package/dist/src/layouts/index.js.map +1 -0
  106. package/dist/src/layouts/specialized/index.d.ts +5 -0
  107. package/dist/src/layouts/specialized/index.js +6 -0
  108. package/dist/src/layouts/specialized/index.js.map +1 -0
  109. package/dist/src/layouts/specialized/planar.d.ts +14 -0
  110. package/dist/src/layouts/specialized/planar.js +47 -0
  111. package/dist/src/layouts/specialized/planar.js.map +1 -0
  112. package/dist/src/layouts/specialized/spectral.d.ts +14 -0
  113. package/dist/src/layouts/specialized/spectral.js +106 -0
  114. package/dist/src/layouts/specialized/spectral.js.map +1 -0
  115. package/dist/src/types/embedding.d.ts +9 -0
  116. package/dist/src/types/embedding.js +5 -0
  117. package/dist/src/types/embedding.js.map +1 -0
  118. package/dist/src/types/graph.d.ts +10 -0
  119. package/dist/src/types/graph.js +5 -0
  120. package/dist/src/types/graph.js.map +1 -0
  121. package/dist/src/types/index.d.ts +6 -0
  122. package/dist/src/types/index.js +7 -0
  123. package/dist/src/types/index.js.map +1 -0
  124. package/dist/src/types/layout.d.ts +6 -0
  125. package/dist/src/types/layout.js +5 -0
  126. package/dist/src/types/layout.js.map +1 -0
  127. package/dist/src/utils/graph.d.ts +34 -0
  128. package/dist/src/utils/graph.js +69 -0
  129. package/dist/src/utils/graph.js.map +1 -0
  130. package/dist/src/utils/index.d.ts +8 -0
  131. package/dist/src/utils/index.js +9 -0
  132. package/dist/src/utils/index.js.map +1 -0
  133. package/dist/src/utils/numpy.d.ts +17 -0
  134. package/dist/src/utils/numpy.js +101 -0
  135. package/dist/src/utils/numpy.js.map +1 -0
  136. package/dist/src/utils/params.d.ts +17 -0
  137. package/dist/src/utils/params.js +22 -0
  138. package/dist/src/utils/params.js.map +1 -0
  139. package/dist/src/utils/random.d.ts +14 -0
  140. package/dist/src/utils/random.js +42 -0
  141. package/dist/src/utils/random.js.map +1 -0
  142. package/dist/src/utils/rescale.d.ts +21 -0
  143. package/dist/src/utils/rescale.js +194 -0
  144. package/dist/src/utils/rescale.js.map +1 -0
  145. package/dist/vitest.config.js +1 -1
  146. package/dist/vitest.config.js.map +1 -1
  147. package/package.json +19 -4
  148. package/src/layouts/force-directed/forceatlas2.ts +23 -4
  149. package/src/utils/rescale.ts +90 -15
  150. package/.env.example +0 -11
  151. package/.github/workflows/ci.yml +0 -101
  152. package/.releaserc.json +0 -22
  153. package/CHANGELOG.md +0 -52
  154. package/CLAUDE.md +0 -104
  155. package/CONTRIBUTING.md +0 -1
  156. package/DEPLOYMENT.md +0 -59
  157. package/build-examples-script.js +0 -26
  158. package/commitlint.config.js +0 -37
  159. package/examples/3d-force-directed.html +0 -611
  160. package/examples/3d-kamada-kawai.html +0 -379
  161. package/examples/3d-layout-comparison.html +0 -448
  162. package/examples/3d-spherical-layout.html +0 -319
  163. package/examples/arf-layout.html +0 -524
  164. package/examples/bfs-layout.html +0 -487
  165. package/examples/bipartite-layout.html +0 -382
  166. package/examples/circular-layout.html +0 -292
  167. package/examples/forceatlas2-layout.html +0 -588
  168. package/examples/index.html +0 -241
  169. package/examples/kamada-kawai-layout.html +0 -425
  170. package/examples/layout-helpers.js +0 -23
  171. package/examples/layout.js +0 -2304
  172. package/examples/multipartite-layout.html +0 -410
  173. package/examples/planar-layout.html +0 -388
  174. package/examples/random-layout.html +0 -328
  175. package/examples/shell-layout.html +0 -323
  176. package/examples/spectral-layout.html +0 -335
  177. package/examples/spiral-layout.html +0 -447
  178. package/examples/spring-layout.html +0 -313
  179. package/test/arf-layout.test.ts +0 -443
  180. package/test/bfs-layout.test.ts +0 -427
  181. package/test/bipartite-layout.test.ts +0 -344
  182. package/test/circular-layout.test.ts +0 -442
  183. package/test/forceatlas2-layout.test.ts +0 -405
  184. package/test/fruchterman-reingold-layout.test.ts +0 -477
  185. package/test/graph-generators.test.ts +0 -450
  186. package/test/kamada-kawai-layout.test.ts +0 -623
  187. package/test/multipartite-layout.test.ts +0 -404
  188. package/test/planar-layout.test.ts +0 -266
  189. package/test/random-layout.test.ts +0 -254
  190. package/test/rescale-layout.test.ts +0 -373
  191. package/test/shell-layout.test.ts +0 -347
  192. package/test/spectral-layout.test.ts +0 -378
  193. package/test/spiral-layout.test.ts +0 -338
  194. package/test/spring-layout.test.ts +0 -241
  195. package/test/test-utils.ts +0 -34
  196. package/test/utils-graph.test.ts +0 -155
  197. package/test/utils-index.test.ts +0 -77
  198. package/test/utils-numpy.test.ts +0 -272
  199. package/test/utils-params.test.ts +0 -99
  200. package/test/utils-random.test.ts +0 -229
  201. package/tsconfig.json +0 -16
  202. package/vite.config.js +0 -36
  203. package/vitest.config.ts +0 -30
@@ -1,2304 +0,0 @@
1
- /**
2
- * Layout
3
- * ======
4
- *
5
- * Node positioning algorithms for graph drawing in TypeScript.
6
- *
7
- * For `randomLayout()` the possible resulting shape
8
- * is a square of side [0, scale] (default: [0, 1])
9
- * Changing `center` shifts the layout by that amount.
10
- *
11
- * For the other layout routines, the extent is
12
- * [center - scale, center + scale] (default: [-1, 1]).
13
- *
14
- * Ported from NetworkX Python library.
15
- */
16
- // Utility array manipulation functions (NumPy-like)
17
- const np = {
18
- zeros: function (shape) {
19
- if (typeof shape === 'number') {
20
- return Array(shape).fill(0);
21
- }
22
- if (shape.length === 1) {
23
- return Array(shape[0]).fill(0);
24
- }
25
- return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
26
- },
27
- ones: function (shape) {
28
- if (typeof shape === 'number') {
29
- return Array(shape).fill(1);
30
- }
31
- if (shape.length === 1) {
32
- return Array(shape[0]).fill(1);
33
- }
34
- return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
35
- },
36
- linspace: function (start, stop, num) {
37
- const step = (stop - start) / (num - 1);
38
- return Array.from({ length: num }, (_, i) => start + i * step);
39
- },
40
- array: function (arr) {
41
- return Array.isArray(arr) ? [...arr] : [arr];
42
- },
43
- repeat: function (a, repeats) {
44
- const result = [];
45
- for (let i = 0; i < repeats; i++) {
46
- result.push(...np.array(a));
47
- }
48
- return result;
49
- },
50
- mean: function (arr, axis = null) {
51
- if (axis === null) {
52
- const flatArr = Array.isArray(arr[0])
53
- ? arr.flat(Infinity)
54
- : arr;
55
- const sum = flatArr.reduce((a, b) => a + b, 0);
56
- return sum / flatArr.length;
57
- }
58
- if (axis === 0) {
59
- const result = [];
60
- const matrix = arr;
61
- for (let i = 0; i < matrix[0].length; i++) {
62
- let sum = 0;
63
- for (let j = 0; j < matrix.length; j++) {
64
- sum += matrix[j][i];
65
- }
66
- result.push(sum / matrix.length);
67
- }
68
- return result;
69
- }
70
- return arr.map(row => np.mean(row));
71
- },
72
- add: function (a, b) {
73
- if (!Array.isArray(a) && !Array.isArray(b)) {
74
- return a + b;
75
- }
76
- if (!Array.isArray(a)) {
77
- return b.map(val => a + val);
78
- }
79
- if (!Array.isArray(b)) {
80
- return a.map(val => val + b);
81
- }
82
- return a.map((val, i) => val + b[i]);
83
- },
84
- subtract: function (a, b) {
85
- if (!Array.isArray(a) && !Array.isArray(b)) {
86
- return a - b;
87
- }
88
- if (!Array.isArray(a)) {
89
- return b.map(val => a - val);
90
- }
91
- if (!Array.isArray(b)) {
92
- return a.map(val => val - b);
93
- }
94
- return a.map((val, i) => val - b[i]);
95
- },
96
- max: function (arr) {
97
- if (!Array.isArray(arr))
98
- return arr;
99
- return Math.max(...arr.flat(Infinity));
100
- },
101
- min: function (arr) {
102
- if (!Array.isArray(arr))
103
- return arr;
104
- return Math.min(...arr.flat(Infinity));
105
- },
106
- norm: function (arr) {
107
- return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
108
- }
109
- };
110
- // Random number generator (for seed-based randomization)
111
- class RandomNumberGenerator {
112
- constructor(seed) {
113
- this.seed = seed || Math.floor(Math.random() * 1000000);
114
- this.m = 2 ** 35 - 31;
115
- this.a = 185852;
116
- this.c = 1;
117
- this._state = this.seed % this.m;
118
- }
119
- _next() {
120
- this._state = (this.a * this._state + this.c) % this.m;
121
- return this._state / this.m;
122
- }
123
- rand(shape = null) {
124
- if (shape === null) {
125
- return this._next();
126
- }
127
- if (typeof shape === 'number') {
128
- const result = [];
129
- for (let i = 0; i < shape; i++) {
130
- result.push(this._next());
131
- }
132
- return result;
133
- }
134
- if (shape.length === 1) {
135
- const result = [];
136
- for (let i = 0; i < shape[0]; i++) {
137
- result.push(this._next());
138
- }
139
- return result;
140
- }
141
- const result = [];
142
- for (let i = 0; i < shape[0]; i++) {
143
- result.push(this.rand(shape.slice(1)));
144
- }
145
- return result;
146
- }
147
- }
148
- /**
149
- * Extract nodes from a graph object
150
- *
151
- * @param G - Graph or list of nodes
152
- * @returns Array of nodes
153
- */
154
- function getNodesFromGraph(G) {
155
- return G.nodes ? G.nodes() : G;
156
- }
157
- /**
158
- * Extract edges from a graph object
159
- *
160
- * @param G - Graph or list of nodes
161
- * @returns Array of edges
162
- */
163
- function getEdgesFromGraph(G) {
164
- return G.edges ? G.edges() : [];
165
- }
166
- // Helper function similar to _process_params in Python version
167
- function _processParams(G, center, dim) {
168
- if (!center) {
169
- center = Array(dim).fill(0);
170
- }
171
- if (center.length !== dim) {
172
- throw new Error("length of center coordinates must match dimension of layout");
173
- }
174
- return { G, center };
175
- }
176
- /**
177
- * Position nodes uniformly at random in the unit square.
178
- *
179
- * @param G - Graph or list of nodes
180
- * @param center - Coordinate pair around which to center the layout
181
- * @param dim - Dimension of layout
182
- * @param seed - Random seed for reproducible layouts
183
- * @returns Positions dictionary keyed by node
184
- */
185
- function randomLayout(G, center = null, dim = 2, seed = null) {
186
- const processed = _processParams(G, center, dim);
187
- const nodes = getNodesFromGraph(processed.G);
188
- center = processed.center;
189
- const rng = new RandomNumberGenerator(seed ?? undefined);
190
- const pos = {};
191
- nodes.forEach((node) => {
192
- pos[node] = rng.rand(dim).map((val, i) => val + center[i]);
193
- });
194
- return pos;
195
- }
196
- /**
197
- * Position nodes on a circle (2D) or sphere (3D).
198
- *
199
- * @param G - Graph or list of nodes
200
- * @param scale - Scale factor for positions
201
- * @param center - Coordinate pair around which to center the layout
202
- * @param dim - Dimension of layout (supports 2D circle or 3D sphere)
203
- * @returns Positions dictionary keyed by node
204
- */
205
- function circularLayout(G, scale = 1, center = null, dim = 2) {
206
- if (dim < 2) {
207
- throw new Error("cannot handle dimensions < 2");
208
- }
209
- const processed = _processParams(G, center, dim);
210
- const nodes = getNodesFromGraph(processed.G);
211
- center = processed.center;
212
- const pos = {};
213
- if (nodes.length === 0) {
214
- return pos;
215
- }
216
- if (nodes.length === 1) {
217
- pos[nodes[0]] = center;
218
- return pos;
219
- }
220
- if (dim === 2) {
221
- // 2D circle layout
222
- const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
223
- nodes.forEach((node, i) => {
224
- const x = Math.cos(theta[i]) * scale + center[0];
225
- const y = Math.sin(theta[i]) * scale + center[1];
226
- pos[node] = [x, y];
227
- });
228
- }
229
- else if (dim === 3) {
230
- // 3D sphere layout using Fibonacci spiral
231
- const n = nodes.length;
232
- const goldenRatio = (1 + Math.sqrt(5)) / 2;
233
- nodes.forEach((node, i) => {
234
- // Use Fibonacci spiral for even distribution on sphere
235
- const theta = 2 * Math.PI * i / goldenRatio;
236
- const phi = Math.acos(1 - 2 * (i + 0.5) / n);
237
- const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
238
- const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
239
- const z = Math.cos(phi) * scale + center[2];
240
- pos[node] = [x, y, z];
241
- });
242
- }
243
- else {
244
- // For higher dimensions, fall back to random on hypersphere
245
- const rng = new RandomNumberGenerator();
246
- nodes.forEach((node) => {
247
- // Generate random point on unit hypersphere
248
- const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
249
- const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
250
- pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
251
- });
252
- }
253
- return pos;
254
- }
255
- /**
256
- * Position nodes in concentric circles.
257
- *
258
- * @param G - Graph or list of nodes
259
- * @param nlist - List of node lists for each shell
260
- * @param scale - Scale factor for positions
261
- * @param center - Coordinate pair around which to center the layout
262
- * @param dim - Dimension of layout (currently only supports dim=2)
263
- * @returns Positions dictionary keyed by node
264
- */
265
- function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
266
- if (dim !== 2) {
267
- throw new Error("can only handle 2 dimensions");
268
- }
269
- const processed = _processParams(G, center, dim);
270
- const nodes = getNodesFromGraph(processed.G);
271
- center = processed.center;
272
- const pos = {};
273
- if (nodes.length === 0) {
274
- return pos;
275
- }
276
- if (nodes.length === 1) {
277
- pos[nodes[0]] = center;
278
- return pos;
279
- }
280
- // If no nlist is specified, put all nodes in a single shell
281
- if (!nlist) {
282
- nlist = [nodes];
283
- }
284
- const radiusBump = scale / nlist.length;
285
- let radius;
286
- if (nlist[0].length === 1) {
287
- // Single node at center
288
- radius = 0;
289
- pos[nlist[0][0]] = [...center];
290
- radius += radiusBump;
291
- }
292
- else {
293
- // Start at radius 1
294
- radius = radiusBump;
295
- }
296
- for (let i = 0; i < nlist.length; i++) {
297
- const shell = nlist[i];
298
- if (shell.length === 0)
299
- continue;
300
- if (shell.length === 1 && i === 0) {
301
- // Already handled the case of a single center node
302
- continue;
303
- }
304
- // Calculate positions on a circle
305
- const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
306
- shell.forEach((node, j) => {
307
- const x = Math.cos(theta[j]) * radius + center[0];
308
- const y = Math.sin(theta[j]) * radius + center[1];
309
- pos[node] = [x, y];
310
- });
311
- radius += radiusBump;
312
- }
313
- return pos;
314
- }
315
- /**
316
- * Position nodes using Fruchterman-Reingold force-directed algorithm.
317
- *
318
- * @param {Object} G - Graph or list of nodes
319
- * @param {number} k - Optimal distance between nodes
320
- * @param {Object} pos - Initial positions for nodes
321
- * @param {Array} fixed - Nodes to keep fixed at initial position
322
- * @param {number} iterations - Maximum number of iterations
323
- * @param {number} scale - Scale factor for positions
324
- * @param {Array|null} center - Coordinate pair around which to center the layout
325
- * @param {number} dim - Dimension of layout
326
- * @param {number} seed - Random seed for initial positions
327
- * @returns {Object} Positions dictionary keyed by node
328
- */
329
- function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
330
- // Legacy compatibility alias
331
- return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
332
- }
333
- /**
334
- * Position nodes using Fruchterman-Reingold force-directed algorithm.
335
- *
336
- * @param {Object} G - Graph or list of nodes
337
- * @param {number} k - Optimal distance between nodes
338
- * @param {Object} pos - Initial positions for nodes
339
- * @param {Array} fixed - Nodes to keep fixed at initial position
340
- * @param {number} iterations - Maximum number of iterations
341
- * @param {number} scale - Scale factor for positions
342
- * @param {Array|null} center - Coordinate pair around which to center the layout
343
- * @param {number} dim - Dimension of layout
344
- * @param {number} seed - Random seed for initial positions
345
- * @returns {Object} Positions dictionary keyed by node
346
- */
347
- function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
348
- const processed = _processParams(G, center, dim);
349
- let graph = processed.G;
350
- center = processed.center;
351
- const nodes = getNodesFromGraph(graph);
352
- const edges = getEdgesFromGraph(graph);
353
- if (nodes.length === 0) {
354
- return {};
355
- }
356
- if (nodes.length === 1) {
357
- const singlePos = {};
358
- singlePos[nodes[0]] = center;
359
- return singlePos;
360
- }
361
- // Set up initial positions
362
- let positions = {};
363
- if (pos) {
364
- // Use provided positions
365
- for (const node of nodes) {
366
- if (pos[node]) {
367
- positions[node] = [...pos[node]];
368
- }
369
- else {
370
- const rng = new RandomNumberGenerator(seed ?? undefined);
371
- positions[node] = rng.rand(dim);
372
- }
373
- }
374
- }
375
- else {
376
- // Random initial positions
377
- const rng = new RandomNumberGenerator(seed ?? undefined);
378
- for (const node of nodes) {
379
- positions[node] = rng.rand(dim);
380
- }
381
- }
382
- // Set up fixed nodes
383
- const fixedNodes = new Set(fixed || []);
384
- // Optimal distance between nodes
385
- if (!k) {
386
- k = 1.0 / Math.sqrt(nodes.length);
387
- }
388
- // Initialize temperature
389
- let t = 0.1;
390
- // Calculate temperature reduction
391
- const dt = t / (iterations + 1);
392
- // Simple cooling schedule
393
- for (let i = 0; i < iterations; i++) {
394
- // Calculate repulsive forces
395
- const displacement = {};
396
- for (const node of nodes) {
397
- displacement[node] = Array(dim).fill(0);
398
- }
399
- // Repulsive forces between nodes
400
- for (let v1i = 0; v1i < nodes.length; v1i++) {
401
- const v1 = nodes[v1i];
402
- for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
403
- const v2 = nodes[v2i];
404
- // Difference vector
405
- const delta = positions[v1].map((p, i) => p - positions[v2][i]);
406
- // Distance
407
- const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
408
- // Force
409
- const force = (k * k) / distance;
410
- // Add force to displacement
411
- for (let j = 0; j < dim; j++) {
412
- const direction = delta[j] / distance;
413
- displacement[v1][j] += direction * force;
414
- displacement[v2][j] -= direction * force;
415
- }
416
- }
417
- }
418
- // Attractive forces between connected nodes
419
- for (const [source, target] of edges) {
420
- // Difference vector
421
- const delta = positions[source].map((p, i) => p - positions[target][i]);
422
- // Distance
423
- const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
424
- // Force
425
- const force = (distance * distance) / k;
426
- // Add force to displacement
427
- for (let j = 0; j < dim; j++) {
428
- const direction = delta[j] / distance;
429
- displacement[source][j] -= direction * force;
430
- displacement[target][j] += direction * force;
431
- }
432
- }
433
- // Update positions
434
- for (const node of nodes) {
435
- if (fixedNodes.has(node))
436
- continue;
437
- // Calculate displacement magnitude
438
- const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
439
- // Limit maximum displacement by temperature
440
- const limitedMagnitude = Math.min(magnitude, t);
441
- // Update position
442
- for (let j = 0; j < dim; j++) {
443
- const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
444
- positions[node][j] += direction * limitedMagnitude;
445
- }
446
- }
447
- // Cool temperature
448
- t -= dt;
449
- }
450
- // Rescale positions
451
- if (!fixed) {
452
- positions = rescaleLayout(positions, scale, center);
453
- }
454
- return positions;
455
- }
456
- /**
457
- * Position nodes in a spectral layout using eigenvectors of the graph Laplacian.
458
- *
459
- * @param G - Graph
460
- * @param scale - Scale factor for positions
461
- * @param center - Coordinate pair around which to center the layout
462
- * @param dim - Dimension of layout
463
- * @returns Positions dictionary keyed by node
464
- */
465
- function spectralLayout(G, scale = 1, center = null, dim = 2) {
466
- const processed = _processParams(G, center, dim);
467
- const graph = processed.G;
468
- center = processed.center;
469
- const nodes = getNodesFromGraph(graph);
470
- if (nodes.length <= 2) {
471
- if (nodes.length === 0) {
472
- return {};
473
- }
474
- else if (nodes.length === 1) {
475
- return { [nodes[0]]: center };
476
- }
477
- else {
478
- return {
479
- [nodes[0]]: center.map(v => v - scale),
480
- [nodes[1]]: center.map(v => v + scale)
481
- };
482
- }
483
- }
484
- // Create adjacency matrix
485
- const N = nodes.length;
486
- const nodeIndices = {};
487
- nodes.forEach((node, i) => { nodeIndices[node] = i; });
488
- const A = Array(N).fill(0).map(() => Array(N).fill(0));
489
- const edges = getEdgesFromGraph(graph);
490
- for (const [source, target] of edges) {
491
- const i = nodeIndices[source];
492
- const j = nodeIndices[target];
493
- A[i][j] = 1;
494
- A[j][i] = 1; // Make symmetric for undirected graphs
495
- }
496
- // Create Laplacian matrix: L = D - A where D is degree matrix
497
- const L = Array(N).fill(0).map(() => Array(N).fill(0));
498
- for (let i = 0; i < N; i++) {
499
- // Compute degree (sum of row)
500
- L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
501
- for (let j = 0; j < N; j++) {
502
- L[i][j] -= A[i][j];
503
- }
504
- }
505
- // Compute eigenvectors using power iteration method
506
- // We need the smallest non-zero eigenvectors of L
507
- const eigenvectors = [];
508
- // For each dimension, find an eigenvector
509
- for (let d = 0; d < dim; d++) {
510
- let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
511
- // Orthogonalize against previous eigenvectors
512
- for (const ev of eigenvectors) {
513
- const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
514
- vector = vector.map((val, idx) => val - dot * ev[idx]);
515
- }
516
- // Normalize
517
- const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
518
- vector = vector.map(val => val / norm);
519
- // Apply shifted inverse iteration to find smallest non-zero eigenvector
520
- // This is a simplification of the actual algorithm
521
- for (let iter = 0; iter < 100; iter++) {
522
- // Apply Laplacian
523
- const newVec = Array(N).fill(0);
524
- for (let i = 0; i < N; i++) {
525
- for (let j = 0; j < N; j++) {
526
- newVec[i] += L[i][j] * vector[j];
527
- }
528
- }
529
- // Orthogonalize against the constant vector (eigenvector with eigenvalue 0)
530
- const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
531
- newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; });
532
- // Normalize
533
- const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
534
- if (newNorm < 1e-10)
535
- continue; // Skip if vector is close to zero
536
- vector = newVec.map(val => val / newNorm);
537
- }
538
- eigenvectors.push(vector);
539
- }
540
- // Create position array from eigenvectors
541
- const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
542
- for (let i = 0; i < N; i++) {
543
- for (let d = 0; d < dim; d++) {
544
- positions[i][d] = eigenvectors[d][i];
545
- }
546
- }
547
- // Rescale and create position dictionary
548
- const scaledPositions = rescaleLayout(positions, scale);
549
- const pos = {};
550
- nodes.forEach((node, i) => {
551
- pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
552
- });
553
- return pos;
554
- }
555
- /**
556
- * Position nodes in a spiral layout.
557
- *
558
- * @param G - Graph or list of nodes
559
- * @param scale - Scale factor for positions
560
- * @param center - Coordinate pair around which to center the layout
561
- * @param dim - Dimension of layout
562
- * @param resolution - Controls the spacing between spiral elements
563
- * @param equidistant - Whether to place nodes equidistant from each other
564
- * @returns Positions dictionary keyed by node
565
- */
566
- function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
567
- if (dim !== 2) {
568
- throw new Error("can only handle 2 dimensions");
569
- }
570
- const processed = _processParams(G, center || [0, 0], dim);
571
- const nodes = getNodesFromGraph(processed.G);
572
- center = processed.center;
573
- const pos = {};
574
- if (nodes.length === 0) {
575
- return pos;
576
- }
577
- if (nodes.length === 1) {
578
- pos[nodes[0]] = [...center];
579
- return pos;
580
- }
581
- let positions = [];
582
- if (equidistant) {
583
- // Create equidistant points along the spiral
584
- // This matches the Python implementation logic
585
- const chord = 1;
586
- const step = 0.5;
587
- let theta = resolution;
588
- theta += chord / (step * theta);
589
- for (let i = 0; i < nodes.length; i++) {
590
- const r = step * theta;
591
- theta += chord / r;
592
- positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
593
- }
594
- }
595
- else {
596
- // Create points with equal angle but increasing distance
597
- const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
598
- const angle = dist.map(d => resolution * d);
599
- positions = dist.map((d, i) => [
600
- Math.cos(angle[i]) * d,
601
- Math.sin(angle[i]) * d
602
- ]);
603
- }
604
- // Convert position array to position matrix for rescaling
605
- const posArray = [];
606
- for (let i = 0; i < positions.length; i++) {
607
- posArray.push(positions[i]);
608
- }
609
- // Rescale positions and add center offset
610
- const scaledPositions = rescaleLayout(posArray, scale);
611
- for (let i = 0; i < scaledPositions.length; i++) {
612
- scaledPositions[i][0] += center[0];
613
- scaledPositions[i][1] += center[1];
614
- }
615
- // Create position dictionary
616
- for (let i = 0; i < nodes.length; i++) {
617
- pos[nodes[i]] = scaledPositions[i];
618
- }
619
- return pos;
620
- }
621
- /**
622
- * Rescale node positions to fit in the specified scale and center.
623
- *
624
- * @param pos - Dictionary or array of positions
625
- * @param scale - Scale factor for positions
626
- * @param center - Coordinate pair around which to center the layout
627
- * @returns Rescaled positions dictionary
628
- */
629
- function rescaleLayout(pos, scale = 1, center = [0, 0]) {
630
- // Check if pos is empty
631
- if (Array.isArray(pos)) {
632
- if (pos.length === 0)
633
- return [];
634
- }
635
- else {
636
- if (Object.keys(pos).length === 0)
637
- return {};
638
- }
639
- // Extract position values
640
- const posValues = Array.isArray(pos) ? pos : Object.values(pos);
641
- const dim = posValues[0].length;
642
- // Calculate center of positions
643
- const posCenter = Array(dim).fill(0);
644
- for (const p of posValues) {
645
- for (let i = 0; i < dim; i++) {
646
- posCenter[i] += p[i] / posValues.length;
647
- }
648
- }
649
- // Center positions
650
- let centeredPos = {};
651
- if (Array.isArray(pos)) {
652
- centeredPos = pos.map(p => p.map((val, i) => val - posCenter[i]));
653
- }
654
- else {
655
- for (const [node, p] of Object.entries(pos)) {
656
- centeredPos[node] = p.map((val, i) => val - posCenter[i]);
657
- }
658
- }
659
- // Find maximum distance from center
660
- let maxDistance = 0;
661
- const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
662
- for (const p of centeredValues) {
663
- const distance = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
664
- maxDistance = Math.max(maxDistance, distance);
665
- }
666
- // Rescale
667
- let scaledPos = Array.isArray(pos) ? [] : {};
668
- if (maxDistance > 0) {
669
- const scaleFactor = scale / maxDistance;
670
- if (Array.isArray(pos)) {
671
- scaledPos = centeredPos.map(p => p.map((val, i) => val * scaleFactor + center[i]));
672
- }
673
- else {
674
- for (const [node, p] of Object.entries(centeredPos)) {
675
- scaledPos[node] = p.map((val, i) => val * scaleFactor + center[i]);
676
- }
677
- }
678
- }
679
- else {
680
- // All nodes at the same position
681
- if (Array.isArray(pos)) {
682
- scaledPos = Array(pos.length).fill(0).map(() => [...center]);
683
- }
684
- else {
685
- for (const node of Object.keys(pos)) {
686
- scaledPos[node] = [...center];
687
- }
688
- }
689
- }
690
- return scaledPos;
691
- }
692
- /**
693
- * Position nodes in two straight lines (bipartite layout).
694
- *
695
- * @param G - Graph or list of nodes
696
- * @param nodes - Nodes in one node set of the graph
697
- * @param align - The alignment of nodes: 'vertical' or 'horizontal'
698
- * @param scale - Scale factor for positions
699
- * @param center - Coordinate pair around which to center the layout
700
- * @param aspectRatio - The ratio of the width to the height of the layout
701
- * @returns Positions dictionary keyed by node
702
- */
703
- function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center = null, aspectRatio = 4 / 3) {
704
- if (align !== 'vertical' && align !== 'horizontal') {
705
- throw new Error("align must be either vertical or horizontal");
706
- }
707
- const processed = _processParams(G, center || [0, 0], 2);
708
- const graph = processed.G;
709
- center = processed.center;
710
- const allNodes = getNodesFromGraph(graph);
711
- if (allNodes.length === 0) {
712
- return {};
713
- }
714
- // If nodes not provided, try to determine bipartite sets
715
- if (!nodes) {
716
- // A simple heuristic for bipartite detection: use nodes with even/odd indices
717
- // This is a simplification, in Python NetworkX has bipartite.sets()
718
- nodes = allNodes.filter((_, i) => i % 2 === 0);
719
- }
720
- const left = new Set(nodes);
721
- const right = new Set(allNodes.filter((n) => !left.has(n)));
722
- const height = 1;
723
- const width = aspectRatio * height;
724
- const offset = [width / 2, height / 2];
725
- const pos = {};
726
- // Position nodes in the left set
727
- const leftNodes = [...left];
728
- leftNodes.forEach((node, i) => {
729
- const x = 0;
730
- const y = i * height / (leftNodes.length || 1);
731
- pos[node] = [x, y];
732
- });
733
- // Position nodes in the right set
734
- const rightNodes = [...right];
735
- rightNodes.forEach((node, i) => {
736
- const x = width;
737
- const y = i * height / (rightNodes.length || 1);
738
- pos[node] = [x, y];
739
- });
740
- // Center positions around the origin and apply offset
741
- for (const node in pos) {
742
- pos[node][0] -= offset[0];
743
- pos[node][1] -= offset[1];
744
- }
745
- // Rescale positions
746
- const scaledPos = rescaleLayout(pos, scale, center);
747
- // Handle horizontal alignment
748
- if (align === 'horizontal') {
749
- for (const node in scaledPos) {
750
- const temp = scaledPos[node][0];
751
- scaledPos[node][0] = scaledPos[node][1];
752
- scaledPos[node][1] = temp;
753
- }
754
- }
755
- return scaledPos;
756
- }
757
- /**
758
- * Position nodes in layers of straight lines (multipartite layout).
759
- *
760
- * @param G - Graph or list of nodes
761
- * @param subsetKey - Object mapping layers to node sets, or node attribute name
762
- * @param align - The alignment of nodes: 'vertical' or 'horizontal'
763
- * @param scale - Scale factor for positions
764
- * @param center - Coordinate pair around which to center the layout
765
- * @returns Positions dictionary keyed by node
766
- */
767
- function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale = 1, center = null) {
768
- if (align !== 'vertical' && align !== 'horizontal') {
769
- throw new Error("align must be either vertical or horizontal");
770
- }
771
- const processed = _processParams(G, center || [0, 0], 2);
772
- const graph = processed.G;
773
- center = processed.center;
774
- const allNodes = getNodesFromGraph(graph);
775
- if (allNodes.length === 0) {
776
- return {};
777
- }
778
- // Convert subsetKey to a layer mapping if it's a string
779
- let layers = {};
780
- if (typeof subsetKey === 'string') {
781
- // In JS we don't have access to node attributes directly
782
- // This is a simplification - in a real implementation we would need
783
- // to access node attributes from the graph
784
- console.warn("Using string subsetKey requires node attributes, using default partitioning");
785
- // Create a simple partitioning as fallback
786
- layers = { 0: allNodes };
787
- }
788
- else {
789
- // subsetKey is already a mapping of layers to nodes
790
- // Convert single nodes to arrays
791
- for (const [key, value] of Object.entries(subsetKey)) {
792
- if (Array.isArray(value)) {
793
- layers[key] = value;
794
- }
795
- else {
796
- layers[key] = [value];
797
- }
798
- }
799
- }
800
- const layerCount = Object.keys(layers).length;
801
- let pos = {};
802
- // Process each layer
803
- Object.entries(layers).forEach(([layer, nodes], layerIdx) => {
804
- const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
805
- const layerSize = layerNodes.length;
806
- layerNodes.forEach((node, nodeIdx) => {
807
- // Place nodes in a grid: layerIdx determines x-coordinate (column)
808
- // nodeIdx determines y-coordinate (row position within column)
809
- const x = layerIdx - (layerCount - 1) / 2;
810
- const y = nodeIdx - (layerSize - 1) / 2;
811
- pos[node] = [x, y];
812
- });
813
- });
814
- // Rescale positions
815
- pos = rescaleLayout(pos, scale, center);
816
- // Handle horizontal alignment
817
- if (align === 'horizontal') {
818
- for (const node in pos) {
819
- const temp = pos[node][0];
820
- pos[node][0] = pos[node][1];
821
- pos[node][1] = temp;
822
- }
823
- }
824
- return pos;
825
- }
826
- /**
827
- * Position nodes according to breadth-first search algorithm.
828
- *
829
- * @param G - Graph
830
- * @param start - Starting node for bfs
831
- * @param align - The alignment of layers: 'vertical' or 'horizontal'
832
- * @param scale - Scale factor for positions
833
- * @param center - Coordinate pair around which to center the layout
834
- * @returns Positions dictionary keyed by node
835
- */
836
- function bfsLayout(G, start, align = 'vertical', scale = 1, center = null) {
837
- const processed = _processParams(G, center || [0, 0], 2);
838
- const graph = processed.G;
839
- center = processed.center;
840
- const allNodes = getNodesFromGraph(graph);
841
- if (allNodes.length === 0) {
842
- return {};
843
- }
844
- // Compute BFS layers
845
- const layers = {};
846
- const visited = new Set();
847
- let currentLayer = 0;
848
- // Starting layer
849
- layers[currentLayer] = [start];
850
- visited.add(start);
851
- // BFS traversal
852
- while (Object.values(layers).flat().length < allNodes.length) {
853
- const nextLayer = [];
854
- const currentNodes = layers[currentLayer];
855
- for (const node of currentNodes) {
856
- // Get neighbors - this is a simplified approach
857
- // In a real implementation, we would get neighbors from the graph
858
- const neighbors = getNeighbors(graph, node);
859
- for (const neighbor of neighbors) {
860
- if (!visited.has(neighbor)) {
861
- nextLayer.push(neighbor);
862
- visited.add(neighbor);
863
- }
864
- }
865
- }
866
- if (nextLayer.length === 0) {
867
- // No more connected nodes
868
- const unvisited = allNodes.filter((node) => !visited.has(node));
869
- if (unvisited.length > 0) {
870
- throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
871
- }
872
- break;
873
- }
874
- currentLayer++;
875
- layers[currentLayer] = nextLayer;
876
- }
877
- // Use multipartite_layout to position the layers
878
- return multipartiteLayout(graph, layers, align, scale, center);
879
- // Helper function to get neighbors
880
- function getNeighbors(graph, node) {
881
- if (!graph.edges)
882
- return [];
883
- return graph.edges()
884
- .filter((edge) => edge[0] === node || edge[1] === node)
885
- .map((edge) => edge[0] === node ? edge[1] : edge[0]);
886
- }
887
- }
888
- /**
889
- * Position nodes without edge intersections (planar layout).
890
- *
891
- * @param G - Graph
892
- * @param scale - Scale factor for positions
893
- * @param center - Coordinate pair around which to center the layout
894
- * @param dim - Dimension of layout (must be 2)
895
- * @returns Positions dictionary keyed by node
896
- */
897
- function planarLayout(G, scale = 1, center = null, dim = 2) {
898
- if (dim !== 2) {
899
- throw new Error("can only handle 2 dimensions");
900
- }
901
- const processed = _processParams(G, center || [0, 0], dim);
902
- const graph = processed.G;
903
- center = processed.center;
904
- const nodes = getNodesFromGraph(graph);
905
- const edges = getEdgesFromGraph(graph);
906
- if (nodes.length === 0) {
907
- return {};
908
- }
909
- // Check if graph is planar and get embedding
910
- const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
911
- if (!isPlanar) {
912
- throw new Error("G is not planar.");
913
- }
914
- if (!embedding) {
915
- throw new Error("Failed to generate planar embedding.");
916
- }
917
- // Convert embedding to positions
918
- let pos = combinatorialEmbeddingToPos(embedding, nodes);
919
- // Rescale the positions
920
- pos = rescaleLayout(pos, scale, center);
921
- return pos;
922
- }
923
- /**
924
- * Check if graph is planar using a simplified version of Boyer-Myrvold algorithm.
925
- * Returns planarity and embedding information.
926
- *
927
- * @param G - Graph
928
- * @param nodes - List of nodes
929
- * @param edges - List of edges
930
- * @returns Object containing isPlanar flag and embedding
931
- */
932
- function checkPlanarity(G, nodes, edges) {
933
- // For small graphs (n <= 4), all are planar
934
- if (nodes.length <= 4) {
935
- return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
936
- }
937
- // For K5 (complete graph with 5 nodes) and K3,3 (complete bipartite with 3,3 nodes)
938
- // these are not planar by Kuratowski's theorem
939
- if (isK5(nodes, edges) || isK33(nodes, edges)) {
940
- return { isPlanar: false, embedding: null };
941
- }
942
- // For other graphs, use LR algorithm (Left-Right Planarity Test)
943
- const result = lrPlanarityTest(nodes, edges);
944
- return result;
945
- }
946
- /**
947
- * Check if graph is K5 (complete graph with 5 nodes)
948
- *
949
- * @param nodes - List of nodes
950
- * @param edges - List of edges
951
- * @returns True if graph is K5
952
- */
953
- function isK5(nodes, edges) {
954
- if (nodes.length !== 5)
955
- return false;
956
- // K5 has exactly 10 edges
957
- if (edges.length !== 10)
958
- return false;
959
- // Check if every pair of distinct nodes is connected
960
- for (let i = 0; i < nodes.length; i++) {
961
- for (let j = i + 1; j < nodes.length; j++) {
962
- const hasEdge = edges.some(e => (e[0] === nodes[i] && e[1] === nodes[j]) ||
963
- (e[0] === nodes[j] && e[1] === nodes[i]));
964
- if (!hasEdge)
965
- return false;
966
- }
967
- }
968
- return true;
969
- }
970
- /**
971
- * Check if graph is K3,3 (complete bipartite with 3,3 nodes)
972
- *
973
- * @param nodes - List of nodes
974
- * @param edges - List of edges
975
- * @returns True if graph is K3,3
976
- */
977
- function isK33(nodes, edges) {
978
- if (nodes.length !== 6)
979
- return false;
980
- // K3,3 has exactly 9 edges
981
- if (edges.length !== 9)
982
- return false;
983
- // Try to find a bipartite partition
984
- const nodePartitions = tryFindBipartitePartition(nodes, edges);
985
- if (!nodePartitions)
986
- return false;
987
- const [part1, part2] = nodePartitions;
988
- // Check if both partitions have size 3
989
- if (part1.length !== 3 || part2.length !== 3)
990
- return false;
991
- // Check if every node in part1 is connected to every node in part2
992
- for (const n1 of part1) {
993
- for (const n2 of part2) {
994
- const hasEdge = edges.some(e => (e[0] === n1 && e[1] === n2) ||
995
- (e[0] === n2 && e[1] === n1));
996
- if (!hasEdge)
997
- return false;
998
- }
999
- }
1000
- return true;
1001
- }
1002
- /**
1003
- * Try to find a bipartite partition of the nodes
1004
- *
1005
- * @param nodes - List of nodes
1006
- * @param edges - List of edges
1007
- * @returns Array of two partitions, or null if not bipartite
1008
- */
1009
- function tryFindBipartitePartition(nodes, edges) {
1010
- const colorMap = {};
1011
- const adjList = {};
1012
- // Create adjacency list
1013
- for (const node of nodes) {
1014
- adjList[node] = [];
1015
- }
1016
- for (const [u, v] of edges) {
1017
- adjList[u].push(v);
1018
- adjList[v].push(u);
1019
- }
1020
- // BFS to color nodes
1021
- const queue = [nodes[0]];
1022
- colorMap[nodes[0]] = 0;
1023
- while (queue.length > 0) {
1024
- const node = queue.shift();
1025
- const nodeColor = colorMap[node];
1026
- for (const neighbor of adjList[node]) {
1027
- if (colorMap[neighbor] === undefined) {
1028
- colorMap[neighbor] = 1 - nodeColor; // Toggle color (0/1)
1029
- queue.push(neighbor);
1030
- }
1031
- else if (colorMap[neighbor] === nodeColor) {
1032
- // Conflict: not bipartite
1033
- return null;
1034
- }
1035
- }
1036
- }
1037
- // Create partitions
1038
- const part0 = [];
1039
- const part1 = [];
1040
- for (const node of nodes) {
1041
- if (colorMap[node] === 0) {
1042
- part0.push(node);
1043
- }
1044
- else {
1045
- part1.push(node);
1046
- }
1047
- }
1048
- return [part0, part1];
1049
- }
1050
- /**
1051
- * Left-Right Planarity Test for general graphs
1052
- *
1053
- * @param nodes - List of nodes
1054
- * @param edges - List of edges
1055
- * @returns Object containing isPlanar flag and embedding
1056
- */
1057
- function lrPlanarityTest(nodes, edges) {
1058
- // Create adjacency list for the graph
1059
- const adjList = {};
1060
- for (const node of nodes) {
1061
- adjList[node] = [];
1062
- }
1063
- for (const [u, v] of edges) {
1064
- adjList[u].push(v);
1065
- adjList[v].push(u);
1066
- }
1067
- // Step 1: Perform DFS to get an st-numbering (ordering of nodes)
1068
- const visited = new Set();
1069
- const ordering = [];
1070
- function dfs(node) {
1071
- visited.add(node);
1072
- ordering.push(node);
1073
- for (const neighbor of adjList[node]) {
1074
- if (!visited.has(neighbor)) {
1075
- dfs(neighbor);
1076
- }
1077
- }
1078
- }
1079
- // Start DFS from first node
1080
- dfs(nodes[0]);
1081
- // If the graph is disconnected, it's still planar but we need to handle each component
1082
- if (ordering.length < nodes.length) {
1083
- // Create a simple triangulation embedding for disconnected graphs
1084
- return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
1085
- }
1086
- // Step 2: For a general implementation, we'll use a simplified approach
1087
- // since this would normally require implementing the entire LR algorithm
1088
- // For this implementation, since we can't fully implement Boyer-Myrvold,
1089
- // we'll create a reasonable planar embedding for most planar graphs
1090
- // We assume the graph is planar if it's sparse enough (|E| <= 3|V| - 6)
1091
- // This is a necessary but not sufficient condition for planar graphs
1092
- if (edges.length > 3 * nodes.length - 6) {
1093
- return { isPlanar: false, embedding: null };
1094
- }
1095
- // Create a planar embedding using a triangulation approach
1096
- const embedding = createTriangulationEmbedding(nodes, edges);
1097
- return { isPlanar: true, embedding };
1098
- }
1099
- /**
1100
- * Create a triangulation-based embedding for a planar graph
1101
- *
1102
- * @param nodes - List of nodes
1103
- * @param edges - List of edges
1104
- * @returns Embedding object
1105
- */
1106
- function createTriangulationEmbedding(nodes, edges) {
1107
- // Create a simple embedding using the incremental approach
1108
- const embedding = {
1109
- nodeOrder: [...nodes],
1110
- faceList: [],
1111
- nodePositions: {}
1112
- };
1113
- // Create a map of adjacent nodes
1114
- const adjMap = {};
1115
- for (const node of nodes) {
1116
- adjMap[node] = new Set();
1117
- }
1118
- for (const [u, v] of edges) {
1119
- adjMap[u].add(v);
1120
- adjMap[v].add(u);
1121
- }
1122
- // Create outer face as a cycle (if possible)
1123
- const outerFace = findCycle(nodes, edges, adjMap) || nodes;
1124
- embedding.faceList.push(outerFace);
1125
- // Position nodes on a convex polygon (outer face)
1126
- const n = outerFace.length;
1127
- for (let i = 0; i < n; i++) {
1128
- const angle = 2 * Math.PI * i / n;
1129
- embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
1130
- }
1131
- // Position interior nodes using barycentric coordinates
1132
- const interiorNodes = nodes.filter(node => !embedding.nodePositions[node]);
1133
- for (const node of interiorNodes) {
1134
- const neighbors = Array.from(adjMap[node]);
1135
- if (neighbors.length === 0) {
1136
- // Isolated node, place at center
1137
- embedding.nodePositions[node] = [0, 0];
1138
- }
1139
- else {
1140
- // Average position of neighbors that have positions
1141
- let xSum = 0, ySum = 0, count = 0;
1142
- for (const neighbor of neighbors) {
1143
- if (embedding.nodePositions[neighbor]) {
1144
- xSum += embedding.nodePositions[neighbor][0];
1145
- ySum += embedding.nodePositions[neighbor][1];
1146
- count++;
1147
- }
1148
- }
1149
- if (count > 0) {
1150
- // Place slightly away from center to avoid overlaps
1151
- const jitter = 0.1 * Math.random();
1152
- embedding.nodePositions[node] = [
1153
- xSum / count + jitter * (Math.random() - 0.5),
1154
- ySum / count + jitter * (Math.random() - 0.5)
1155
- ];
1156
- }
1157
- else {
1158
- // No neighbors have positions yet, place randomly inside unit circle
1159
- const r = 0.5 * Math.random();
1160
- const angle = 2 * Math.PI * Math.random();
1161
- embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
1162
- }
1163
- }
1164
- }
1165
- return embedding;
1166
- }
1167
- /**
1168
- * Find a simple cycle in the graph (for outer face)
1169
- *
1170
- * @param nodes - List of nodes
1171
- * @param edges - List of edges
1172
- * @param adjMap - Adjacency map
1173
- * @returns Cycle as array of nodes, or null if none found
1174
- */
1175
- function findCycle(nodes, edges, adjMap) {
1176
- if (nodes.length === 0)
1177
- return null;
1178
- if (nodes.length <= 2)
1179
- return nodes; // Not a real cycle but handle it
1180
- // Try to find a Hamiltonian cycle for simplicity (for small graphs)
1181
- if (nodes.length <= 8) {
1182
- const visited = new Set();
1183
- const path = [];
1184
- function hamiltonianCycleDFS(node) {
1185
- path.push(node);
1186
- visited.add(node);
1187
- if (path.length === nodes.length) {
1188
- // Check if it's a cycle (last node connects to first)
1189
- if (adjMap[node].has(path[0])) {
1190
- return true;
1191
- }
1192
- // Not a cycle
1193
- visited.delete(node);
1194
- path.pop();
1195
- return false;
1196
- }
1197
- for (const neighbor of adjMap[node]) {
1198
- if (!visited.has(neighbor)) {
1199
- if (hamiltonianCycleDFS(neighbor)) {
1200
- return true;
1201
- }
1202
- }
1203
- }
1204
- visited.delete(node);
1205
- path.pop();
1206
- return false;
1207
- }
1208
- if (hamiltonianCycleDFS(nodes[0])) {
1209
- return path;
1210
- }
1211
- }
1212
- // Fallback: try to find any cycle using DFS
1213
- const visited = new Set();
1214
- const parent = {};
1215
- let cycleFound = null;
1216
- function findCycleDFS(node, parentNode) {
1217
- visited.add(node);
1218
- for (const neighbor of adjMap[node]) {
1219
- if (neighbor === parentNode)
1220
- continue;
1221
- if (visited.has(neighbor)) {
1222
- // Found a cycle
1223
- cycleFound = constructCycle(node, neighbor, parent);
1224
- return true;
1225
- }
1226
- parent[neighbor] = node;
1227
- if (findCycleDFS(neighbor, node)) {
1228
- return true;
1229
- }
1230
- }
1231
- return false;
1232
- }
1233
- function constructCycle(u, v, parent) {
1234
- const cycle = [v, u];
1235
- let current = u;
1236
- while (parent[current] !== undefined && parent[current] !== v) {
1237
- current = parent[current];
1238
- cycle.push(current);
1239
- }
1240
- return cycle;
1241
- }
1242
- // Try to find a cycle
1243
- for (const node of nodes) {
1244
- if (!visited.has(node)) {
1245
- parent[node] = null;
1246
- if (findCycleDFS(node, null)) {
1247
- break;
1248
- }
1249
- }
1250
- }
1251
- return cycleFound || nodes; // Fallback to all nodes if no cycle found
1252
- }
1253
- /**
1254
- * Convert a combinatorial embedding to node positions
1255
- *
1256
- * @param embedding - The embedding object
1257
- * @param nodes - List of nodes
1258
- * @returns Dictionary mapping nodes to positions
1259
- */
1260
- function combinatorialEmbeddingToPos(embedding, nodes) {
1261
- const pos = {};
1262
- // Use the positions from the embedding
1263
- for (const node of nodes) {
1264
- if (embedding.nodePositions[node]) {
1265
- pos[node] = embedding.nodePositions[node];
1266
- }
1267
- else {
1268
- // Fallback for any nodes without positions
1269
- pos[node] = [0, 0];
1270
- }
1271
- }
1272
- return pos;
1273
- }
1274
- /**
1275
- * Position nodes using Kamada-Kawai path-length cost-function.
1276
- *
1277
- * @param G - NetworkX graph or list of nodes
1278
- * @param dist - A two-level dictionary of optimal distances between nodes
1279
- * @param pos - Initial positions for nodes
1280
- * @param weight - The edge attribute used for edge weights
1281
- * @param scale - Scale factor for positions
1282
- * @param center - Coordinate pair around which to center the layout
1283
- * @param dim - Dimension of layout
1284
- * @returns Positions dictionary keyed by node
1285
- */
1286
- function kamadaKawaiLayout(G, dist = null, pos = null, weight = 'weight', scale = 1, center = null, dim = 2) {
1287
- const processed = _processParams(G, center, dim);
1288
- const graph = processed.G;
1289
- center = processed.center;
1290
- const nodes = getNodesFromGraph(graph);
1291
- if (nodes.length === 0) {
1292
- return {};
1293
- }
1294
- if (nodes.length === 1) {
1295
- return { [nodes[0]]: center };
1296
- }
1297
- // Initialize distance matrix
1298
- if (!dist) {
1299
- dist = _computeShortestPathDistances(graph, weight);
1300
- }
1301
- // Convert distances to a matrix
1302
- const nodesArray = Array.from(nodes);
1303
- const nNodes = nodesArray.length;
1304
- const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
1305
- for (let i = 0; i < nNodes; i++) {
1306
- const nodeI = nodesArray[i];
1307
- distMatrix[i][i] = 0;
1308
- if (!dist[nodeI])
1309
- continue;
1310
- for (let j = 0; j < nNodes; j++) {
1311
- const nodeJ = nodesArray[j];
1312
- if (dist[nodeI][nodeJ] !== undefined) {
1313
- distMatrix[i][j] = dist[nodeI][nodeJ];
1314
- }
1315
- }
1316
- }
1317
- // Initialize positions if not provided
1318
- if (!pos) {
1319
- if (dim >= 2) {
1320
- // Use circular/spherical layout for 2D and 3D
1321
- pos = circularLayout(G, 1, center, dim);
1322
- }
1323
- else {
1324
- // For 1D, use a linear layout
1325
- const posArray = {};
1326
- nodesArray.forEach((node, i) => {
1327
- posArray[node] = [i / (nNodes - 1 || 1)];
1328
- });
1329
- pos = posArray;
1330
- }
1331
- }
1332
- // Convert positions to array for computation
1333
- const posArray = new Array(nNodes);
1334
- for (let i = 0; i < nNodes; i++) {
1335
- const node = nodesArray[i];
1336
- posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
1337
- // Ensure correct dimensionality
1338
- while (posArray[i].length < dim) {
1339
- posArray[i].push(0);
1340
- }
1341
- }
1342
- // Run the Kamada-Kawai algorithm
1343
- const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
1344
- // Convert positions array back to dictionary and rescale
1345
- const finalPos = {};
1346
- for (let i = 0; i < nNodes; i++) {
1347
- finalPos[nodesArray[i]] = newPositions[i];
1348
- }
1349
- return rescaleLayout(finalPos, scale, center);
1350
- }
1351
- /**
1352
- * Compute all-pairs shortest path distances for the graph
1353
- *
1354
- * @param G - NetworkX graph
1355
- * @param weight - Edge attribute for weight
1356
- * @returns Dictionary of dictionaries of shortest path distances
1357
- */
1358
- function _computeShortestPathDistances(G, weight) {
1359
- const distances = {};
1360
- const nodes = G.nodes ? G.nodes() : G;
1361
- const edges = G.edges ? G.edges() : [];
1362
- // Initialize distances with direct edges
1363
- for (const node of nodes) {
1364
- distances[node] = {};
1365
- distances[node][node] = 0;
1366
- for (const other of nodes) {
1367
- if (node !== other) {
1368
- distances[node][other] = Infinity;
1369
- }
1370
- }
1371
- }
1372
- // Add direct edges
1373
- for (const [source, target] of edges) {
1374
- // In a real implementation, we would get the weight from the graph
1375
- // For now, assume weight = 1 or use weight attribute if available
1376
- let edgeWeight = 1;
1377
- if (G.getEdgeData) {
1378
- edgeWeight = G.getEdgeData(source, target, weight) || 1;
1379
- }
1380
- distances[source][target] = edgeWeight;
1381
- distances[target][source] = edgeWeight; // Assuming undirected graph
1382
- }
1383
- // Floyd-Warshall algorithm for all-pairs shortest paths
1384
- for (const k of nodes) {
1385
- for (const i of nodes) {
1386
- for (const j of nodes) {
1387
- if (distances[i][k] + distances[k][j] < distances[i][j]) {
1388
- distances[i][j] = distances[i][k] + distances[k][j];
1389
- }
1390
- }
1391
- }
1392
- }
1393
- return distances;
1394
- }
1395
- /**
1396
- * Solve the Kamada-Kawai layout optimization problem
1397
- *
1398
- * @param distMatrix - Matrix of desired distances between nodes
1399
- * @param positions - Initial node positions
1400
- * @param dim - Dimension of layout
1401
- * @returns Optimized node positions
1402
- */
1403
- function _kamadaKawaiSolve(distMatrix, positions, dim) {
1404
- // Implementation of L-BFGS optimization for Kamada-Kawai
1405
- const nNodes = positions.length;
1406
- const meanWeight = 1e-3;
1407
- // Convert distances to inverse distances (with protection against division by zero)
1408
- const invDistMatrix = distMatrix.map(row => row.map(d => d === 0 ? 0 : 1 / (d + 1e-3)));
1409
- // Flatten positions for optimization
1410
- let posVec = positions.flat();
1411
- // Optimization parameters
1412
- const maxIter = 500;
1413
- const gtol = 1e-5;
1414
- const m = 10; // L-BFGS memory size
1415
- // Implement a simplified L-BFGS-B algorithm
1416
- let alpha = 1.0;
1417
- const oldValues = [];
1418
- const oldGrads = [];
1419
- for (let iter = 0; iter < maxIter; iter++) {
1420
- // Calculate cost and gradient
1421
- const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
1422
- // Compute search direction using L-BFGS approximation
1423
- const direction = _lbfgsDirection(grad, oldValues, oldGrads, m);
1424
- // Simple line search for step size
1425
- alpha = _backtrackingLineSearch(posVec, direction, cost, grad, (x) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0], alpha);
1426
- // Save current position and gradient for next iteration
1427
- const oldPos = [...posVec];
1428
- // Update position
1429
- for (let i = 0; i < posVec.length; i++) {
1430
- posVec[i] += alpha * direction[i];
1431
- }
1432
- // Calculate new gradient
1433
- const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
1434
- // Update L-BFGS memory
1435
- oldValues.push(posVec.map((val, i) => val - oldPos[i]));
1436
- oldGrads.push(newGrad.map((val, i) => val - grad[i]));
1437
- // Keep only m most recent updates
1438
- if (oldValues.length > m) {
1439
- oldValues.shift();
1440
- oldGrads.shift();
1441
- }
1442
- // Check convergence
1443
- const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0));
1444
- if (gradNorm < gtol) {
1445
- break;
1446
- }
1447
- }
1448
- // Reshape result back into positions array
1449
- const result = [];
1450
- for (let i = 0; i < nNodes; i++) {
1451
- result.push(posVec.slice(i * dim, (i + 1) * dim));
1452
- }
1453
- return result;
1454
- }
1455
- /**
1456
- * Cost function and gradient for Kamada-Kawai layout algorithm
1457
- *
1458
- * @param posVec - Flattened position array
1459
- * @param invDist - Inverse distance matrix
1460
- * @param meanWeight - Weight for centering positions
1461
- * @param dim - Dimension of layout
1462
- * @returns Array with [cost, gradient]
1463
- */
1464
- function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
1465
- const nNodes = invDist.length;
1466
- const positions = [];
1467
- // Reshape flat vector into positions array
1468
- for (let i = 0; i < nNodes; i++) {
1469
- positions.push(posVec.slice(i * dim, (i + 1) * dim));
1470
- }
1471
- // Calculate cost
1472
- let cost = 0;
1473
- // Add mean position penalty term
1474
- const sumPos = Array(dim).fill(0);
1475
- for (let i = 0; i < nNodes; i++) {
1476
- for (let d = 0; d < dim; d++) {
1477
- sumPos[d] += positions[i][d];
1478
- }
1479
- }
1480
- cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0);
1481
- // Add distance penalty terms
1482
- for (let i = 0; i < nNodes; i++) {
1483
- for (let j = i + 1; j < nNodes; j++) {
1484
- // Calculate actual distance
1485
- const diff = positions[i].map((val, d) => val - positions[j][d]);
1486
- const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0));
1487
- // Add penalty for difference between actual and ideal distance
1488
- const idealInvDist = invDist[i][j];
1489
- const offset = distance * idealInvDist - 1.0;
1490
- cost += 0.5 * offset * offset;
1491
- }
1492
- }
1493
- // Calculate gradient
1494
- const grad = new Array(posVec.length).fill(0);
1495
- // Add gradient of mean position penalty
1496
- for (let i = 0; i < nNodes; i++) {
1497
- for (let d = 0; d < dim; d++) {
1498
- grad[i * dim + d] += meanWeight * sumPos[d];
1499
- }
1500
- }
1501
- // Add gradient of distance penalties
1502
- for (let i = 0; i < nNodes; i++) {
1503
- for (let j = i + 1; j < nNodes; j++) {
1504
- // Calculate actual distance and direction
1505
- const diff = positions[i].map((val, d) => val - positions[j][d]);
1506
- const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10;
1507
- const direction = diff.map(d => d / distance);
1508
- // Calculate contribution to gradient
1509
- const idealInvDist = invDist[i][j];
1510
- const offset = distance * idealInvDist - 1.0;
1511
- for (let d = 0; d < dim; d++) {
1512
- const force = idealInvDist * offset * direction[d];
1513
- grad[i * dim + d] += force;
1514
- grad[j * dim + d] -= force;
1515
- }
1516
- }
1517
- }
1518
- return [cost, grad];
1519
- }
1520
- /**
1521
- * Compute the search direction using L-BFGS approximation
1522
- *
1523
- * @param grad - Current gradient
1524
- * @param sList - List of position differences (s_k)
1525
- * @param yList - List of gradient differences (y_k)
1526
- * @param m - Memory size
1527
- * @returns Direction vector
1528
- */
1529
- function _lbfgsDirection(grad, sList, yList, m) {
1530
- if (sList.length === 0) {
1531
- // First iteration - use negative gradient
1532
- return grad.map(g => -g);
1533
- }
1534
- const q = grad.slice();
1535
- const alpha = Array(sList.length).fill(0);
1536
- const rho = [];
1537
- // Compute rho values
1538
- for (let i = 0; i < sList.length; i++) {
1539
- const s = sList[i];
1540
- const y = yList[i];
1541
- rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
1542
- }
1543
- // Forward pass
1544
- for (let i = sList.length - 1; i >= 0; i--) {
1545
- const s = sList[i];
1546
- alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0);
1547
- for (let j = 0; j < q.length; j++) {
1548
- q[j] -= alpha[i] * yList[i][j];
1549
- }
1550
- }
1551
- // Scale initial Hessian approximation
1552
- let gamma = 1;
1553
- if (sList.length > 0 && yList.length > 0) {
1554
- const y = yList[yList.length - 1];
1555
- const s = sList[sList.length - 1];
1556
- gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
1557
- y.reduce((sum, val) => sum + val * val, 0);
1558
- }
1559
- // Initialize direction with scaled negative gradient
1560
- const direction = q.map(val => -gamma * val);
1561
- // Backward pass
1562
- for (let i = 0; i < sList.length; i++) {
1563
- const s = sList[i];
1564
- const y = yList[i];
1565
- const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0);
1566
- for (let j = 0; j < direction.length; j++) {
1567
- direction[j] += s[j] * (alpha[i] - beta);
1568
- }
1569
- }
1570
- return direction;
1571
- }
1572
- /**
1573
- * Backtracking line search to find step size
1574
- *
1575
- * @param x - Current position
1576
- * @param direction - Search direction
1577
- * @param f - Function value at current position
1578
- * @param grad - Gradient at current position
1579
- * @param func - Function to evaluate cost
1580
- * @param alpha0 - Initial step size
1581
- * @returns Optimal step size
1582
- */
1583
- function _backtrackingLineSearch(x, direction, f, grad, func, alpha0) {
1584
- const c1 = 1e-4;
1585
- const c2 = 0.9;
1586
- const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
1587
- if (initialSlope >= 0) {
1588
- return 1e-8; // Direction is not a descent direction
1589
- }
1590
- let alpha = alpha0;
1591
- const maxIter = 20;
1592
- for (let i = 0; i < maxIter; i++) {
1593
- // Try step
1594
- const newX = x.map((val, i) => val + alpha * direction[i]);
1595
- const newF = func(newX);
1596
- // Check sufficient decrease condition (Armijo condition)
1597
- if (newF <= f + c1 * alpha * initialSlope) {
1598
- return alpha;
1599
- }
1600
- // Reduce step size
1601
- alpha *= c2;
1602
- }
1603
- return alpha; // Return last alpha even if not optimal
1604
- }
1605
- /**
1606
- * Position nodes using the ForceAtlas2 force-directed algorithm.
1607
- *
1608
- * @param G - Graph
1609
- * @param pos - Initial positions for nodes
1610
- * @param maxIter - Maximum number of iterations
1611
- * @param jitterTolerance - Controls tolerance for node speed adjustments
1612
- * @param scalingRatio - Scaling of attraction and repulsion forces
1613
- * @param gravity - Attraction to center to prevent disconnected components from drifting
1614
- * @param distributedAction - Distributes attraction force among nodes
1615
- * @param strongGravity - Uses a stronger gravity model
1616
- * @param nodeMass - Dictionary mapping nodes to their masses
1617
- * @param nodeSize - Dictionary mapping nodes to their sizes
1618
- * @param weight - Edge attribute for weight
1619
- * @param dissuadeHubs - Whether to prevent hub nodes from clustering
1620
- * @param linlog - Whether to use logarithmic attraction
1621
- * @param seed - Random seed for initial positions
1622
- * @param dim - Dimension of layout
1623
- * @returns Positions dictionary keyed by node
1624
- */
1625
- function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1.0, scalingRatio = 2.0, gravity = 1.0, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) {
1626
- const processed = _processParams(G, null, dim);
1627
- const graph = processed.G;
1628
- const nodes = getNodesFromGraph(graph);
1629
- if (nodes.length === 0) {
1630
- return {};
1631
- }
1632
- // Initialize random number generator
1633
- const rng = new RandomNumberGenerator(seed ?? undefined);
1634
- // Initialize positions if not provided
1635
- let posArray;
1636
- if (pos === null) {
1637
- pos = {};
1638
- posArray = new Array(nodes.length);
1639
- for (let i = 0; i < nodes.length; i++) {
1640
- posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
1641
- pos[nodes[i]] = posArray[i];
1642
- }
1643
- }
1644
- else if (Object.keys(pos).length === nodes.length) {
1645
- // Use provided positions
1646
- posArray = new Array(nodes.length);
1647
- for (let i = 0; i < nodes.length; i++) {
1648
- posArray[i] = [...pos[nodes[i]]];
1649
- }
1650
- }
1651
- else {
1652
- // Some nodes don't have positions, initialize within the range of existing positions
1653
- let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
1654
- let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
1655
- // Find min and max of existing positions
1656
- for (const node in pos) {
1657
- for (let d = 0; d < dim; d++) {
1658
- minPos[d] = Math.min(minPos[d], pos[node][d]);
1659
- maxPos[d] = Math.max(maxPos[d], pos[node][d]);
1660
- }
1661
- }
1662
- posArray = new Array(nodes.length);
1663
- for (let i = 0; i < nodes.length; i++) {
1664
- const node = nodes[i];
1665
- if (pos[node]) {
1666
- posArray[i] = [...pos[node]];
1667
- }
1668
- else {
1669
- posArray[i] = Array(dim).fill(0).map((_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d]));
1670
- pos[node] = posArray[i];
1671
- }
1672
- }
1673
- }
1674
- // Initialize mass and size arrays
1675
- const mass = new Array(nodes.length).fill(0);
1676
- const size = new Array(nodes.length).fill(0);
1677
- // Flag to track whether to adjust for node sizes
1678
- const adjustSizes = nodeSize !== null;
1679
- // Set node masses and sizes
1680
- for (let i = 0; i < nodes.length; i++) {
1681
- const node = nodes[i];
1682
- mass[i] = nodeMass && nodeMass[node] ?
1683
- nodeMass[node] :
1684
- (graph.edges ? getNodeDegree(graph, node) + 1 : 1);
1685
- size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
1686
- }
1687
- // Create adjacency matrix
1688
- const n = nodes.length;
1689
- const A = Array(n).fill(0).map(() => Array(n).fill(0));
1690
- // Populate adjacency matrix with edge weights
1691
- const edges = graph.edges ? graph.edges() : [];
1692
- const nodeIndices = {};
1693
- nodes.forEach((node, i) => { nodeIndices[node] = i; });
1694
- for (const [source, target] of edges) {
1695
- const i = nodeIndices[source];
1696
- const j = nodeIndices[target];
1697
- // Use edge weight if provided, otherwise default to 1
1698
- let edgeWeight = 1;
1699
- if (weight && graph.getEdgeData) {
1700
- edgeWeight = graph.getEdgeData(source, target, weight) || 1;
1701
- }
1702
- A[i][j] = edgeWeight;
1703
- A[j][i] = edgeWeight; // For undirected graphs
1704
- }
1705
- // Initialize force arrays
1706
- const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
1707
- const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
1708
- const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
1709
- // Simulation parameters
1710
- let speed = 1;
1711
- let speedEfficiency = 1;
1712
- let swing = 1;
1713
- let traction = 1;
1714
- // Helper function to estimate factor for force scaling
1715
- function estimateFactor(n, swing, traction, speed, speedEfficiency, jitterTolerance) {
1716
- // Optimal jitter parameters
1717
- const optJitter = 0.05 * Math.sqrt(n);
1718
- const minJitter = Math.sqrt(optJitter);
1719
- const maxJitter = 10;
1720
- const minSpeedEfficiency = 0.05;
1721
- // Estimate jitter based on current state
1722
- const other = Math.min(maxJitter, optJitter * traction / (n * n));
1723
- let jitter = jitterTolerance * Math.max(minJitter, other);
1724
- // Adjust speed efficiency based on swing/traction ratio
1725
- if (swing / traction > 2.0) {
1726
- if (speedEfficiency > minSpeedEfficiency) {
1727
- speedEfficiency *= 0.5;
1728
- }
1729
- jitter = Math.max(jitter, jitterTolerance);
1730
- }
1731
- // Calculate target speed
1732
- let targetSpeed = swing === 0 ?
1733
- Number.POSITIVE_INFINITY :
1734
- jitter * speedEfficiency * traction / swing;
1735
- // Further adjust speed efficiency
1736
- if (swing > jitter * traction) {
1737
- if (speedEfficiency > minSpeedEfficiency) {
1738
- speedEfficiency *= 0.7;
1739
- }
1740
- }
1741
- else if (speed < 1000) {
1742
- speedEfficiency *= 1.3;
1743
- }
1744
- // Limit the speed increase
1745
- const maxRise = 0.5;
1746
- speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
1747
- return [speed, speedEfficiency];
1748
- }
1749
- // Main simulation loop
1750
- for (let iter = 0; iter < maxIter; iter++) {
1751
- // Reset forces for this iteration
1752
- for (let i = 0; i < n; i++) {
1753
- for (let d = 0; d < dim; d++) {
1754
- attraction[i][d] = 0;
1755
- repulsion[i][d] = 0;
1756
- gravities[i][d] = 0;
1757
- }
1758
- }
1759
- // Compute pairwise differences and distances
1760
- const diff = Array(n).fill(0).map(() => Array(n).fill(0).map(() => Array(dim).fill(0)));
1761
- const distance = Array(n).fill(0).map(() => Array(n).fill(0));
1762
- for (let i = 0; i < n; i++) {
1763
- for (let j = 0; j < n; j++) {
1764
- if (i === j)
1765
- continue;
1766
- for (let d = 0; d < dim; d++) {
1767
- diff[i][j][d] = posArray[i][d] - posArray[j][d];
1768
- }
1769
- distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
1770
- // Prevent division by zero
1771
- if (distance[i][j] < 0.01)
1772
- distance[i][j] = 0.01;
1773
- }
1774
- }
1775
- // Calculate attraction forces
1776
- if (linlog) {
1777
- // Logarithmic attraction model
1778
- for (let i = 0; i < n; i++) {
1779
- for (let j = 0; j < n; j++) {
1780
- if (i === j || A[i][j] === 0)
1781
- continue;
1782
- const dist = distance[i][j];
1783
- const factor = -Math.log(1 + dist) / dist * A[i][j];
1784
- for (let d = 0; d < dim; d++) {
1785
- const force = factor * diff[i][j][d];
1786
- attraction[i][d] += force;
1787
- }
1788
- }
1789
- }
1790
- }
1791
- else {
1792
- // Linear attraction model
1793
- for (let i = 0; i < n; i++) {
1794
- for (let j = 0; j < n; j++) {
1795
- if (i === j || A[i][j] === 0)
1796
- continue;
1797
- for (let d = 0; d < dim; d++) {
1798
- const force = -diff[i][j][d] * A[i][j];
1799
- attraction[i][d] += force;
1800
- }
1801
- }
1802
- }
1803
- }
1804
- // Apply distributed attraction if enabled
1805
- if (distributedAction) {
1806
- for (let i = 0; i < n; i++) {
1807
- for (let d = 0; d < dim; d++) {
1808
- attraction[i][d] /= mass[i];
1809
- }
1810
- }
1811
- }
1812
- // Calculate repulsion forces
1813
- for (let i = 0; i < n; i++) {
1814
- for (let j = 0; j < n; j++) {
1815
- if (i === j)
1816
- continue;
1817
- let dist = distance[i][j];
1818
- // Adjust distance for node sizes if needed
1819
- if (adjustSizes) {
1820
- dist -= size[i] - size[j];
1821
- dist = Math.max(dist, 0.01); // Prevent negative or zero distances
1822
- }
1823
- const distSquared = dist * dist;
1824
- const massProduct = mass[i] * mass[j];
1825
- const factor = (massProduct / distSquared) * scalingRatio;
1826
- for (let d = 0; d < dim; d++) {
1827
- const direction = diff[i][j][d] / dist;
1828
- repulsion[i][d] += direction * factor;
1829
- }
1830
- }
1831
- }
1832
- // Calculate gravity forces
1833
- // First find the center of mass
1834
- const centerOfMass = Array(dim).fill(0);
1835
- for (let i = 0; i < n; i++) {
1836
- for (let d = 0; d < dim; d++) {
1837
- centerOfMass[d] += posArray[i][d] / n;
1838
- }
1839
- }
1840
- for (let i = 0; i < n; i++) {
1841
- const posCentered = Array(dim);
1842
- for (let d = 0; d < dim; d++) {
1843
- posCentered[d] = posArray[i][d] - centerOfMass[d];
1844
- }
1845
- if (strongGravity) {
1846
- // Strong gravity model
1847
- for (let d = 0; d < dim; d++) {
1848
- gravities[i][d] = -gravity * mass[i] * posCentered[d];
1849
- }
1850
- }
1851
- else {
1852
- // Regular gravity model
1853
- const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
1854
- if (dist > 0.01) {
1855
- for (let d = 0; d < dim; d++) {
1856
- const direction = posCentered[d] / dist;
1857
- gravities[i][d] = -gravity * mass[i] * direction;
1858
- }
1859
- }
1860
- }
1861
- }
1862
- // Calculate total forces and update positions
1863
- const update = Array(n).fill(0).map(() => Array(dim).fill(0));
1864
- let totalSwing = 0;
1865
- let totalTraction = 0;
1866
- for (let i = 0; i < n; i++) {
1867
- for (let d = 0; d < dim; d++) {
1868
- update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
1869
- }
1870
- // Calculate swing and traction for this node
1871
- const oldPos = [...posArray[i]];
1872
- const newPos = oldPos.map((p, d) => p + update[i][d]);
1873
- const swingVector = oldPos.map((p, d) => p - newPos[d]);
1874
- const tractionVector = oldPos.map((p, d) => p + newPos[d]);
1875
- const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
1876
- const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
1877
- totalSwing += mass[i] * swingMagnitude;
1878
- totalTraction += 0.5 * mass[i] * tractionMagnitude;
1879
- }
1880
- // Update speed and efficiency
1881
- [speed, speedEfficiency] = estimateFactor(n, totalSwing, totalTraction, speed, speedEfficiency, jitterTolerance);
1882
- // Apply forces to update positions
1883
- let totalMovement = 0;
1884
- for (let i = 0; i < n; i++) {
1885
- let factor;
1886
- if (adjustSizes) {
1887
- // Calculate displacement magnitude
1888
- const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
1889
- const swinging = mass[i] * df;
1890
- // Determine scaling factor with size adjustments
1891
- factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
1892
- factor = Math.min(factor * df, 10) / df;
1893
- }
1894
- else {
1895
- // Standard scaling factor
1896
- const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
1897
- factor = speed / (1 + Math.sqrt(speed * swinging));
1898
- }
1899
- // Apply factor to update position
1900
- for (let d = 0; d < dim; d++) {
1901
- const movement = update[i][d] * factor;
1902
- posArray[i][d] += movement;
1903
- totalMovement += Math.abs(movement);
1904
- }
1905
- }
1906
- // Check for convergence
1907
- if (totalMovement < 1e-10) {
1908
- break;
1909
- }
1910
- }
1911
- // Create position dictionary
1912
- const positions = {};
1913
- for (let i = 0; i < n; i++) {
1914
- positions[nodes[i]] = posArray[i];
1915
- }
1916
- return rescaleLayout(positions);
1917
- // Helper function to get node degree
1918
- function getNodeDegree(graph, node) {
1919
- if (!graph.edges)
1920
- return 0;
1921
- return graph.edges().filter((edge) => edge[0] === node || edge[1] === node).length;
1922
- }
1923
- }
1924
- /**
1925
- * Layout algorithm with attractive and repulsive forces (ARF).
1926
- *
1927
- * @param G - Graph
1928
- * @param pos - Initial positions for nodes
1929
- * @param scaling - Scale factor for positions
1930
- * @param a - Strength of springs between connected nodes (should be > 1)
1931
- * @param maxIter - Maximum number of iterations
1932
- * @param seed - Random seed for initial positions
1933
- * @returns Positions dictionary keyed by node
1934
- */
1935
- function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1000, seed = null) {
1936
- if (a <= 1) {
1937
- throw new Error("The parameter a should be larger than 1");
1938
- }
1939
- const nodes = getNodesFromGraph(G);
1940
- const edges = getEdgesFromGraph(G);
1941
- if (nodes.length === 0) {
1942
- return {};
1943
- }
1944
- // Initialize positions if not provided
1945
- if (!pos) {
1946
- pos = randomLayout(G, null, 2, seed);
1947
- }
1948
- else {
1949
- // Make sure all nodes have positions
1950
- const rng = new RandomNumberGenerator(seed ?? undefined);
1951
- const defaultPos = {};
1952
- nodes.forEach((node) => {
1953
- if (!pos[node]) {
1954
- defaultPos[node] = [rng.rand(), rng.rand()];
1955
- }
1956
- });
1957
- pos = { ...pos, ...defaultPos };
1958
- }
1959
- // Create node index mapping
1960
- const nodeIndex = {};
1961
- nodes.forEach((node, i) => {
1962
- nodeIndex[node] = i;
1963
- });
1964
- // Create positions array
1965
- const positions = nodes.map((node) => [...pos[node]]);
1966
- // Initialize spring constant matrix
1967
- const N = nodes.length;
1968
- const K = Array(N).fill(0).map(() => Array(N).fill(1));
1969
- // Set diagonal to zero (no self-attraction)
1970
- for (let i = 0; i < N; i++) {
1971
- K[i][i] = 0;
1972
- }
1973
- // Set stronger attraction between connected nodes
1974
- for (const [source, target] of edges) {
1975
- if (source === target)
1976
- continue;
1977
- const i = nodeIndex[source];
1978
- const j = nodeIndex[target];
1979
- K[i][j] = a;
1980
- K[j][i] = a;
1981
- }
1982
- // Calculate rho (scale factor)
1983
- const rho = scaling * Math.sqrt(N);
1984
- // Optimization loop
1985
- const dt = 1e-3; // Time step
1986
- const etol = 1e-6; // Error tolerance
1987
- let error = etol + 1;
1988
- let nIter = 0;
1989
- while (error > etol && nIter < maxIter) {
1990
- // Calculate changes for each node
1991
- const change = Array(N).fill(0).map(() => [0, 0]);
1992
- for (let i = 0; i < N; i++) {
1993
- for (let j = 0; j < N; j++) {
1994
- if (i === j)
1995
- continue;
1996
- // Calculate difference vector
1997
- const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
1998
- // Calculate distance (with minimum to avoid division by zero)
1999
- const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
2000
- // Calculate attractive and repulsive forces
2001
- for (let d = 0; d < diff.length; d++) {
2002
- change[i][d] += K[i][j] * diff[d] - (rho / dist) * diff[d];
2003
- }
2004
- }
2005
- }
2006
- // Update positions
2007
- for (let i = 0; i < N; i++) {
2008
- for (let d = 0; d < positions[i].length; d++) {
2009
- positions[i][d] += change[i][d] * dt;
2010
- }
2011
- }
2012
- // Calculate error (sum of force magnitudes)
2013
- error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
2014
- nIter++;
2015
- }
2016
- // Convert positions array back to object
2017
- const finalPos = {};
2018
- nodes.forEach((node, i) => {
2019
- finalPos[node] = positions[i];
2020
- });
2021
- return finalPos;
2022
- }
2023
- /**
2024
- * Return a dictionary of scaled positions keyed by node.
2025
- *
2026
- * @param pos - Dictionary of positions keyed by node
2027
- * @param scale - Scale factor for positions
2028
- * @returns Dictionary of scaled positions
2029
- */
2030
- function rescaleLayoutDict(pos, scale = 1) {
2031
- if (Object.keys(pos).length === 0) {
2032
- return {};
2033
- }
2034
- // Extract positions as array
2035
- const posArray = Object.values(pos);
2036
- // Find center of positions
2037
- const center = [];
2038
- for (let d = 0; d < posArray[0].length; d++) {
2039
- center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
2040
- }
2041
- // Center positions
2042
- const centeredPos = {};
2043
- for (const [node, p] of Object.entries(pos)) {
2044
- centeredPos[node] = p.map((val, d) => val - center[d]);
2045
- }
2046
- // Find maximum distance from center
2047
- let maxDist = 0;
2048
- for (const p of Object.values(centeredPos)) {
2049
- const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
2050
- maxDist = Math.max(maxDist, dist);
2051
- }
2052
- // Scale positions
2053
- const scaledPos = {};
2054
- if (maxDist > 0) {
2055
- for (const [node, p] of Object.entries(centeredPos)) {
2056
- scaledPos[node] = p.map(val => val * scale / maxDist);
2057
- }
2058
- }
2059
- else {
2060
- // All points at the center
2061
- for (const node of Object.keys(centeredPos)) {
2062
- scaledPos[node] = Array(centeredPos[node].length).fill(0);
2063
- }
2064
- }
2065
- return scaledPos;
2066
- }
2067
- // Graph generation utilities
2068
- /**
2069
- * Create a complete graph with n nodes
2070
- * @param n - Number of nodes
2071
- * @returns Graph object with all nodes connected to all other nodes
2072
- */
2073
- function completeGraph(n) {
2074
- const nodes = Array.from({ length: n }, (_, i) => i);
2075
- const edges = [];
2076
- for (let i = 0; i < n; i++) {
2077
- for (let j = i + 1; j < n; j++) {
2078
- edges.push([i, j]);
2079
- }
2080
- }
2081
- return {
2082
- nodes: () => nodes,
2083
- edges: () => edges
2084
- };
2085
- }
2086
- /**
2087
- * Create a cycle graph with n nodes
2088
- * @param n - Number of nodes
2089
- * @returns Graph object with nodes connected in a cycle
2090
- */
2091
- function cycleGraph(n) {
2092
- const nodes = Array.from({ length: n }, (_, i) => i);
2093
- const edges = [];
2094
- for (let i = 0; i < n; i++) {
2095
- edges.push([i, (i + 1) % n]);
2096
- }
2097
- return {
2098
- nodes: () => nodes,
2099
- edges: () => edges
2100
- };
2101
- }
2102
- /**
2103
- * Create a star graph with n nodes (1 center + n-1 leaves)
2104
- * @param n - Total number of nodes
2105
- * @returns Graph object with star topology
2106
- */
2107
- function starGraph(n) {
2108
- const nodes = Array.from({ length: n }, (_, i) => i);
2109
- const edges = [];
2110
- // Connect all nodes to node 0 (center)
2111
- for (let i = 1; i < n; i++) {
2112
- edges.push([0, i]);
2113
- }
2114
- return {
2115
- nodes: () => nodes,
2116
- edges: () => edges
2117
- };
2118
- }
2119
- /**
2120
- * Create a wheel graph with n nodes (1 center + n-1 rim nodes)
2121
- * @param n - Total number of nodes
2122
- * @returns Graph object with wheel topology
2123
- */
2124
- function wheelGraph(n) {
2125
- const nodes = Array.from({ length: n }, (_, i) => i);
2126
- const edges = [];
2127
- // Connect all rim nodes to center (node 0)
2128
- for (let i = 1; i < n; i++) {
2129
- edges.push([0, i]);
2130
- }
2131
- // Connect rim nodes in a cycle
2132
- for (let i = 1; i < n - 1; i++) {
2133
- edges.push([i, i + 1]);
2134
- }
2135
- if (n > 2) {
2136
- edges.push([n - 1, 1]);
2137
- }
2138
- return {
2139
- nodes: () => nodes,
2140
- edges: () => edges
2141
- };
2142
- }
2143
- /**
2144
- * Create a grid graph with rows x cols nodes
2145
- * @param rows - Number of rows
2146
- * @param cols - Number of columns
2147
- * @returns Graph object with grid topology
2148
- */
2149
- function gridGraph(rows, cols) {
2150
- const nodes = [];
2151
- const edges = [];
2152
- // Create nodes
2153
- for (let i = 0; i < rows; i++) {
2154
- for (let j = 0; j < cols; j++) {
2155
- nodes.push(`${i},${j}`);
2156
- }
2157
- }
2158
- // Create edges
2159
- for (let i = 0; i < rows; i++) {
2160
- for (let j = 0; j < cols; j++) {
2161
- // Connect to right neighbor
2162
- if (j < cols - 1) {
2163
- edges.push([`${i},${j}`, `${i},${j + 1}`]);
2164
- }
2165
- // Connect to bottom neighbor
2166
- if (i < rows - 1) {
2167
- edges.push([`${i},${j}`, `${i + 1},${j}`]);
2168
- }
2169
- }
2170
- }
2171
- return {
2172
- nodes: () => nodes,
2173
- edges: () => edges
2174
- };
2175
- }
2176
- /**
2177
- * Create a random graph with n nodes and given edge probability
2178
- * @param n - Number of nodes
2179
- * @param p - Probability of edge between any two nodes (0-1)
2180
- * @param seed - Random seed for reproducibility
2181
- * @returns Graph object with random edges
2182
- */
2183
- function randomGraph(n, p, seed) {
2184
- const nodes = Array.from({ length: n }, (_, i) => i);
2185
- const edges = [];
2186
- // Simple deterministic pseudo-random if seed provided
2187
- let currentSeed = seed;
2188
- let random = seed !== undefined
2189
- ? () => {
2190
- currentSeed = (currentSeed * 9301 + 49297) % 233280;
2191
- return currentSeed / 233280;
2192
- }
2193
- : Math.random;
2194
- for (let i = 0; i < n; i++) {
2195
- for (let j = i + 1; j < n; j++) {
2196
- if (random() < p) {
2197
- edges.push([i, j]);
2198
- }
2199
- }
2200
- }
2201
- return {
2202
- nodes: () => nodes,
2203
- edges: () => edges
2204
- };
2205
- }
2206
- /**
2207
- * Create a bipartite graph with two sets of nodes
2208
- * @param n1 - Number of nodes in first set
2209
- * @param n2 - Number of nodes in second set
2210
- * @param p - Probability of edge between nodes in different sets
2211
- * @param seed - Random seed for reproducibility
2212
- * @returns Graph object with bipartite structure and setA/setB properties
2213
- */
2214
- function bipartiteGraph(n1, n2, p, seed) {
2215
- const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
2216
- const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
2217
- const nodes = [...setA, ...setB];
2218
- const edges = [];
2219
- // Simple deterministic pseudo-random if seed provided
2220
- let currentSeed = seed;
2221
- let random = seed !== undefined
2222
- ? () => {
2223
- currentSeed = (currentSeed * 9301 + 49297) % 233280;
2224
- return currentSeed / 233280;
2225
- }
2226
- : Math.random;
2227
- // Only connect nodes between sets
2228
- for (const a of setA) {
2229
- for (const b of setB) {
2230
- if (random() < p) {
2231
- edges.push([a, b]);
2232
- }
2233
- }
2234
- }
2235
- return {
2236
- nodes: () => nodes,
2237
- edges: () => edges,
2238
- setA,
2239
- setB
2240
- };
2241
- }
2242
- /**
2243
- * Create a scale-free graph using Barabási-Albert model
2244
- * @param n - Total number of nodes
2245
- * @param m - Number of edges to attach from new node
2246
- * @param seed - Random seed for reproducibility
2247
- * @returns Graph object with scale-free properties
2248
- */
2249
- function scaleFreeGraph(n, m, seed) {
2250
- if (m >= n) {
2251
- throw new Error('m must be less than n');
2252
- }
2253
- const nodes = Array.from({ length: n }, (_, i) => i);
2254
- const edges = [];
2255
- const degrees = new Array(n).fill(0);
2256
- // Simple deterministic pseudo-random if seed provided
2257
- let currentSeed = seed;
2258
- let random = seed !== undefined
2259
- ? () => {
2260
- currentSeed = (currentSeed * 9301 + 49297) % 233280;
2261
- return currentSeed / 233280;
2262
- }
2263
- : Math.random;
2264
- // Start with complete graph of m+1 nodes
2265
- for (let i = 0; i <= m; i++) {
2266
- for (let j = i + 1; j <= m; j++) {
2267
- edges.push([i, j]);
2268
- degrees[i]++;
2269
- degrees[j]++;
2270
- }
2271
- }
2272
- // Add remaining nodes
2273
- for (let i = m + 1; i < n; i++) {
2274
- const targets = new Set();
2275
- const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
2276
- // Choose m targets based on preferential attachment
2277
- while (targets.size < m) {
2278
- let r = random() * totalDegree;
2279
- let cumSum = 0;
2280
- for (let j = 0; j < i; j++) {
2281
- cumSum += degrees[j];
2282
- if (r <= cumSum && !targets.has(j)) {
2283
- targets.add(j);
2284
- break;
2285
- }
2286
- }
2287
- }
2288
- // Add edges to targets
2289
- for (const target of targets) {
2290
- edges.push([i, target]);
2291
- degrees[i]++;
2292
- degrees[target]++;
2293
- }
2294
- }
2295
- return {
2296
- nodes: () => nodes,
2297
- edges: () => edges
2298
- };
2299
- }
2300
- // Export the layout functions
2301
- export { randomLayout, circularLayout, shellLayout, springLayout, fruchtermanReingoldLayout, spectralLayout, spiralLayout, bipartiteLayout, multipartiteLayout, bfsLayout, planarLayout, kamadaKawaiLayout, forceatlas2Layout, arfLayout, rescaleLayout, rescaleLayoutDict,
2302
- // Graph generation utilities
2303
- completeGraph, cycleGraph, starGraph, wheelGraph, gridGraph, randomGraph, bipartiteGraph, scaleFreeGraph };
2304
- //# sourceMappingURL=layout.js.map