@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,404 +0,0 @@
1
- import { describe, it, assert } from 'vitest';
2
- import {
3
- multipartiteLayout,
4
- completeGraph,
5
- cycleGraph,
6
- randomGraph
7
- } from '../src';
8
-
9
- describe('Multipartite Layout', () => {
10
- describe('Basic functionality', () => {
11
- it('should position all nodes', () => {
12
- const graph = completeGraph(12);
13
- const subsets = [
14
- [0, 1, 2, 3],
15
- [4, 5, 6, 7],
16
- [8, 9, 10, 11]
17
- ];
18
- const positions = multipartiteLayout(graph, subsets);
19
-
20
- assert.equal(Object.keys(positions).length, 12);
21
- graph.nodes().forEach(node => {
22
- assert.isDefined(positions[node]);
23
- assert.equal(positions[node].length, 2);
24
- assert.isNumber(positions[node][0]);
25
- assert.isNumber(positions[node][1]);
26
- });
27
- });
28
-
29
- it('should handle empty graph', () => {
30
- const emptyGraph = { nodes: () => [], edges: () => [] };
31
- const positions = multipartiteLayout(emptyGraph, []);
32
-
33
- assert.equal(Object.keys(positions).length, 0);
34
- });
35
-
36
- it('should handle single node', () => {
37
- const singleNode = { nodes: () => ['A'], edges: () => [] };
38
- const positions = multipartiteLayout(singleNode, [['A']]);
39
-
40
- assert.equal(Object.keys(positions).length, 1);
41
- assert.deepEqual(positions['A'], [0, 0]);
42
- });
43
-
44
- it('should handle disconnected multipartite graph', () => {
45
- const graph = {
46
- nodes: () => ['A0', 'A1', 'B0', 'B1', 'C0', 'C1'],
47
- edges: () => [['A0', 'B0'], ['A1', 'B1']] // C nodes disconnected
48
- };
49
- const subsets = [['A0', 'A1'], ['B0', 'B1'], ['C0', 'C1']];
50
-
51
- const positions = multipartiteLayout(graph, subsets);
52
-
53
- assert.equal(Object.keys(positions).length, 6);
54
- graph.nodes().forEach(node => {
55
- assert.isDefined(positions[node]);
56
- assert.equal(positions[node].length, 2);
57
- });
58
- });
59
- });
60
-
61
- describe('Multipartite arrangement', () => {
62
- it('should arrange subsets in layers for vertical alignment', () => {
63
- const graph = completeGraph(9);
64
- const subsets = [
65
- [0, 1, 2],
66
- [3, 4, 5],
67
- [6, 7, 8]
68
- ];
69
-
70
- const positions = multipartiteLayout(graph, subsets, 'vertical');
71
-
72
- // Calculate average x-coordinate for each subset
73
- const avgX0 = subsets[0].reduce((sum, n) => sum + positions[n][0], 0) / 3;
74
- const avgX1 = subsets[1].reduce((sum, n) => sum + positions[n][0], 0) / 3;
75
- const avgX2 = subsets[2].reduce((sum, n) => sum + positions[n][0], 0) / 3;
76
-
77
- // Subsets should be separated horizontally
78
- assert.notEqual(avgX0, avgX1);
79
- assert.notEqual(avgX1, avgX2);
80
- assert.notEqual(avgX0, avgX2);
81
-
82
- // Should be evenly spaced
83
- const spacing1 = Math.abs(avgX1 - avgX0);
84
- const spacing2 = Math.abs(avgX2 - avgX1);
85
- assert.approximately(spacing1, spacing2, 0.1);
86
- });
87
-
88
- it('should arrange subsets in layers for horizontal alignment', () => {
89
- const graph = completeGraph(8);
90
- const subsets = [
91
- [0, 1],
92
- [2, 3, 4],
93
- [5, 6, 7]
94
- ];
95
-
96
- const positions = multipartiteLayout(graph, subsets, 'horizontal');
97
-
98
- // Calculate average y-coordinate for each subset
99
- const avgY0 = subsets[0].reduce((sum, n) => sum + positions[n][1], 0) / subsets[0].length;
100
- const avgY1 = subsets[1].reduce((sum, n) => sum + positions[n][1], 0) / subsets[1].length;
101
- const avgY2 = subsets[2].reduce((sum, n) => sum + positions[n][1], 0) / subsets[2].length;
102
-
103
- // Subsets should be separated vertically
104
- assert.notEqual(avgY0, avgY1);
105
- assert.notEqual(avgY1, avgY2);
106
- assert.notEqual(avgY0, avgY2);
107
- });
108
-
109
- it('should evenly space nodes within each subset', () => {
110
- const graph = {
111
- nodes: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
112
- edges: () => [[0, 3], [1, 4], [2, 5], [3, 6], [4, 7], [5, 8]]
113
- };
114
- const subsets = [
115
- [0, 1, 2],
116
- [3, 4, 5],
117
- [6, 7, 8, 9]
118
- ];
119
-
120
- const positions = multipartiteLayout(graph, subsets, 'vertical');
121
-
122
- // Check spacing within first subset
123
- const subset0Y = subsets[0].map(n => positions[n][1]).sort((a, b) => a - b);
124
- const spacings0 = [];
125
- for (let i = 1; i < subset0Y.length; i++) {
126
- spacings0.push(subset0Y[i] - subset0Y[i-1]);
127
- }
128
-
129
- // All spacings should be equal
130
- if (spacings0.length > 0) {
131
- const avgSpacing = spacings0.reduce((a, b) => a + b) / spacings0.length;
132
- spacings0.forEach(s => assert.approximately(s, avgSpacing, 1e-10));
133
- }
134
- });
135
-
136
- it('should handle subsets of different sizes', () => {
137
- const graph = {
138
- nodes: () => ['A', 'B0', 'B1', 'C0', 'C1', 'C2', 'C3'],
139
- edges: () => [['A', 'B0'], ['A', 'B1'], ['B0', 'C0'], ['B1', 'C2']]
140
- };
141
- const subsets = [['A'], ['B0', 'B1'], ['C0', 'C1', 'C2', 'C3']];
142
-
143
- const positions = multipartiteLayout(graph, subsets);
144
-
145
- // Single node should be centered
146
- assert.equal(positions['A'][1], 0);
147
-
148
- // Larger subsets should still be evenly spaced
149
- const cPositions = ['C0', 'C1', 'C2', 'C3'].map(n => positions[n][1]).sort((a, b) => a - b);
150
- const span = cPositions[cPositions.length - 1] - cPositions[0];
151
- assert.isAbove(span, 0);
152
- });
153
- });
154
-
155
- describe('Parameter variations', () => {
156
- it('should respect scale parameter', () => {
157
- const graph = completeGraph(12);
158
- const subsets = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]];
159
-
160
- const positions1 = multipartiteLayout(graph, subsets, 'vertical', 1);
161
- const positions2 = multipartiteLayout(graph, subsets, 'vertical', 2);
162
-
163
- // Calculate layout spans
164
- const span1 = getLayoutSpan(positions1);
165
- const span2 = getLayoutSpan(positions2);
166
-
167
- assert.approximately(span2.width / span1.width, 2, 0.1);
168
- assert.approximately(span2.height / span1.height, 2, 0.1);
169
- });
170
-
171
- it('should respect center parameter', () => {
172
- const graph = completeGraph(6);
173
- const subsets = [[0, 1], [2, 3], [4, 5]];
174
- const center = [10, -5];
175
-
176
- const positions = multipartiteLayout(graph, subsets, 'vertical', 1, center);
177
-
178
- // Calculate center of mass
179
- const com = getCenterOfMass(positions);
180
-
181
- assert.approximately(com[0], center[0], 0.5);
182
- assert.approximately(com[1], center[1], 0.5);
183
- });
184
-
185
- it('should respect alignment parameter', () => {
186
- const graph = completeGraph(9);
187
- const subsets = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
188
-
189
- const verticalPos = multipartiteLayout(graph, subsets, 'vertical');
190
- const horizontalPos = multipartiteLayout(graph, subsets, 'horizontal');
191
-
192
- // In vertical, subsets differ in x; in horizontal, subsets differ in y
193
- const vDiffX = Math.abs(verticalPos[0][0] - verticalPos[3][0]);
194
- const vDiffY = Math.abs(verticalPos[0][1] - verticalPos[3][1]);
195
-
196
- const hDiffX = Math.abs(horizontalPos[0][0] - horizontalPos[3][0]);
197
- const hDiffY = Math.abs(horizontalPos[0][1] - horizontalPos[3][1]);
198
-
199
- // Vertical: large X difference, potentially small Y difference
200
- assert.isAbove(vDiffX, 0.5);
201
-
202
- // Horizontal: potentially small X difference, large Y difference
203
- assert.isAbove(hDiffY, 0.5);
204
- });
205
- });
206
-
207
- describe('Special cases and edge cases', () => {
208
- it('should handle string node IDs', () => {
209
- const graph = {
210
- nodes: () => ['red1', 'red2', 'green1', 'green2', 'blue1', 'blue2'],
211
- edges: () => [['red1', 'green1'], ['red2', 'green2'], ['green1', 'blue1'], ['green2', 'blue2']]
212
- };
213
- const subsets = [['red1', 'red2'], ['green1', 'green2'], ['blue1', 'blue2']];
214
-
215
- const positions = multipartiteLayout(graph, subsets);
216
-
217
- assert.equal(Object.keys(positions).length, 6);
218
- graph.nodes().forEach(node => {
219
- assert.isDefined(positions[node]);
220
- assert.equal(positions[node].length, 2);
221
- });
222
- });
223
-
224
- it('should handle nodes not in any subset', () => {
225
- const graph = {
226
- nodes: () => [0, 1, 2, 3, 4, 5],
227
- edges: () => [[0, 2], [1, 3], [2, 4]]
228
- };
229
- const subsets = [[0, 1], [2, 3]]; // Nodes 4, 5 not in subsets
230
-
231
- const positions = multipartiteLayout(graph, subsets);
232
-
233
- // Only nodes in subsets should be positioned
234
- assert.equal(Object.keys(positions).length, 4);
235
- assert.isUndefined(positions[4]);
236
- assert.isUndefined(positions[5]);
237
- });
238
-
239
- it('should handle empty subsets', () => {
240
- const graph = completeGraph(6);
241
- const subsets = [
242
- [0, 1],
243
- [], // Empty subset
244
- [2, 3, 4, 5]
245
- ];
246
-
247
- const positions = multipartiteLayout(graph, subsets);
248
-
249
- // Should position nodes from non-empty subsets
250
- assert.equal(Object.keys(positions).length, 6);
251
- });
252
-
253
- it('should produce consistent results', () => {
254
- const graph = randomGraph(15, 0.3, 42);
255
- const subsets = [
256
- graph.nodes().slice(0, 5),
257
- graph.nodes().slice(5, 10),
258
- graph.nodes().slice(10, 15)
259
- ];
260
-
261
- const positions1 = multipartiteLayout(graph, subsets);
262
- const positions2 = multipartiteLayout(graph, subsets);
263
-
264
- // Multipartite layout is deterministic
265
- graph.nodes().forEach(node => {
266
- assert.deepEqual(positions1[node], positions2[node]);
267
- });
268
- });
269
-
270
- it('should handle complete k-partite graphs', () => {
271
- // Complete 3-partite graph K(2,3,4)
272
- const graph = {
273
- nodes: () => ['A0', 'A1', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2', 'C3'],
274
- edges: () => []
275
- };
276
-
277
- // Add all edges between different subsets
278
- const subsets = [['A0', 'A1'], ['B0', 'B1', 'B2'], ['C0', 'C1', 'C2', 'C3']];
279
- for (let i = 0; i < subsets.length; i++) {
280
- for (let j = i + 1; j < subsets.length; j++) {
281
- for (const u of subsets[i]) {
282
- for (const v of subsets[j]) {
283
- graph.edges().push([u, v]);
284
- }
285
- }
286
- }
287
- }
288
-
289
- const positions = multipartiteLayout(graph, subsets);
290
-
291
- assert.equal(Object.keys(positions).length, 9);
292
-
293
- // Check that subsets are well-separated
294
- const avgX = subsets.map(subset =>
295
- subset.reduce((sum, n) => sum + positions[n][0], 0) / subset.length
296
- );
297
-
298
- assert.notEqual(avgX[0], avgX[1]);
299
- assert.notEqual(avgX[1], avgX[2]);
300
- });
301
-
302
- it('should handle large multipartite graphs efficiently', () => {
303
- const n = 60; // 20 nodes per subset
304
- const nodes = Array.from({ length: n }, (_, i) => i);
305
- const graph = {
306
- nodes: () => nodes,
307
- edges: () => []
308
- };
309
-
310
- const subsets = [
311
- nodes.slice(0, 20),
312
- nodes.slice(20, 40),
313
- nodes.slice(40, 60)
314
- ];
315
-
316
- const startTime = performance.now();
317
- const positions = multipartiteLayout(graph, subsets);
318
- const endTime = performance.now();
319
-
320
- assert.equal(Object.keys(positions).length, 60);
321
- assert.isBelow(endTime - startTime, 100); // Should be very fast
322
- });
323
- });
324
-
325
- describe('Layout quality', () => {
326
- it('should minimize edge crossings between adjacent layers', () => {
327
- // Create a graph with clear layer structure
328
- const graph = {
329
- nodes: () => ['A0', 'A1', 'A2', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2'],
330
- edges: () => [
331
- ['A0', 'B0'], ['A1', 'B1'], ['A2', 'B2'], // Parallel edges
332
- ['B0', 'C0'], ['B1', 'C1'], ['B2', 'C2'] // Parallel edges
333
- ]
334
- };
335
- const subsets = [['A0', 'A1', 'A2'], ['B0', 'B1', 'B2'], ['C0', 'C1', 'C2']];
336
-
337
- const positions = multipartiteLayout(graph, subsets, 'vertical');
338
-
339
- // Check that connected nodes have similar y-coordinates
340
- graph.edges().forEach(([u, v]) => {
341
- const yDiff = Math.abs(positions[u][1] - positions[v][1]);
342
- assert.isBelow(yDiff, 2); // Connected nodes should be roughly aligned
343
- });
344
- });
345
-
346
- it('should create clear visual separation between layers', () => {
347
- const graph = completeGraph(15);
348
- const subsets = [
349
- [0, 1, 2, 3, 4],
350
- [5, 6, 7, 8, 9],
351
- [10, 11, 12, 13, 14]
352
- ];
353
-
354
- const positions = multipartiteLayout(graph, subsets, 'vertical');
355
-
356
- // Calculate min distance between layers
357
- let minInterLayerDist = Infinity;
358
- for (let i = 0; i < subsets.length - 1; i++) {
359
- for (const u of subsets[i]) {
360
- for (const v of subsets[i + 1]) {
361
- const dist = Math.abs(positions[u][0] - positions[v][0]);
362
- minInterLayerDist = Math.min(minInterLayerDist, dist);
363
- }
364
- }
365
- }
366
-
367
- // Layers should be clearly separated
368
- assert.isAbove(minInterLayerDist, 0.4); // Allow slightly closer layers
369
- });
370
- });
371
- });
372
-
373
- // Helper functions
374
- function getLayoutSpan(positions) {
375
- const nodes = Object.keys(positions);
376
- let minX = Infinity, maxX = -Infinity;
377
- let minY = Infinity, maxY = -Infinity;
378
-
379
- nodes.forEach(node => {
380
- minX = Math.min(minX, positions[node][0]);
381
- maxX = Math.max(maxX, positions[node][0]);
382
- minY = Math.min(minY, positions[node][1]);
383
- maxY = Math.max(maxY, positions[node][1]);
384
- });
385
-
386
- return {
387
- width: maxX - minX,
388
- height: maxY - minY
389
- };
390
- }
391
-
392
- function getCenterOfMass(positions) {
393
- const nodes = Object.keys(positions);
394
- const com = [0, 0];
395
-
396
- nodes.forEach(node => {
397
- com[0] += positions[node][0];
398
- com[1] += positions[node][1];
399
- });
400
-
401
- com[0] /= nodes.length;
402
- com[1] /= nodes.length;
403
- return com;
404
- }
@@ -1,266 +0,0 @@
1
- import { describe, it, assert } from 'vitest';
2
- import {
3
- planarLayout,
4
- completeGraph,
5
- cycleGraph,
6
- starGraph,
7
- gridGraph,
8
- wheelGraph
9
- } from '../src';
10
-
11
- describe('Planar Layout', () => {
12
- describe('Basic functionality', () => {
13
- it('should position all nodes for planar graphs', () => {
14
- // Cycle graph is planar
15
- const graph = cycleGraph(6);
16
- const positions = planarLayout(graph);
17
-
18
- assert.equal(Object.keys(positions).length, 6);
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 = planarLayout(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 = planarLayout(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 throw error for non-planar graphs', () => {
44
- // K5 (complete graph with 5 nodes) is non-planar
45
- const graph = completeGraph(5);
46
-
47
- assert.throws(() => {
48
- planarLayout(graph);
49
- }, 'G is not planar');
50
- });
51
-
52
- it('should only support 2D layouts', () => {
53
- const graph = cycleGraph(4);
54
-
55
- // 2D works
56
- assert.doesNotThrow(() => {
57
- planarLayout(graph, 1, [0, 0], 2);
58
- });
59
-
60
- // 3D should throw error
61
- assert.throws(() => {
62
- planarLayout(graph, 1, [0, 0], 3);
63
- }, 'can only handle 2 dimensions');
64
- });
65
- });
66
-
67
- describe('Planar graph types', () => {
68
- it('should handle star graphs (which are planar)', () => {
69
- const graph = starGraph(8);
70
- const positions = planarLayout(graph);
71
-
72
- assert.equal(Object.keys(positions).length, 8);
73
-
74
- // Center node should be distinguishable
75
- const centerPos = positions[0];
76
- assert.isDefined(centerPos);
77
- });
78
-
79
- it('should handle grid graphs (which are planar)', () => {
80
- const graph = gridGraph(3, 3);
81
- const positions = planarLayout(graph);
82
-
83
- assert.equal(Object.keys(positions).length, 9);
84
-
85
- // All nodes should be positioned
86
- graph.nodes().forEach(node => {
87
- assert.isDefined(positions[node]);
88
- });
89
- });
90
-
91
- it('should handle wheel graphs (which are planar)', () => {
92
- const graph = wheelGraph(6);
93
- const positions = planarLayout(graph);
94
-
95
- assert.equal(Object.keys(positions).length, 6);
96
- });
97
-
98
- it('should handle tree structures', () => {
99
- const tree = {
100
- nodes: () => [0, 1, 2, 3, 4, 5, 6],
101
- edges: () => [[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [2, 6]]
102
- };
103
-
104
- const positions = planarLayout(tree);
105
- assert.equal(Object.keys(positions).length, 7);
106
- });
107
- });
108
-
109
- describe('Parameter variations', () => {
110
- it('should respect scale parameter', () => {
111
- const graph = cycleGraph(8);
112
-
113
- const positions1 = planarLayout(graph, 1);
114
- const positions2 = planarLayout(graph, 2);
115
-
116
- // Calculate bounding boxes
117
- const bounds1 = getLayoutBounds(positions1);
118
- const bounds2 = getLayoutBounds(positions2);
119
-
120
- assert.approximately(bounds2.width / bounds1.width, 2, 0.1);
121
- assert.approximately(bounds2.height / bounds1.height, 2, 0.1);
122
- });
123
-
124
- it('should respect center parameter', () => {
125
- const graph = starGraph(5);
126
- const center = [10, -5];
127
-
128
- const positions = planarLayout(graph, 1, center);
129
-
130
- // Calculate center of mass
131
- const com = getCenterOfMass(positions);
132
-
133
- assert.approximately(com[0], center[0], 1);
134
- assert.approximately(com[1], center[1], 1);
135
- });
136
- });
137
-
138
- describe('Special cases and edge cases', () => {
139
- it('should handle string node IDs', () => {
140
- const graph = {
141
- nodes: () => ['A', 'B', 'C', 'D'],
142
- edges: () => [['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'A']]
143
- };
144
-
145
- const positions = planarLayout(graph);
146
-
147
- assert.equal(Object.keys(positions).length, 4);
148
- ['A', 'B', 'C', 'D'].forEach(node => {
149
- assert.isDefined(positions[node]);
150
- assert.equal(positions[node].length, 2);
151
- });
152
- });
153
-
154
- it('should produce consistent results', () => {
155
- const graph = gridGraph(4, 4);
156
-
157
- const positions1 = planarLayout(graph);
158
- const positions2 = planarLayout(graph);
159
-
160
- // Planar layout should produce valid results
161
- assert.equal(Object.keys(positions1).length, 16);
162
- assert.equal(Object.keys(positions2).length, 16);
163
- });
164
-
165
- it('should handle disconnected planar components', () => {
166
- const graph = {
167
- nodes: () => [0, 1, 2, 3, 4, 5],
168
- edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]] // Two triangles
169
- };
170
-
171
- const positions = planarLayout(graph);
172
-
173
- assert.equal(Object.keys(positions).length, 6);
174
- graph.nodes().forEach(node => {
175
- assert.isDefined(positions[node]);
176
- });
177
- });
178
-
179
- it('should reject K3,3 (complete bipartite non-planar)', () => {
180
- const k33 = {
181
- nodes: () => ['A1', 'A2', 'A3', 'B1', 'B2', 'B3'],
182
- edges: () => [
183
- ['A1', 'B1'], ['A1', 'B2'], ['A1', 'B3'],
184
- ['A2', 'B1'], ['A2', 'B2'], ['A2', 'B3'],
185
- ['A3', 'B1'], ['A3', 'B2'], ['A3', 'B3']
186
- ]
187
- };
188
-
189
- assert.throws(() => {
190
- planarLayout(k33);
191
- }, 'G is not planar');
192
- });
193
- });
194
-
195
- describe('Layout quality', () => {
196
- it('should produce non-crossing edges for planar graphs', () => {
197
- const graph = cycleGraph(10);
198
- const positions = planarLayout(graph);
199
-
200
- // While we can't easily verify no crossings algorithmically,
201
- // we can check that nodes are well-separated
202
- const minDist = getMinimumDistance(positions);
203
- assert.isAbove(minDist, 0.1);
204
- });
205
-
206
- it('should handle embedded graphs efficiently', () => {
207
- const graph = gridGraph(5, 5); // 25 nodes
208
-
209
- const startTime = performance.now();
210
- const positions = planarLayout(graph);
211
- const endTime = performance.now();
212
-
213
- assert.equal(Object.keys(positions).length, 25);
214
- assert.isBelow(endTime - startTime, 100); // Should be fast
215
- });
216
- });
217
- });
218
-
219
- // Helper functions
220
- function getLayoutBounds(positions) {
221
- const nodes = Object.keys(positions);
222
- let minX = Infinity, maxX = -Infinity;
223
- let minY = Infinity, maxY = -Infinity;
224
-
225
- nodes.forEach(node => {
226
- minX = Math.min(minX, positions[node][0]);
227
- maxX = Math.max(maxX, positions[node][0]);
228
- minY = Math.min(minY, positions[node][1]);
229
- maxY = Math.max(maxY, positions[node][1]);
230
- });
231
-
232
- return {
233
- width: maxX - minX,
234
- height: maxY - minY
235
- };
236
- }
237
-
238
- function getCenterOfMass(positions) {
239
- const nodes = Object.keys(positions);
240
- const com = [0, 0];
241
-
242
- nodes.forEach(node => {
243
- com[0] += positions[node][0];
244
- com[1] += positions[node][1];
245
- });
246
-
247
- com[0] /= nodes.length;
248
- com[1] /= nodes.length;
249
- return com;
250
- }
251
-
252
- function getMinimumDistance(positions) {
253
- const nodes = Object.keys(positions);
254
- let minDist = Infinity;
255
-
256
- for (let i = 0; i < nodes.length; i++) {
257
- for (let j = i + 1; j < nodes.length; j++) {
258
- const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
259
- const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
260
- const dist = Math.sqrt(dx * dx + dy * dy);
261
- minDist = Math.min(minDist, dist);
262
- }
263
- }
264
-
265
- return minDist;
266
- }