@graphty/layout 1.2.2 → 1.2.4

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 (200) hide show
  1. package/README.md +317 -237
  2. package/dist/src/algorithms/index.d.ts +5 -0
  3. package/dist/src/algorithms/index.js +6 -0
  4. package/dist/src/algorithms/index.js.map +1 -0
  5. package/dist/src/algorithms/optimization/index.d.ts +7 -0
  6. package/dist/src/algorithms/optimization/index.js +7 -0
  7. package/dist/src/algorithms/optimization/index.js.map +1 -0
  8. package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +32 -0
  9. package/dist/src/algorithms/optimization/kamada-kawai-solver.js +176 -0
  10. package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -0
  11. package/dist/src/algorithms/optimization/lbfgs.d.ts +13 -0
  12. package/dist/src/algorithms/optimization/lbfgs.js +56 -0
  13. package/dist/src/algorithms/optimization/lbfgs.js.map +1 -0
  14. package/dist/src/algorithms/optimization/line-search.d.ts +15 -0
  15. package/dist/src/algorithms/optimization/line-search.js +37 -0
  16. package/dist/src/algorithms/optimization/line-search.js.map +1 -0
  17. package/dist/src/algorithms/optimization/types.d.ts +5 -0
  18. package/dist/src/algorithms/optimization/types.js +5 -0
  19. package/dist/src/algorithms/optimization/types.js.map +1 -0
  20. package/dist/src/algorithms/planarity/check.d.ts +17 -0
  21. package/dist/src/algorithms/planarity/check.js +30 -0
  22. package/dist/src/algorithms/planarity/check.js.map +1 -0
  23. package/dist/src/algorithms/planarity/embedding.d.ts +29 -0
  24. package/dist/src/algorithms/planarity/embedding.js +179 -0
  25. package/dist/src/algorithms/planarity/embedding.js.map +1 -0
  26. package/dist/src/algorithms/planarity/index.d.ts +7 -0
  27. package/dist/src/algorithms/planarity/index.js +8 -0
  28. package/dist/src/algorithms/planarity/index.js.map +1 -0
  29. package/dist/src/algorithms/planarity/lr-test.d.ts +15 -0
  30. package/dist/src/algorithms/planarity/lr-test.js +54 -0
  31. package/dist/src/algorithms/planarity/lr-test.js.map +1 -0
  32. package/dist/src/algorithms/planarity/special-graphs.d.ts +28 -0
  33. package/dist/src/algorithms/planarity/special-graphs.js +108 -0
  34. package/dist/src/algorithms/planarity/special-graphs.js.map +1 -0
  35. package/dist/src/generators/basic.d.ts +28 -0
  36. package/dist/src/generators/basic.js +79 -0
  37. package/dist/src/generators/basic.js.map +1 -0
  38. package/dist/src/generators/bipartite.d.ts +16 -0
  39. package/dist/src/generators/bipartite.js +40 -0
  40. package/dist/src/generators/bipartite.js.map +1 -0
  41. package/dist/src/generators/grid.d.ts +11 -0
  42. package/dist/src/generators/grid.js +37 -0
  43. package/dist/src/generators/grid.js.map +1 -0
  44. package/dist/src/generators/index.d.ts +8 -0
  45. package/dist/src/generators/index.js +9 -0
  46. package/dist/src/generators/index.js.map +1 -0
  47. package/dist/src/generators/random.d.ts +12 -0
  48. package/dist/src/generators/random.js +34 -0
  49. package/dist/src/generators/random.js.map +1 -0
  50. package/dist/src/generators/scale-free.d.ts +12 -0
  51. package/dist/src/generators/scale-free.js +62 -0
  52. package/dist/src/generators/scale-free.js.map +1 -0
  53. package/dist/src/index.d.ts +10 -0
  54. package/dist/src/index.js +15 -0
  55. package/dist/src/index.js.map +1 -0
  56. package/dist/src/layouts/basic/index.d.ts +4 -0
  57. package/dist/src/layouts/basic/index.js +5 -0
  58. package/dist/src/layouts/basic/index.js.map +1 -0
  59. package/dist/src/layouts/basic/random.d.ts +14 -0
  60. package/dist/src/layouts/basic/random.js +27 -0
  61. package/dist/src/layouts/basic/random.js.map +1 -0
  62. package/dist/src/layouts/force-directed/arf.d.ts +13 -0
  63. package/dist/src/layouts/force-directed/arf.js +103 -0
  64. package/dist/src/layouts/force-directed/arf.js.map +1 -0
  65. package/dist/src/layouts/force-directed/forceatlas2.d.ts +22 -0
  66. package/dist/src/layouts/force-directed/forceatlas2.js +322 -0
  67. package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -0
  68. package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +19 -0
  69. package/dist/src/layouts/force-directed/fruchterman-reingold.js +131 -0
  70. package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -0
  71. package/dist/src/layouts/force-directed/index.d.ts +8 -0
  72. package/dist/src/layouts/force-directed/index.js +9 -0
  73. package/dist/src/layouts/force-directed/index.js.map +1 -0
  74. package/dist/src/layouts/force-directed/kamada-kawai.d.ts +15 -0
  75. package/dist/src/layouts/force-directed/kamada-kawai.js +87 -0
  76. package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -0
  77. package/dist/src/layouts/force-directed/spring.d.ts +19 -0
  78. package/dist/src/layouts/force-directed/spring.js +23 -0
  79. package/dist/src/layouts/force-directed/spring.js.map +1 -0
  80. package/dist/src/layouts/geometric/circular.d.ts +14 -0
  81. package/dist/src/layouts/geometric/circular.js +67 -0
  82. package/dist/src/layouts/geometric/circular.js.map +1 -0
  83. package/dist/src/layouts/geometric/index.d.ts +6 -0
  84. package/dist/src/layouts/geometric/index.js +7 -0
  85. package/dist/src/layouts/geometric/index.js.map +1 -0
  86. package/dist/src/layouts/geometric/shell.d.ts +15 -0
  87. package/dist/src/layouts/geometric/shell.js +67 -0
  88. package/dist/src/layouts/geometric/shell.js.map +1 -0
  89. package/dist/src/layouts/geometric/spiral.d.ts +16 -0
  90. package/dist/src/layouts/geometric/spiral.js +73 -0
  91. package/dist/src/layouts/geometric/spiral.js.map +1 -0
  92. package/dist/src/layouts/hierarchical/bfs.d.ts +12 -0
  93. package/dist/src/layouts/hierarchical/bfs.js +62 -0
  94. package/dist/src/layouts/hierarchical/bfs.js.map +1 -0
  95. package/dist/src/layouts/hierarchical/bipartite.d.ts +16 -0
  96. package/dist/src/layouts/hierarchical/bipartite.js +72 -0
  97. package/dist/src/layouts/hierarchical/bipartite.js.map +1 -0
  98. package/dist/src/layouts/hierarchical/index.d.ts +6 -0
  99. package/dist/src/layouts/hierarchical/index.js +7 -0
  100. package/dist/src/layouts/hierarchical/index.js.map +1 -0
  101. package/dist/src/layouts/hierarchical/multipartite.d.ts +12 -0
  102. package/dist/src/layouts/hierarchical/multipartite.js +73 -0
  103. package/dist/src/layouts/hierarchical/multipartite.js.map +1 -0
  104. package/dist/src/layouts/index.d.ts +8 -0
  105. package/dist/src/layouts/index.js +9 -0
  106. package/dist/src/layouts/index.js.map +1 -0
  107. package/dist/src/layouts/specialized/index.d.ts +5 -0
  108. package/dist/src/layouts/specialized/index.js +6 -0
  109. package/dist/src/layouts/specialized/index.js.map +1 -0
  110. package/dist/src/layouts/specialized/planar.d.ts +14 -0
  111. package/dist/src/layouts/specialized/planar.js +47 -0
  112. package/dist/src/layouts/specialized/planar.js.map +1 -0
  113. package/dist/src/layouts/specialized/spectral.d.ts +14 -0
  114. package/dist/src/layouts/specialized/spectral.js +106 -0
  115. package/dist/src/layouts/specialized/spectral.js.map +1 -0
  116. package/dist/src/types/embedding.d.ts +9 -0
  117. package/dist/src/types/embedding.js +5 -0
  118. package/dist/src/types/embedding.js.map +1 -0
  119. package/dist/src/types/graph.d.ts +10 -0
  120. package/dist/src/types/graph.js +5 -0
  121. package/dist/src/types/graph.js.map +1 -0
  122. package/dist/src/types/index.d.ts +6 -0
  123. package/dist/src/types/index.js +7 -0
  124. package/dist/src/types/index.js.map +1 -0
  125. package/dist/src/types/layout.d.ts +6 -0
  126. package/dist/src/types/layout.js +5 -0
  127. package/dist/src/types/layout.js.map +1 -0
  128. package/dist/src/utils/graph.d.ts +34 -0
  129. package/dist/src/utils/graph.js +69 -0
  130. package/dist/src/utils/graph.js.map +1 -0
  131. package/dist/src/utils/index.d.ts +8 -0
  132. package/dist/src/utils/index.js +9 -0
  133. package/dist/src/utils/index.js.map +1 -0
  134. package/dist/src/utils/numpy.d.ts +17 -0
  135. package/dist/src/utils/numpy.js +101 -0
  136. package/dist/src/utils/numpy.js.map +1 -0
  137. package/dist/src/utils/params.d.ts +17 -0
  138. package/dist/src/utils/params.js +22 -0
  139. package/dist/src/utils/params.js.map +1 -0
  140. package/dist/src/utils/random.d.ts +14 -0
  141. package/dist/src/utils/random.js +42 -0
  142. package/dist/src/utils/random.js.map +1 -0
  143. package/dist/src/utils/rescale.d.ts +21 -0
  144. package/dist/src/utils/rescale.js +119 -0
  145. package/dist/src/utils/rescale.js.map +1 -0
  146. package/dist/vitest.config.js +1 -1
  147. package/dist/vitest.config.js.map +1 -1
  148. package/package.json +15 -1
  149. package/.env.example +0 -11
  150. package/.github/workflows/ci.yml +0 -101
  151. package/.releaserc.json +0 -22
  152. package/CHANGELOG.md +0 -45
  153. package/CLAUDE.md +0 -104
  154. package/CONTRIBUTING.md +0 -1
  155. package/DEPLOYMENT.md +0 -59
  156. package/commitlint.config.js +0 -37
  157. package/examples/3d-force-directed.html +0 -611
  158. package/examples/3d-kamada-kawai.html +0 -379
  159. package/examples/3d-layout-comparison.html +0 -448
  160. package/examples/3d-spherical-layout.html +0 -319
  161. package/examples/arf-layout.html +0 -524
  162. package/examples/bfs-layout.html +0 -487
  163. package/examples/bipartite-layout.html +0 -382
  164. package/examples/circular-layout.html +0 -292
  165. package/examples/forceatlas2-layout.html +0 -588
  166. package/examples/index.html +0 -241
  167. package/examples/kamada-kawai-layout.html +0 -425
  168. package/examples/layout-helpers.js +0 -486
  169. package/examples/multipartite-layout.html +0 -410
  170. package/examples/planar-layout.html +0 -388
  171. package/examples/random-layout.html +0 -328
  172. package/examples/shell-layout.html +0 -323
  173. package/examples/spectral-layout.html +0 -335
  174. package/examples/spiral-layout.html +0 -447
  175. package/examples/spring-layout.html +0 -313
  176. package/test/arf-layout.test.ts +0 -443
  177. package/test/bfs-layout.test.ts +0 -427
  178. package/test/bipartite-layout.test.ts +0 -344
  179. package/test/circular-layout.test.ts +0 -442
  180. package/test/forceatlas2-layout.test.ts +0 -405
  181. package/test/fruchterman-reingold-layout.test.ts +0 -477
  182. package/test/graph-generators.test.ts +0 -450
  183. package/test/kamada-kawai-layout.test.ts +0 -623
  184. package/test/multipartite-layout.test.ts +0 -404
  185. package/test/planar-layout.test.ts +0 -266
  186. package/test/random-layout.test.ts +0 -254
  187. package/test/rescale-layout.test.ts +0 -373
  188. package/test/shell-layout.test.ts +0 -347
  189. package/test/spectral-layout.test.ts +0 -378
  190. package/test/spiral-layout.test.ts +0 -338
  191. package/test/spring-layout.test.ts +0 -241
  192. package/test/test-utils.ts +0 -34
  193. package/test/utils-graph.test.ts +0 -155
  194. package/test/utils-index.test.ts +0 -77
  195. package/test/utils-numpy.test.ts +0 -272
  196. package/test/utils-params.test.ts +0 -99
  197. package/test/utils-random.test.ts +0 -229
  198. package/tsconfig.json +0 -16
  199. package/vite.config.js +0 -36
  200. 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
- }