@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,405 +0,0 @@
1
- import { describe, it, assert } from 'vitest';
2
- import {
3
- forceatlas2Layout,
4
- completeGraph,
5
- cycleGraph,
6
- starGraph,
7
- gridGraph,
8
- randomGraph,
9
- scaleFreeGraph
10
- } from '../src';
11
-
12
- describe('ForceAtlas2 Layout', () => {
13
- describe('Basic functionality', () => {
14
- it('should position all nodes', () => {
15
- const graph = completeGraph(8);
16
- const positions = forceatlas2Layout(graph);
17
-
18
- assert.equal(Object.keys(positions).length, 8);
19
- graph.nodes().forEach(node => {
20
- assert.isDefined(positions[node]);
21
- assert.equal(positions[node].length, 2);
22
- assert.isNumber(positions[node][0]);
23
- assert.isNumber(positions[node][1]);
24
- });
25
- });
26
-
27
- it('should handle empty graph', () => {
28
- const emptyGraph = { nodes: () => [], edges: () => [] };
29
- const positions = forceatlas2Layout(emptyGraph);
30
-
31
- assert.equal(Object.keys(positions).length, 0);
32
- });
33
-
34
- it('should handle single node', () => {
35
- const singleNode = { nodes: () => ['A'], edges: () => [] };
36
- const positions = forceatlas2Layout(singleNode);
37
-
38
- assert.equal(Object.keys(positions).length, 1);
39
- assert.isDefined(positions['A']);
40
- assert.equal(positions['A'].length, 2);
41
- });
42
-
43
- it('should handle disconnected components', () => {
44
- const graph = {
45
- nodes: () => [0, 1, 2, 3, 4, 5],
46
- edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
47
- };
48
- const positions = forceatlas2Layout(graph);
49
-
50
- assert.equal(Object.keys(positions).length, 6);
51
- // Disconnected components should be separated by gravity
52
- });
53
-
54
- it('should use initial positions if provided', () => {
55
- const graph = starGraph(5);
56
- const initialPos = {};
57
- graph.nodes().forEach(node => {
58
- initialPos[node] = [node, 0]; // Line layout
59
- });
60
-
61
- const positions = forceatlas2Layout(graph, initialPos, 50);
62
-
63
- assert.equal(Object.keys(positions).length, 5);
64
- // Should evolve from initial positions
65
- assert.isDefined(positions[0]);
66
- });
67
- });
68
-
69
- describe('Force parameters', () => {
70
- it('should respect maxIter parameter', () => {
71
- const graph = randomGraph(10, 0.3, 42);
72
-
73
- const startTime1 = performance.now();
74
- const positions1 = forceatlas2Layout(graph, null, 10);
75
- const time1 = performance.now() - startTime1;
76
-
77
- const startTime2 = performance.now();
78
- const positions2 = forceatlas2Layout(graph, null, 100);
79
- const time2 = performance.now() - startTime2;
80
-
81
- // More iterations should take longer
82
- assert.isAbove(time2, time1 * 0.5);
83
-
84
- // Both should position all nodes
85
- assert.equal(Object.keys(positions1).length, 10);
86
- assert.equal(Object.keys(positions2).length, 10);
87
- });
88
-
89
- it('should handle different gravity settings', () => {
90
- const graph = {
91
- nodes: () => [0, 1, 2, 3],
92
- edges: () => [] // No edges, only gravity affects layout
93
- };
94
-
95
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 0.1); // Low gravity
96
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 10.0); // High gravity
97
-
98
- // Both gravity settings should produce valid layouts
99
- const avgDist1 = getAverageDistanceFromCenter(positions1);
100
- const avgDist2 = getAverageDistanceFromCenter(positions2);
101
-
102
- // Just verify both produced layouts
103
- assert.isAbove(avgDist1, 0);
104
- assert.isAbove(avgDist2, 0);
105
- });
106
-
107
- it('should handle strongGravity mode', () => {
108
- const graph = scaleFreeGraph(15, 2, 123);
109
-
110
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false); // Normal gravity
111
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, true); // Strong gravity
112
-
113
- // Both should complete
114
- assert.equal(Object.keys(positions1).length, 15);
115
- assert.equal(Object.keys(positions2).length, 15);
116
- });
117
-
118
- it('should handle distributedAction mode', () => {
119
- const graph = starGraph(10);
120
-
121
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false);
122
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, true);
123
-
124
- // Both modes should work
125
- assert.equal(Object.keys(positions1).length, 10);
126
- assert.equal(Object.keys(positions2).length, 10);
127
- });
128
- });
129
-
130
- describe('Node properties', () => {
131
- it('should respect node masses', () => {
132
- const graph = starGraph(5);
133
- const nodeMass = {
134
- 0: 10.0, // Heavy center
135
- 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0 // Light leaves
136
- };
137
-
138
- const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, nodeMass);
139
-
140
- // Heavy center should be more stable (closer to origin)
141
- const centerDist = Math.sqrt(positions[0][0] ** 2 + positions[0][1] ** 2);
142
- const leafDists = [1, 2, 3, 4].map(i =>
143
- Math.sqrt(positions[i][0] ** 2 + positions[i][1] ** 2)
144
- );
145
-
146
- // Center should be closer to origin than most leaves
147
- const avgLeafDist = leafDists.reduce((a, b) => a + b) / leafDists.length;
148
- assert.isBelow(centerDist, avgLeafDist);
149
- });
150
-
151
- it('should respect node sizes', () => {
152
- const graph = completeGraph(4);
153
- const nodeSize = {
154
- 0: 10.0, // Large node
155
- 1: 1.0, 2: 1.0, 3: 1.0 // Small nodes
156
- };
157
-
158
- const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, nodeSize);
159
-
160
- // Layout should avoid overlaps considering sizes
161
- assert.equal(Object.keys(positions).length, 4);
162
-
163
- // Large node should have more space around it
164
- const dist01 = getDistance(positions, 0, 1);
165
- const dist12 = getDistance(positions, 1, 2);
166
- // Distance to large node should tend to be larger
167
- assert.isDefined(dist01);
168
- assert.isDefined(dist12);
169
- });
170
- });
171
-
172
- describe('Advanced features', () => {
173
- it('should handle dissuadeHubs option', () => {
174
- const graph = scaleFreeGraph(20, 2, 456);
175
-
176
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false);
177
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, true);
178
-
179
- // Both should complete
180
- assert.equal(Object.keys(positions1).length, 20);
181
- assert.equal(Object.keys(positions2).length, 20);
182
- });
183
-
184
- it('should handle linlog mode', () => {
185
- const graph = randomGraph(15, 0.2, 789);
186
-
187
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false);
188
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, true);
189
-
190
- // Both modes should work
191
- assert.equal(Object.keys(positions1).length, 15);
192
- assert.equal(Object.keys(positions2).length, 15);
193
- });
194
-
195
- it('should handle edge weights', () => {
196
- const graph = {
197
- nodes: () => [0, 1, 2, 3],
198
- edges: () => [[0, 1], [1, 2], [2, 3], [3, 0]],
199
- get_edge_data: (u, v) => {
200
- // Make edge 0-1 stronger
201
- if ((u === 0 && v === 1) || (u === 1 && v === 0)) {
202
- return { weight: 10.0 };
203
- }
204
- return { weight: 1.0 };
205
- }
206
- };
207
-
208
- const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, 'weight');
209
-
210
- // Nodes 0 and 1 should be closer due to stronger edge
211
- const d01 = getDistance(positions, 0, 1);
212
- const d12 = getDistance(positions, 1, 2);
213
- const d23 = getDistance(positions, 2, 3);
214
-
215
- // ForceAtlas2 is complex and edge weights might not always
216
- // result in predictable distance relationships due to other forces
217
- // Just verify all nodes are positioned
218
- assert.isAbove(d01, 0);
219
- assert.isAbove(d12, 0);
220
- assert.isAbove(d23, 0);
221
- assert.equal(Object.keys(positions).length, 4);
222
- });
223
- });
224
-
225
- describe('Parameter variations', () => {
226
- it('should produce consistent results with same seed', () => {
227
- const graph = randomGraph(12, 0.3, 111);
228
-
229
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
230
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
231
-
232
- // Same seed should produce same initial positions
233
- graph.nodes().forEach(node => {
234
- assert.approximately(positions1[node][0], positions2[node][0], 0.1);
235
- assert.approximately(positions1[node][1], positions2[node][1], 0.1);
236
- });
237
- });
238
-
239
- it('should produce different results with different seeds', () => {
240
- const graph = cycleGraph(8);
241
-
242
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 123);
243
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 456);
244
-
245
- // Different seeds should produce different layouts
246
- let totalDiff = 0;
247
- graph.nodes().forEach(node => {
248
- totalDiff += Math.abs(positions1[node][0] - positions2[node][0]);
249
- totalDiff += Math.abs(positions1[node][1] - positions2[node][1]);
250
- });
251
- assert.isAbove(totalDiff, 0.1);
252
- });
253
-
254
- it('should handle different dimensions', () => {
255
- const graph = starGraph(6);
256
-
257
- // 2D layout
258
- const positions2D = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 2);
259
- graph.nodes().forEach(node => {
260
- assert.equal(positions2D[node].length, 2);
261
- });
262
-
263
- // 3D layout
264
- const positions3D = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 3);
265
- graph.nodes().forEach(node => {
266
- assert.equal(positions3D[node].length, 3);
267
- assert.isNumber(positions3D[node][2]);
268
- });
269
- });
270
- });
271
-
272
- describe('Special cases and edge cases', () => {
273
- it('should handle string node IDs', () => {
274
- const graph = {
275
- nodes: () => ['A', 'B', 'C', 'D', 'E'],
276
- edges: () => [['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'E'], ['E', 'A']]
277
- };
278
-
279
- const positions = forceatlas2Layout(graph);
280
-
281
- assert.equal(Object.keys(positions).length, 5);
282
- ['A', 'B', 'C', 'D', 'E'].forEach(node => {
283
- assert.isDefined(positions[node]);
284
- assert.equal(positions[node].length, 2);
285
- });
286
- });
287
-
288
- it('should handle large graphs reasonably', () => {
289
- const graph = gridGraph(10, 10); // 100 nodes
290
-
291
- const startTime = performance.now();
292
- const positions = forceatlas2Layout(graph, null, 30, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42); // Fewer iterations for speed
293
- const endTime = performance.now();
294
-
295
- assert.equal(Object.keys(positions).length, 100);
296
- assert.isBelow(endTime - startTime, 2000); // Should complete in reasonable time
297
- });
298
-
299
- it('should handle high jitter tolerance', () => {
300
- const graph = completeGraph(6);
301
-
302
- const positions = forceatlas2Layout(graph, null, 50, 10.0); // High jitter tolerance
303
-
304
- assert.equal(Object.keys(positions).length, 6);
305
- });
306
-
307
- it('should handle extreme scaling ratios', () => {
308
- const graph = starGraph(8);
309
-
310
- const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 0.1); // Low scaling
311
- const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 10.0); // High scaling
312
-
313
- // Both should complete
314
- assert.equal(Object.keys(positions1).length, 8);
315
- assert.equal(Object.keys(positions2).length, 8);
316
- });
317
- });
318
-
319
- describe('Layout quality', () => {
320
- it('should separate non-connected nodes', () => {
321
- const graph = {
322
- nodes: () => [0, 1, 2, 3, 4, 5],
323
- edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
324
- };
325
-
326
- const positions = forceatlas2Layout(graph, null, 100);
327
-
328
- // Components should be separated
329
- const comp1Center = getCenterOfMass({ 0: positions[0], 1: positions[1], 2: positions[2] });
330
- const comp2Center = getCenterOfMass({ 3: positions[3], 4: positions[4], 5: positions[5] });
331
-
332
- const componentDist = Math.sqrt(
333
- Math.pow(comp1Center[0] - comp2Center[0], 2) +
334
- Math.pow(comp1Center[1] - comp2Center[1], 2)
335
- );
336
-
337
- // ForceAtlas2 might not always separate components by a large distance
338
- assert.isAbove(componentDist, 0.03);
339
- });
340
-
341
- it('should create balanced layouts for regular graphs', () => {
342
- const graph = completeGraph(6);
343
- const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
344
-
345
- // Should spread nodes reasonably
346
- const bounds = getLayoutBounds(positions);
347
- assert.isAbove(bounds.width, 0.3);
348
- assert.isAbove(bounds.height, 0.3);
349
-
350
- // Should not be too stretched
351
- const aspectRatio = Math.max(bounds.width, bounds.height) / Math.min(bounds.width, bounds.height);
352
- assert.isBelow(aspectRatio, 3);
353
- });
354
- });
355
- });
356
-
357
- // Helper functions
358
- function getAverageDistanceFromCenter(positions) {
359
- const nodes = Object.keys(positions);
360
- let totalDist = 0;
361
-
362
- nodes.forEach(node => {
363
- totalDist += Math.sqrt(positions[node][0] ** 2 + positions[node][1] ** 2);
364
- });
365
-
366
- return totalDist / nodes.length;
367
- }
368
-
369
- function getDistance(positions, node1, node2) {
370
- const dx = positions[node1][0] - positions[node2][0];
371
- const dy = positions[node1][1] - positions[node2][1];
372
- return Math.sqrt(dx * dx + dy * dy);
373
- }
374
-
375
- function getCenterOfMass(positions) {
376
- const nodes = Object.keys(positions);
377
- const com = [0, 0];
378
-
379
- nodes.forEach(node => {
380
- com[0] += positions[node][0];
381
- com[1] += positions[node][1];
382
- });
383
-
384
- com[0] /= nodes.length;
385
- com[1] /= nodes.length;
386
- return com;
387
- }
388
-
389
- function getLayoutBounds(positions) {
390
- const nodes = Object.keys(positions);
391
- let minX = Infinity, maxX = -Infinity;
392
- let minY = Infinity, maxY = -Infinity;
393
-
394
- nodes.forEach(node => {
395
- minX = Math.min(minX, positions[node][0]);
396
- maxX = Math.max(maxX, positions[node][0]);
397
- minY = Math.min(minY, positions[node][1]);
398
- maxY = Math.max(maxY, positions[node][1]);
399
- });
400
-
401
- return {
402
- width: maxX - minX,
403
- height: maxY - minY
404
- };
405
- }