@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.
- package/README.md +317 -237
- package/dist/src/algorithms/index.d.ts +5 -0
- package/dist/src/algorithms/index.js +6 -0
- package/dist/src/algorithms/index.js.map +1 -0
- package/dist/src/algorithms/optimization/index.d.ts +7 -0
- package/dist/src/algorithms/optimization/index.js +7 -0
- package/dist/src/algorithms/optimization/index.js.map +1 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +32 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +176 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -0
- package/dist/src/algorithms/optimization/lbfgs.d.ts +13 -0
- package/dist/src/algorithms/optimization/lbfgs.js +56 -0
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -0
- package/dist/src/algorithms/optimization/line-search.d.ts +15 -0
- package/dist/src/algorithms/optimization/line-search.js +37 -0
- package/dist/src/algorithms/optimization/line-search.js.map +1 -0
- package/dist/src/algorithms/optimization/types.d.ts +5 -0
- package/dist/src/algorithms/optimization/types.js +5 -0
- package/dist/src/algorithms/optimization/types.js.map +1 -0
- package/dist/src/algorithms/planarity/check.d.ts +17 -0
- package/dist/src/algorithms/planarity/check.js +30 -0
- package/dist/src/algorithms/planarity/check.js.map +1 -0
- package/dist/src/algorithms/planarity/embedding.d.ts +29 -0
- package/dist/src/algorithms/planarity/embedding.js +179 -0
- package/dist/src/algorithms/planarity/embedding.js.map +1 -0
- package/dist/src/algorithms/planarity/index.d.ts +7 -0
- package/dist/src/algorithms/planarity/index.js +8 -0
- package/dist/src/algorithms/planarity/index.js.map +1 -0
- package/dist/src/algorithms/planarity/lr-test.d.ts +15 -0
- package/dist/src/algorithms/planarity/lr-test.js +54 -0
- package/dist/src/algorithms/planarity/lr-test.js.map +1 -0
- package/dist/src/algorithms/planarity/special-graphs.d.ts +28 -0
- package/dist/src/algorithms/planarity/special-graphs.js +108 -0
- package/dist/src/algorithms/planarity/special-graphs.js.map +1 -0
- package/dist/src/generators/basic.d.ts +28 -0
- package/dist/src/generators/basic.js +79 -0
- package/dist/src/generators/basic.js.map +1 -0
- package/dist/src/generators/bipartite.d.ts +16 -0
- package/dist/src/generators/bipartite.js +40 -0
- package/dist/src/generators/bipartite.js.map +1 -0
- package/dist/src/generators/grid.d.ts +11 -0
- package/dist/src/generators/grid.js +37 -0
- package/dist/src/generators/grid.js.map +1 -0
- package/dist/src/generators/index.d.ts +8 -0
- package/dist/src/generators/index.js +9 -0
- package/dist/src/generators/index.js.map +1 -0
- package/dist/src/generators/random.d.ts +12 -0
- package/dist/src/generators/random.js +34 -0
- package/dist/src/generators/random.js.map +1 -0
- package/dist/src/generators/scale-free.d.ts +12 -0
- package/dist/src/generators/scale-free.js +62 -0
- package/dist/src/generators/scale-free.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/layouts/basic/index.d.ts +4 -0
- package/dist/src/layouts/basic/index.js +5 -0
- package/dist/src/layouts/basic/index.js.map +1 -0
- package/dist/src/layouts/basic/random.d.ts +14 -0
- package/dist/src/layouts/basic/random.js +27 -0
- package/dist/src/layouts/basic/random.js.map +1 -0
- package/dist/src/layouts/force-directed/arf.d.ts +13 -0
- package/dist/src/layouts/force-directed/arf.js +103 -0
- package/dist/src/layouts/force-directed/arf.js.map +1 -0
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +22 -0
- package/dist/src/layouts/force-directed/forceatlas2.js +322 -0
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +19 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +131 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/force-directed/index.d.ts +8 -0
- package/dist/src/layouts/force-directed/index.js +9 -0
- package/dist/src/layouts/force-directed/index.js.map +1 -0
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +15 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js +87 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -0
- package/dist/src/layouts/force-directed/spring.d.ts +19 -0
- package/dist/src/layouts/force-directed/spring.js +23 -0
- package/dist/src/layouts/force-directed/spring.js.map +1 -0
- package/dist/src/layouts/geometric/circular.d.ts +14 -0
- package/dist/src/layouts/geometric/circular.js +67 -0
- package/dist/src/layouts/geometric/circular.js.map +1 -0
- package/dist/src/layouts/geometric/index.d.ts +6 -0
- package/dist/src/layouts/geometric/index.js +7 -0
- package/dist/src/layouts/geometric/index.js.map +1 -0
- package/dist/src/layouts/geometric/shell.d.ts +15 -0
- package/dist/src/layouts/geometric/shell.js +67 -0
- package/dist/src/layouts/geometric/shell.js.map +1 -0
- package/dist/src/layouts/geometric/spiral.d.ts +16 -0
- package/dist/src/layouts/geometric/spiral.js +73 -0
- package/dist/src/layouts/geometric/spiral.js.map +1 -0
- package/dist/src/layouts/hierarchical/bfs.d.ts +12 -0
- package/dist/src/layouts/hierarchical/bfs.js +62 -0
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -0
- package/dist/src/layouts/hierarchical/bipartite.d.ts +16 -0
- package/dist/src/layouts/hierarchical/bipartite.js +72 -0
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -0
- package/dist/src/layouts/hierarchical/index.d.ts +6 -0
- package/dist/src/layouts/hierarchical/index.js +7 -0
- package/dist/src/layouts/hierarchical/index.js.map +1 -0
- package/dist/src/layouts/hierarchical/multipartite.d.ts +12 -0
- package/dist/src/layouts/hierarchical/multipartite.js +73 -0
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -0
- package/dist/src/layouts/index.d.ts +8 -0
- package/dist/src/layouts/index.js +9 -0
- package/dist/src/layouts/index.js.map +1 -0
- package/dist/src/layouts/specialized/index.d.ts +5 -0
- package/dist/src/layouts/specialized/index.js +6 -0
- package/dist/src/layouts/specialized/index.js.map +1 -0
- package/dist/src/layouts/specialized/planar.d.ts +14 -0
- package/dist/src/layouts/specialized/planar.js +47 -0
- package/dist/src/layouts/specialized/planar.js.map +1 -0
- package/dist/src/layouts/specialized/spectral.d.ts +14 -0
- package/dist/src/layouts/specialized/spectral.js +106 -0
- package/dist/src/layouts/specialized/spectral.js.map +1 -0
- package/dist/src/types/embedding.d.ts +9 -0
- package/dist/src/types/embedding.js +5 -0
- package/dist/src/types/embedding.js.map +1 -0
- package/dist/src/types/graph.d.ts +10 -0
- package/dist/src/types/graph.js +5 -0
- package/dist/src/types/graph.js.map +1 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/layout.d.ts +6 -0
- package/dist/src/types/layout.js +5 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/utils/graph.d.ts +34 -0
- package/dist/src/utils/graph.js +69 -0
- package/dist/src/utils/graph.js.map +1 -0
- package/dist/src/utils/index.d.ts +8 -0
- package/dist/src/utils/index.js +9 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/numpy.d.ts +17 -0
- package/dist/src/utils/numpy.js +101 -0
- package/dist/src/utils/numpy.js.map +1 -0
- package/dist/src/utils/params.d.ts +17 -0
- package/dist/src/utils/params.js +22 -0
- package/dist/src/utils/params.js.map +1 -0
- package/dist/src/utils/random.d.ts +14 -0
- package/dist/src/utils/random.js +42 -0
- package/dist/src/utils/random.js.map +1 -0
- package/dist/src/utils/rescale.d.ts +21 -0
- package/dist/src/utils/rescale.js +119 -0
- package/dist/src/utils/rescale.js.map +1 -0
- package/dist/vitest.config.js +1 -1
- package/dist/vitest.config.js.map +1 -1
- package/package.json +15 -1
- package/.env.example +0 -11
- package/.github/workflows/ci.yml +0 -101
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -45
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -1
- package/DEPLOYMENT.md +0 -59
- package/commitlint.config.js +0 -37
- package/examples/3d-force-directed.html +0 -611
- package/examples/3d-kamada-kawai.html +0 -379
- package/examples/3d-layout-comparison.html +0 -448
- package/examples/3d-spherical-layout.html +0 -319
- package/examples/arf-layout.html +0 -524
- package/examples/bfs-layout.html +0 -487
- package/examples/bipartite-layout.html +0 -382
- package/examples/circular-layout.html +0 -292
- package/examples/forceatlas2-layout.html +0 -588
- package/examples/index.html +0 -241
- package/examples/kamada-kawai-layout.html +0 -425
- package/examples/layout-helpers.js +0 -486
- package/examples/multipartite-layout.html +0 -410
- package/examples/planar-layout.html +0 -388
- package/examples/random-layout.html +0 -328
- package/examples/shell-layout.html +0 -323
- package/examples/spectral-layout.html +0 -335
- package/examples/spiral-layout.html +0 -447
- package/examples/spring-layout.html +0 -313
- package/test/arf-layout.test.ts +0 -443
- package/test/bfs-layout.test.ts +0 -427
- package/test/bipartite-layout.test.ts +0 -344
- package/test/circular-layout.test.ts +0 -442
- package/test/forceatlas2-layout.test.ts +0 -405
- package/test/fruchterman-reingold-layout.test.ts +0 -477
- package/test/graph-generators.test.ts +0 -450
- package/test/kamada-kawai-layout.test.ts +0 -623
- package/test/multipartite-layout.test.ts +0 -404
- package/test/planar-layout.test.ts +0 -266
- package/test/random-layout.test.ts +0 -254
- package/test/rescale-layout.test.ts +0 -373
- package/test/shell-layout.test.ts +0 -347
- package/test/spectral-layout.test.ts +0 -378
- package/test/spiral-layout.test.ts +0 -338
- package/test/spring-layout.test.ts +0 -241
- package/test/test-utils.ts +0 -34
- package/test/utils-graph.test.ts +0 -155
- package/test/utils-index.test.ts +0 -77
- package/test/utils-numpy.test.ts +0 -272
- package/test/utils-params.test.ts +0 -99
- package/test/utils-random.test.ts +0 -229
- package/tsconfig.json +0 -16
- package/vite.config.js +0 -36
- package/vitest.config.ts +0 -30
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
circularLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
wheelGraph,
|
|
8
|
-
gridGraph,
|
|
9
|
-
randomGraph
|
|
10
|
-
} from '../src';
|
|
11
|
-
|
|
12
|
-
describe('Circular Layout', () => {
|
|
13
|
-
describe('Basic functionality', () => {
|
|
14
|
-
it('should position all nodes', () => {
|
|
15
|
-
const graph = completeGraph(6);
|
|
16
|
-
const positions = circularLayout(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 = circularLayout(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 = circularLayout(singleNode);
|
|
37
|
-
|
|
38
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
39
|
-
assert.deepEqual(positions['A'], [0, 0]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should handle disconnected components', () => {
|
|
43
|
-
const disconnected = {
|
|
44
|
-
nodes: () => ['A', 'B', 'C', 'D'],
|
|
45
|
-
edges: () => [['A', 'B'], ['C', 'D']]
|
|
46
|
-
};
|
|
47
|
-
const positions = circularLayout(disconnected);
|
|
48
|
-
|
|
49
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
50
|
-
['A', 'B', 'C', 'D'].forEach(node => {
|
|
51
|
-
assert.isDefined(positions[node]);
|
|
52
|
-
assert.equal(positions[node].length, 2);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('Circular arrangement properties', () => {
|
|
58
|
-
it('should arrange nodes in a circle with equal distances from center', () => {
|
|
59
|
-
const n = 8;
|
|
60
|
-
const graph = cycleGraph(n);
|
|
61
|
-
const positions = circularLayout(graph);
|
|
62
|
-
|
|
63
|
-
// Calculate distances from origin (default center)
|
|
64
|
-
const distances = graph.nodes().map(node => {
|
|
65
|
-
const [x, y] = positions[node];
|
|
66
|
-
return Math.sqrt(x * x + y * y);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// All distances should be equal
|
|
70
|
-
const firstDistance = distances[0];
|
|
71
|
-
distances.forEach(d => {
|
|
72
|
-
assert.approximately(d, firstDistance, 1e-10);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Distance should be 1 (default scale)
|
|
76
|
-
assert.approximately(firstDistance, 1, 1e-10);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should space nodes evenly around the circle', () => {
|
|
80
|
-
const n = 6;
|
|
81
|
-
const graph = completeGraph(n);
|
|
82
|
-
const positions = circularLayout(graph);
|
|
83
|
-
|
|
84
|
-
// Calculate angles from center
|
|
85
|
-
const angles = graph.nodes().map(node => {
|
|
86
|
-
const [x, y] = positions[node];
|
|
87
|
-
return Math.atan2(y, x);
|
|
88
|
-
}).sort((a, b) => a - b);
|
|
89
|
-
|
|
90
|
-
// Calculate angle differences
|
|
91
|
-
const angleDiffs: number[] = [];
|
|
92
|
-
for (let i = 0; i < n; i++) {
|
|
93
|
-
const diff = i < n - 1
|
|
94
|
-
? angles[i + 1] - angles[i]
|
|
95
|
-
: angles[0] + 2 * Math.PI - angles[i];
|
|
96
|
-
angleDiffs.push(diff);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// All angle differences should be equal (2π/n)
|
|
100
|
-
const expectedAngle = 2 * Math.PI / n;
|
|
101
|
-
angleDiffs.forEach(diff => {
|
|
102
|
-
assert.approximately(diff, expectedAngle, 1e-5);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should maintain circular arrangement for different graph types', () => {
|
|
107
|
-
const graphs = [
|
|
108
|
-
completeGraph(5),
|
|
109
|
-
cycleGraph(5),
|
|
110
|
-
starGraph(5),
|
|
111
|
-
wheelGraph(5)
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
graphs.forEach(graph => {
|
|
115
|
-
const positions = circularLayout(graph);
|
|
116
|
-
|
|
117
|
-
// Check all nodes are on a circle
|
|
118
|
-
const distances = graph.nodes().map(node => {
|
|
119
|
-
const [x, y] = positions[node];
|
|
120
|
-
return Math.sqrt(x * x + y * y);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
124
|
-
distances.forEach(d => {
|
|
125
|
-
assert.isBelow(Math.abs(d - avgDistance), 0.001);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('Parameter variations', () => {
|
|
132
|
-
it('should respect scale parameter', () => {
|
|
133
|
-
const graph = cycleGraph(4);
|
|
134
|
-
const scale1 = 1;
|
|
135
|
-
const scale2 = 2.5;
|
|
136
|
-
|
|
137
|
-
const positions1 = circularLayout(graph, scale1);
|
|
138
|
-
const positions2 = circularLayout(graph, scale2);
|
|
139
|
-
|
|
140
|
-
// Calculate radii
|
|
141
|
-
const radius1 = Math.sqrt(positions1[0][0] ** 2 + positions1[0][1] ** 2);
|
|
142
|
-
const radius2 = Math.sqrt(positions2[0][0] ** 2 + positions2[0][1] ** 2);
|
|
143
|
-
|
|
144
|
-
assert.approximately(radius2 / radius1, scale2 / scale1, 1e-5);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should respect center parameter', () => {
|
|
148
|
-
const graph = starGraph(5);
|
|
149
|
-
const center1 = [0, 0];
|
|
150
|
-
const center2 = [3, 4];
|
|
151
|
-
|
|
152
|
-
const positions1 = circularLayout(graph, 1, center1);
|
|
153
|
-
const positions2 = circularLayout(graph, 1, center2);
|
|
154
|
-
|
|
155
|
-
// Check that all positions are shifted by the center difference
|
|
156
|
-
graph.nodes().forEach(node => {
|
|
157
|
-
assert.approximately(positions2[node][0] - positions1[node][0], 3, 1e-10);
|
|
158
|
-
assert.approximately(positions2[node][1] - positions1[node][1], 4, 1e-10);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should handle different dimensions', () => {
|
|
163
|
-
const graph = completeGraph(4);
|
|
164
|
-
|
|
165
|
-
// 2D layout (default)
|
|
166
|
-
const positions2D = circularLayout(graph, 1, [0, 0], 2);
|
|
167
|
-
graph.nodes().forEach(node => {
|
|
168
|
-
assert.equal(positions2D[node].length, 2);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// 3D layout
|
|
172
|
-
const positions3D = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
173
|
-
graph.nodes().forEach(node => {
|
|
174
|
-
assert.equal(positions3D[node].length, 3);
|
|
175
|
-
// In 3D spherical layout, z-coordinates should vary
|
|
176
|
-
assert.isNumber(positions3D[node][2]);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should produce same layout for same inputs', () => {
|
|
181
|
-
const graph = randomGraph(10, 0.3, 12345);
|
|
182
|
-
|
|
183
|
-
const positions1 = circularLayout(graph, 1.5, [2, 3]);
|
|
184
|
-
const positions2 = circularLayout(graph, 1.5, [2, 3]);
|
|
185
|
-
|
|
186
|
-
// Circular layout is deterministic
|
|
187
|
-
graph.nodes().forEach(node => {
|
|
188
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('Special cases and edge cases', () => {
|
|
194
|
-
it('should handle graphs with string node IDs', () => {
|
|
195
|
-
const graph = {
|
|
196
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david'],
|
|
197
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const positions = circularLayout(graph);
|
|
201
|
-
|
|
202
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
203
|
-
['alice', 'bob', 'charlie', 'david'].forEach(node => {
|
|
204
|
-
assert.isDefined(positions[node]);
|
|
205
|
-
const [x, y] = positions[node];
|
|
206
|
-
const radius = Math.sqrt(x * x + y * y);
|
|
207
|
-
assert.approximately(radius, 1, 1e-10);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should handle large graphs efficiently', () => {
|
|
212
|
-
const n = 100;
|
|
213
|
-
const graph = cycleGraph(n);
|
|
214
|
-
|
|
215
|
-
const startTime = performance.now();
|
|
216
|
-
const positions = circularLayout(graph);
|
|
217
|
-
const endTime = performance.now();
|
|
218
|
-
|
|
219
|
-
assert.equal(Object.keys(positions).length, n);
|
|
220
|
-
assert.isBelow(endTime - startTime, 100); // Should complete quickly
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should produce visually pleasing layout for grid graphs', () => {
|
|
224
|
-
const graph = gridGraph(3, 3);
|
|
225
|
-
const positions = circularLayout(graph);
|
|
226
|
-
|
|
227
|
-
// All 9 nodes should be on a circle
|
|
228
|
-
assert.equal(Object.keys(positions).length, 9);
|
|
229
|
-
|
|
230
|
-
const distances = graph.nodes().map(node => {
|
|
231
|
-
const [x, y] = positions[node];
|
|
232
|
-
return Math.sqrt(x * x + y * y);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// All should be at same distance
|
|
236
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
237
|
-
distances.forEach(d => {
|
|
238
|
-
assert.isBelow(Math.abs(d - avgDistance), 0.001);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should handle negative scale gracefully', () => {
|
|
243
|
-
const graph = completeGraph(4);
|
|
244
|
-
const positions = circularLayout(graph, -1);
|
|
245
|
-
|
|
246
|
-
// Should still produce valid layout with absolute value of scale
|
|
247
|
-
graph.nodes().forEach(node => {
|
|
248
|
-
const [x, y] = positions[node];
|
|
249
|
-
const radius = Math.sqrt(x * x + y * y);
|
|
250
|
-
assert.approximately(radius, 1, 1e-10);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('Comparison with other layouts', () => {
|
|
256
|
-
it('should produce perfect circle unlike force-directed layouts', () => {
|
|
257
|
-
const graph = cycleGraph(8);
|
|
258
|
-
const positions = circularLayout(graph);
|
|
259
|
-
|
|
260
|
-
// Calculate variance in radii
|
|
261
|
-
const radii = graph.nodes().map(node => {
|
|
262
|
-
const [x, y] = positions[node];
|
|
263
|
-
return Math.sqrt(x * x + y * y);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const avgRadius = radii.reduce((a, b) => a + b) / radii.length;
|
|
267
|
-
const variance = radii.reduce((sum, r) => sum + Math.pow(r - avgRadius, 2), 0) / radii.length;
|
|
268
|
-
|
|
269
|
-
// Variance should be essentially zero for perfect circle
|
|
270
|
-
assert.isBelow(variance, 1e-20);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should maintain node order in layout', () => {
|
|
274
|
-
const nodes = [0, 1, 2, 3, 4, 5];
|
|
275
|
-
const graph = {
|
|
276
|
-
nodes: () => nodes,
|
|
277
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0]]
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const positions = circularLayout(graph);
|
|
281
|
-
|
|
282
|
-
// Calculate angles and check they increase monotonically
|
|
283
|
-
const angles = nodes.map(node => {
|
|
284
|
-
const [x, y] = positions[node];
|
|
285
|
-
let angle = Math.atan2(y, x);
|
|
286
|
-
if (angle < 0) angle += 2 * Math.PI;
|
|
287
|
-
return angle;
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// First node should be at angle 0 (positioned at (1, 0))
|
|
291
|
-
assert.approximately(positions[0][0], 1, 1e-10);
|
|
292
|
-
assert.approximately(positions[0][1], 0, 1e-10);
|
|
293
|
-
|
|
294
|
-
// Angles should increase in order
|
|
295
|
-
for (let i = 1; i < angles.length; i++) {
|
|
296
|
-
assert.isAbove(angles[i], angles[i - 1]);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe('3D spherical layout', () => {
|
|
302
|
-
it('should create spherical layout for dim=3', () => {
|
|
303
|
-
const graph = completeGraph(8);
|
|
304
|
-
const positions = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
305
|
-
|
|
306
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
307
|
-
graph.nodes().forEach(node => {
|
|
308
|
-
assert.isDefined(positions[node]);
|
|
309
|
-
assert.equal(positions[node].length, 3);
|
|
310
|
-
assert.isNumber(positions[node][0]);
|
|
311
|
-
assert.isNumber(positions[node][1]);
|
|
312
|
-
assert.isNumber(positions[node][2]);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('should place all nodes on unit sphere', () => {
|
|
317
|
-
const graph = cycleGraph(12);
|
|
318
|
-
const positions = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
319
|
-
|
|
320
|
-
graph.nodes().forEach(node => {
|
|
321
|
-
const [x, y, z] = positions[node];
|
|
322
|
-
const radius = Math.sqrt(x * x + y * y + z * z);
|
|
323
|
-
assert.approximately(radius, 1, 1e-10, `Node ${node} should be on unit sphere`);
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('should respect scale in 3D', () => {
|
|
328
|
-
const graph = starGraph(6);
|
|
329
|
-
const scale = 2.5;
|
|
330
|
-
const positions = circularLayout(graph, scale, [0, 0, 0], 3);
|
|
331
|
-
|
|
332
|
-
graph.nodes().forEach(node => {
|
|
333
|
-
const [x, y, z] = positions[node];
|
|
334
|
-
const radius = Math.sqrt(x * x + y * y + z * z);
|
|
335
|
-
assert.approximately(radius, scale, 1e-10);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('should respect center in 3D', () => {
|
|
340
|
-
const graph = completeGraph(4);
|
|
341
|
-
const center = [10, -5, 7];
|
|
342
|
-
const positions = circularLayout(graph, 1, center, 3);
|
|
343
|
-
|
|
344
|
-
graph.nodes().forEach(node => {
|
|
345
|
-
const [x, y, z] = positions[node];
|
|
346
|
-
const dx = x - center[0];
|
|
347
|
-
const dy = y - center[1];
|
|
348
|
-
const dz = z - center[2];
|
|
349
|
-
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
350
|
-
assert.approximately(radius, 1, 1e-10);
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should distribute nodes evenly on sphere using Fibonacci spiral', () => {
|
|
355
|
-
const graph = completeGraph(20);
|
|
356
|
-
const positions = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
357
|
-
|
|
358
|
-
// Check that no two nodes are too close together
|
|
359
|
-
const nodes = graph.nodes();
|
|
360
|
-
const minDistance = 0.3; // Reasonable minimum distance for 20 nodes on unit sphere
|
|
361
|
-
|
|
362
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
363
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
364
|
-
const pos1 = positions[nodes[i]];
|
|
365
|
-
const pos2 = positions[nodes[j]];
|
|
366
|
-
const dx = pos1[0] - pos2[0];
|
|
367
|
-
const dy = pos1[1] - pos2[1];
|
|
368
|
-
const dz = pos1[2] - pos2[2];
|
|
369
|
-
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
370
|
-
assert.isAbove(distance, minDistance, `Nodes ${i} and ${j} are too close`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('should handle single node in 3D', () => {
|
|
376
|
-
const singleNode = { nodes: () => ['A'], edges: () => [] };
|
|
377
|
-
const center = [1, 2, 3];
|
|
378
|
-
const positions = circularLayout(singleNode, 1, center, 3);
|
|
379
|
-
|
|
380
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
381
|
-
assert.deepEqual(positions['A'], center);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('should produce deterministic results in 3D', () => {
|
|
385
|
-
const graph = cycleGraph(10);
|
|
386
|
-
|
|
387
|
-
const positions1 = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
388
|
-
const positions2 = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
389
|
-
|
|
390
|
-
graph.nodes().forEach(node => {
|
|
391
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should handle higher dimensions gracefully', () => {
|
|
396
|
-
const graph = completeGraph(5);
|
|
397
|
-
const positions = circularLayout(graph, 1, [0, 0, 0, 0], 4);
|
|
398
|
-
|
|
399
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
400
|
-
graph.nodes().forEach(node => {
|
|
401
|
-
assert.equal(positions[node].length, 4);
|
|
402
|
-
// Should be on unit hypersphere
|
|
403
|
-
const radius = Math.sqrt(positions[node].reduce((sum, coord) => sum + coord * coord, 0));
|
|
404
|
-
assert.approximately(radius, 1, 0.1); // Allow some tolerance for random hypersphere
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it('should use all three dimensions meaningfully', () => {
|
|
409
|
-
const graph = completeGraph(30);
|
|
410
|
-
const positions = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
411
|
-
|
|
412
|
-
// Extract all z-coordinates
|
|
413
|
-
const zCoords = graph.nodes().map(node => positions[node][2]);
|
|
414
|
-
|
|
415
|
-
// Check that z-coordinates have meaningful variation
|
|
416
|
-
const minZ = Math.min(...zCoords);
|
|
417
|
-
const maxZ = Math.max(...zCoords);
|
|
418
|
-
const zRange = maxZ - minZ;
|
|
419
|
-
|
|
420
|
-
assert.isAbove(zRange, 1.5, 'Z-coordinates should have significant variation');
|
|
421
|
-
assert.approximately(maxZ, 1, 0.1, 'Max Z should be near 1');
|
|
422
|
-
assert.approximately(minZ, -1, 0.1, 'Min Z should be near -1');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('should produce valid 3D layout for string node IDs', () => {
|
|
426
|
-
const graph = {
|
|
427
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david', 'eve'],
|
|
428
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'],
|
|
429
|
-
['david', 'eve'], ['eve', 'alice']]
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
const positions = circularLayout(graph, 1, [0, 0, 0], 3);
|
|
433
|
-
|
|
434
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
435
|
-
graph.nodes().forEach(node => {
|
|
436
|
-
const [x, y, z] = positions[node];
|
|
437
|
-
const radius = Math.sqrt(x * x + y * y + z * z);
|
|
438
|
-
assert.approximately(radius, 1, 1e-10);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
});
|