@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,477 +0,0 @@
1
- import { describe, it, assert } from 'vitest';
2
- import {
3
- fruchtermanReingoldLayout,
4
- completeGraph,
5
- cycleGraph,
6
- starGraph,
7
- gridGraph,
8
- randomGraph
9
- } from '../src';
10
-
11
- describe('Fruchterman-Reingold Layout', () => {
12
- describe('Basic functionality', () => {
13
- it('should position all nodes', () => {
14
- const graph = completeGraph(6);
15
- const positions = fruchtermanReingoldLayout(graph);
16
-
17
- assert.equal(Object.keys(positions).length, 6);
18
- graph.nodes().forEach(node => {
19
- assert.isDefined(positions[node]);
20
- assert.equal(positions[node].length, 2);
21
- assert.isNumber(positions[node][0]);
22
- assert.isNumber(positions[node][1]);
23
- });
24
- });
25
-
26
- it('should handle empty graph', () => {
27
- const emptyGraph = { nodes: () => [], edges: () => [] };
28
- const positions = fruchtermanReingoldLayout(emptyGraph);
29
-
30
- assert.equal(Object.keys(positions).length, 0);
31
- });
32
-
33
- it('should handle single node', () => {
34
- const singleNode = { nodes: () => ['A'], edges: () => [] };
35
- const positions = fruchtermanReingoldLayout(singleNode);
36
-
37
- assert.equal(Object.keys(positions).length, 1);
38
- assert.isDefined(positions['A']);
39
- assert.equal(positions['A'].length, 2);
40
- });
41
-
42
- it('should handle disconnected components', () => {
43
- const graph = {
44
- nodes: () => [0, 1, 2, 3, 4, 5],
45
- edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
46
- };
47
- const positions = fruchtermanReingoldLayout(graph);
48
-
49
- assert.equal(Object.keys(positions).length, 6);
50
- graph.nodes().forEach(node => {
51
- assert.isDefined(positions[node]);
52
- });
53
- });
54
- });
55
-
56
- describe('Parameter variations', () => {
57
- it('should respect k parameter (optimal distance)', () => {
58
- const graph = cycleGraph(8);
59
-
60
- const positions1 = fruchtermanReingoldLayout(graph, 0.5);
61
- const positions2 = fruchtermanReingoldLayout(graph, 2.0);
62
-
63
- // Both should produce valid layouts
64
- assert.equal(Object.keys(positions1).length, 8);
65
- assert.equal(Object.keys(positions2).length, 8);
66
-
67
- // Larger k should generally produce more spread out layout
68
- const avgDist1 = getAverageEdgeLength(graph, positions1);
69
- const avgDist2 = getAverageEdgeLength(graph, positions2);
70
-
71
- // k affects the ideal spring length
72
- assert.isAbove(avgDist1, 0);
73
- assert.isAbove(avgDist2, 0);
74
- });
75
-
76
- it('should use provided initial positions', () => {
77
- const graph = starGraph(6);
78
- const initialPos = {};
79
- graph.nodes().forEach((node, i) => {
80
- initialPos[node] = [i * 0.2, 0]; // Line layout
81
- });
82
-
83
- const positions = fruchtermanReingoldLayout(graph, null, initialPos);
84
-
85
- assert.equal(Object.keys(positions).length, 6);
86
- // Should evolve from initial positions
87
- let different = false;
88
- graph.nodes().forEach(node => {
89
- if (positions[node][0] !== initialPos[node][0] ||
90
- positions[node][1] !== initialPos[node][1]) {
91
- different = true;
92
- }
93
- });
94
- assert.isTrue(different);
95
- });
96
-
97
- it('should respect fixed nodes', () => {
98
- const graph = cycleGraph(5);
99
- const initialPos = {};
100
- graph.nodes().forEach((node, i) => {
101
- const angle = (2 * Math.PI * i) / 5;
102
- initialPos[node] = [Math.cos(angle), Math.sin(angle)];
103
- });
104
-
105
- const fixed = [0, 2]; // Fix nodes 0 and 2
106
- const positions = fruchtermanReingoldLayout(graph, null, initialPos, fixed);
107
-
108
- // Fixed nodes should not move
109
- assert.deepEqual(positions[0], initialPos[0]);
110
- assert.deepEqual(positions[2], initialPos[2]);
111
-
112
- // Other nodes should be able to move
113
- assert.notDeepEqual(positions[1], initialPos[1]);
114
- });
115
-
116
- it('should respect iterations parameter', () => {
117
- const graph = randomGraph(15, 0.3, 42);
118
-
119
- const startTime1 = performance.now();
120
- const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 10);
121
- const time1 = performance.now() - startTime1;
122
-
123
- const startTime2 = performance.now();
124
- const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 100);
125
- const time2 = performance.now() - startTime2;
126
-
127
- // More iterations should take longer
128
- assert.isAbove(time2, time1);
129
-
130
- // Both should complete
131
- assert.equal(Object.keys(positions1).length, 15);
132
- assert.equal(Object.keys(positions2).length, 15);
133
- });
134
-
135
- it('should respect scale parameter', () => {
136
- const graph = gridGraph(4, 4);
137
-
138
- const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1);
139
- const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 2);
140
-
141
- // Calculate spans
142
- const span1 = getLayoutSpan(positions1);
143
- const span2 = getLayoutSpan(positions2);
144
-
145
- // Scale parameter affects the final layout size
146
- // but the exact ratio might vary due to force-directed nature
147
- assert.isAbove(span2.width, span1.width);
148
- assert.isAbove(span2.height, span1.height);
149
-
150
- // Both should produce reasonable layouts
151
- assert.isAbove(span1.width, 0);
152
- assert.isAbove(span1.height, 0);
153
- assert.isAbove(span2.width, 0);
154
- assert.isAbove(span2.height, 0);
155
- });
156
-
157
- it('should respect center parameter', () => {
158
- const graph = completeGraph(5);
159
- const center = [10, -5];
160
-
161
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, center);
162
-
163
- // Calculate center of mass
164
- const com = getCenterOfMass(positions);
165
-
166
- assert.approximately(com[0], center[0], 1);
167
- assert.approximately(com[1], center[1], 1);
168
- });
169
-
170
- it('should handle different dimensions', () => {
171
- const graph = cycleGraph(6);
172
-
173
- // 2D layout
174
- const positions2D = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0], 2);
175
- graph.nodes().forEach(node => {
176
- assert.equal(positions2D[node].length, 2);
177
- });
178
-
179
- // 3D layout
180
- const positions3D = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0, 0], 3);
181
- graph.nodes().forEach(node => {
182
- assert.equal(positions3D[node].length, 3);
183
- assert.isNumber(positions3D[node][2]);
184
- });
185
- });
186
- });
187
-
188
- describe('Force-directed properties', () => {
189
- it('should separate non-connected nodes', () => {
190
- const graph = {
191
- nodes: () => [0, 1, 2, 3],
192
- edges: () => [[0, 1], [2, 3]] // Two separate edges
193
- };
194
-
195
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
196
-
197
- // Connected pairs should be at reasonable distance
198
- const d01 = getDistance(positions, 0, 1);
199
- const d23 = getDistance(positions, 2, 3);
200
-
201
- // Non-connected nodes should be further apart
202
- const d02 = getDistance(positions, 0, 2);
203
- const d13 = getDistance(positions, 1, 3);
204
-
205
- assert.isAbove(d01, 0);
206
- assert.isAbove(d23, 0);
207
- assert.isAbove(d02, d01);
208
- assert.isAbove(d13, d23);
209
- });
210
-
211
- it('should create symmetric layouts for symmetric graphs', () => {
212
- const graph = completeGraph(6);
213
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
214
-
215
- // All pairwise distances should be similar in complete graph
216
- const distances = [];
217
- const nodes = graph.nodes();
218
- for (let i = 0; i < nodes.length; i++) {
219
- for (let j = i + 1; j < nodes.length; j++) {
220
- distances.push(getDistance(positions, nodes[i], nodes[j]));
221
- }
222
- }
223
-
224
- const avgDist = distances.reduce((a, b) => a + b) / distances.length;
225
- const maxDeviation = Math.max(...distances.map(d => Math.abs(d - avgDist) / avgDist));
226
-
227
- // Allow some variation but should be roughly symmetric
228
- assert.isBelow(maxDeviation, 0.5);
229
- });
230
-
231
- it('should handle star graphs well', () => {
232
- const graph = starGraph(8);
233
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
234
-
235
- // Center node should be roughly in the middle
236
- const centerNode = 0;
237
- const leafNodes = Array.from({length: 7}, (_, i) => i + 1);
238
-
239
- // Leaves should be roughly equidistant from center
240
- const leafDistances = leafNodes.map(leaf => getDistance(positions, centerNode, leaf));
241
- const avgDist = leafDistances.reduce((a, b) => a + b) / leafDistances.length;
242
-
243
- leafDistances.forEach(d => {
244
- assert.approximately(d, avgDist, avgDist * 0.3);
245
- });
246
- });
247
- });
248
-
249
- describe('Convergence and stability', () => {
250
- it('should converge to stable layout', () => {
251
- const graph = randomGraph(10, 0.3, 123);
252
-
253
- // Run with many iterations
254
- const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 200);
255
-
256
- // Run a few more iterations from the result
257
- const positions2 = fruchtermanReingoldLayout(graph, null, positions1, null, 20);
258
-
259
- // Should not change much
260
- let totalMovement = 0;
261
- graph.nodes().forEach(node => {
262
- totalMovement += getDistance(positions1, positions2, node, node);
263
- });
264
-
265
- assert.isBelow(totalMovement / graph.nodes().length, 0.1);
266
- });
267
-
268
- it('should produce reasonable layouts for grid graphs', () => {
269
- const graph = gridGraph(5, 5);
270
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
271
-
272
- // Should maintain some grid-like structure
273
- // Check that adjacent nodes in grid are relatively close
274
- for (let i = 0; i < 5; i++) {
275
- for (let j = 0; j < 4; j++) {
276
- const node1 = `${i},${j}`;
277
- const node2 = `${i},${j+1}`;
278
- const dist = getDistance(positions, node1, node2);
279
-
280
- assert.isAbove(dist, 0);
281
- assert.isBelow(dist, 2); // Should be reasonably close
282
- }
283
- }
284
- });
285
- });
286
-
287
- describe('Seed consistency', () => {
288
- it('should produce consistent results with same seed', () => {
289
- const graph = randomGraph(12, 0.3, 999);
290
-
291
- const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 42);
292
- const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 42);
293
-
294
- // Same seed should produce same initial random positions
295
- graph.nodes().forEach(node => {
296
- assert.approximately(positions1[node][0], positions2[node][0], 0.1);
297
- assert.approximately(positions1[node][1], positions2[node][1], 0.1);
298
- });
299
- });
300
-
301
- it('should produce different results with different seeds', () => {
302
- const graph = cycleGraph(8);
303
-
304
- const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 123);
305
- const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 456);
306
-
307
- // Different seeds should produce different layouts
308
- let totalDiff = 0;
309
- graph.nodes().forEach(node => {
310
- totalDiff += Math.abs(positions1[node][0] - positions2[node][0]);
311
- totalDiff += Math.abs(positions1[node][1] - positions2[node][1]);
312
- });
313
-
314
- assert.isAbove(totalDiff, 1);
315
- });
316
- });
317
-
318
- describe('Special cases and edge cases', () => {
319
- it('should handle string node IDs', () => {
320
- const graph = {
321
- nodes: () => ['alice', 'bob', 'charlie', 'david'],
322
- edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
323
- };
324
-
325
- const positions = fruchtermanReingoldLayout(graph);
326
-
327
- assert.equal(Object.keys(positions).length, 4);
328
- ['alice', 'bob', 'charlie', 'david'].forEach(node => {
329
- assert.isDefined(positions[node]);
330
- assert.equal(positions[node].length, 2);
331
- });
332
- });
333
-
334
- it('should handle large graphs reasonably', () => {
335
- const graph = gridGraph(10, 10); // 100 nodes
336
-
337
- const startTime = performance.now();
338
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0], 2);
339
- const endTime = performance.now();
340
-
341
- assert.equal(Object.keys(positions).length, 100);
342
- assert.isBelow(endTime - startTime, 2000); // Should complete quickly
343
- });
344
-
345
- it('should handle dense graphs', () => {
346
- const graph = completeGraph(15);
347
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 50);
348
-
349
- // Should spread nodes out reasonably
350
- const minDist = getMinimumDistance(positions);
351
- assert.isAbove(minDist, 0.01);
352
- });
353
-
354
- it('should handle path graphs', () => {
355
- const path = {
356
- nodes: () => [0, 1, 2, 3, 4, 5, 6, 7],
357
- edges: () => [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]
358
- };
359
-
360
- const positions = fruchtermanReingoldLayout(path, null, null, null, 100);
361
-
362
- // Path should stretch out somewhat
363
- const startEnd = getDistance(positions, 0, 7);
364
- const avgEdgeLength = getAverageEdgeLength(path, positions);
365
-
366
- // End-to-end distance should be several times the average edge length
367
- assert.isAbove(startEnd, avgEdgeLength * 3);
368
- });
369
- });
370
-
371
- describe('Layout quality', () => {
372
- it('should minimize edge crossings for planar graphs', () => {
373
- const graph = cycleGraph(10);
374
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 200);
375
-
376
- // For a cycle, adjacent nodes should be among the closest
377
- graph.nodes().forEach((node, i) => {
378
- const nextNode = (i + 1) % 10;
379
- const prevNode = (i + 9) % 10;
380
-
381
- const distNext = getDistance(positions, node, nextNode);
382
- const distPrev = getDistance(positions, node, prevNode);
383
-
384
- // Check distances to all other nodes
385
- const otherDists = [];
386
- graph.nodes().forEach((other, j) => {
387
- if (j !== i && j !== (i + 1) % 10 && j !== (i + 9) % 10) {
388
- otherDists.push(getDistance(positions, node, other));
389
- }
390
- });
391
-
392
- const minOtherDist = Math.min(...otherDists);
393
-
394
- // Adjacent nodes should generally be closer than non-adjacent
395
- assert.isBelow(Math.min(distNext, distPrev), minOtherDist * 1.6);
396
- });
397
- });
398
-
399
- it('should avoid node overlap', () => {
400
- const graph = completeGraph(10);
401
- const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
402
-
403
- // Check minimum distance between nodes
404
- const minDist = getMinimumDistance(positions);
405
-
406
- // Nodes should be separated
407
- assert.isAbove(minDist, 0.05);
408
- });
409
- });
410
- });
411
-
412
- // Helper functions
413
- function getLayoutSpan(positions) {
414
- const nodes = Object.keys(positions);
415
- let minX = Infinity, maxX = -Infinity;
416
- let minY = Infinity, maxY = -Infinity;
417
-
418
- nodes.forEach(node => {
419
- minX = Math.min(minX, positions[node][0]);
420
- maxX = Math.max(maxX, positions[node][0]);
421
- minY = Math.min(minY, positions[node][1]);
422
- maxY = Math.max(maxY, positions[node][1]);
423
- });
424
-
425
- return {
426
- width: maxX - minX,
427
- height: maxY - minY
428
- };
429
- }
430
-
431
- function getCenterOfMass(positions) {
432
- const nodes = Object.keys(positions);
433
- const com = [0, 0];
434
-
435
- nodes.forEach(node => {
436
- com[0] += positions[node][0];
437
- com[1] += positions[node][1];
438
- });
439
-
440
- com[0] /= nodes.length;
441
- com[1] /= nodes.length;
442
- return com;
443
- }
444
-
445
- function getDistance(positions, node1, node2, node1Alt?, node2Alt?) {
446
- const n1 = node1Alt !== undefined ? node1Alt : node1;
447
- const n2 = node2Alt !== undefined ? node2Alt : node2;
448
-
449
- const dx = positions[n1][0] - positions[n2][0];
450
- const dy = positions[n1][1] - positions[n2][1];
451
- return Math.sqrt(dx * dx + dy * dy);
452
- }
453
-
454
- function getMinimumDistance(positions) {
455
- const nodes = Object.keys(positions);
456
- let minDist = Infinity;
457
-
458
- for (let i = 0; i < nodes.length; i++) {
459
- for (let j = i + 1; j < nodes.length; j++) {
460
- const dist = getDistance(positions, nodes[i], nodes[j]);
461
- minDist = Math.min(minDist, dist);
462
- }
463
- }
464
-
465
- return minDist;
466
- }
467
-
468
- function getAverageEdgeLength(graph, positions) {
469
- const edges = graph.edges();
470
- let totalLength = 0;
471
-
472
- edges.forEach(edge => {
473
- totalLength += getDistance(positions, edge[0], edge[1]);
474
- });
475
-
476
- return totalLength / edges.length;
477
- }