@graphty/layout 1.2.3 → 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/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 +14 -1
- package/.env.example +0 -11
- package/.github/workflows/ci.yml +0 -101
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -52
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -1
- package/DEPLOYMENT.md +0 -59
- package/build-examples-script.js +0 -26
- 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 -23
- package/examples/layout.js +0 -2304
- 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,338 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
spiralLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Spiral Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes', () => {
|
|
14
|
-
const graph = completeGraph(10);
|
|
15
|
-
const positions = spiralLayout(graph);
|
|
16
|
-
|
|
17
|
-
assert.equal(Object.keys(positions).length, 10);
|
|
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 = spiralLayout(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 = spiralLayout(singleNode);
|
|
36
|
-
|
|
37
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
38
|
-
assert.isDefined(positions['A']);
|
|
39
|
-
assert.equal(positions['A'].length, 2);
|
|
40
|
-
// Single node should be at center
|
|
41
|
-
assert.isBelow(Math.abs(positions['A'][0]), 0.1);
|
|
42
|
-
assert.isBelow(Math.abs(positions['A'][1]), 0.1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle disconnected components', () => {
|
|
46
|
-
const disconnected = {
|
|
47
|
-
nodes: () => [1, 2, 3, 4, 5, 6],
|
|
48
|
-
edges: () => [[1, 2], [2, 3], [4, 5], [5, 6]]
|
|
49
|
-
};
|
|
50
|
-
const positions = spiralLayout(disconnected);
|
|
51
|
-
|
|
52
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
53
|
-
[1, 2, 3, 4, 5, 6].forEach(node => {
|
|
54
|
-
assert.isDefined(positions[node]);
|
|
55
|
-
assert.equal(positions[node].length, 2);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('Spiral properties', () => {
|
|
61
|
-
it('should arrange nodes in spiral pattern with increasing radius', () => {
|
|
62
|
-
const graph = cycleGraph(20);
|
|
63
|
-
const positions = spiralLayout(graph);
|
|
64
|
-
const nodes = graph.nodes();
|
|
65
|
-
|
|
66
|
-
// Calculate distances from center for sequential nodes
|
|
67
|
-
const distances = nodes.map(node => {
|
|
68
|
-
const [x, y] = positions[node];
|
|
69
|
-
return Math.sqrt(x * x + y * y);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Generally, later nodes should be further from center
|
|
73
|
-
let increasingTrend = 0;
|
|
74
|
-
for (let i = 1; i < distances.length; i++) {
|
|
75
|
-
if (distances[i] >= distances[i - 1]) {
|
|
76
|
-
increasingTrend++;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Most distances should increase (allowing for some spiral curvature)
|
|
81
|
-
assert.isAbove(increasingTrend / (distances.length - 1), 0.7);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should create smooth spiral curve', () => {
|
|
85
|
-
const graph = cycleGraph(30);
|
|
86
|
-
const positions = spiralLayout(graph);
|
|
87
|
-
const nodes = graph.nodes();
|
|
88
|
-
|
|
89
|
-
// Calculate angles for sequential nodes
|
|
90
|
-
const angles: number[] = [];
|
|
91
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
92
|
-
const [x, y] = positions[nodes[i]];
|
|
93
|
-
let angle = Math.atan2(y, x);
|
|
94
|
-
// Normalize to [0, 2π]
|
|
95
|
-
if (angle < 0) angle += 2 * Math.PI;
|
|
96
|
-
angles.push(angle);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Count angle wraparounds (full rotations)
|
|
100
|
-
let rotations = 0;
|
|
101
|
-
for (let i = 1; i < angles.length; i++) {
|
|
102
|
-
if (angles[i] < angles[i - 1] - Math.PI) {
|
|
103
|
-
rotations++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Should have at least one full rotation for 30 nodes
|
|
108
|
-
assert.isAtLeast(rotations, 1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should handle equidistant mode', () => {
|
|
112
|
-
const graph = starGraph(15);
|
|
113
|
-
const positions = spiralLayout(graph, 1, [0, 0], 2, true);
|
|
114
|
-
|
|
115
|
-
// Calculate distances between consecutive nodes
|
|
116
|
-
const nodes = graph.nodes();
|
|
117
|
-
const distances: number[] = [];
|
|
118
|
-
|
|
119
|
-
for (let i = 1; i < nodes.length; i++) {
|
|
120
|
-
const dx = positions[nodes[i]][0] - positions[nodes[i-1]][0];
|
|
121
|
-
const dy = positions[nodes[i]][1] - positions[nodes[i-1]][1];
|
|
122
|
-
distances.push(Math.sqrt(dx * dx + dy * dy));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// In equidistant mode, distances vary but should have some consistency
|
|
126
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
127
|
-
|
|
128
|
-
// Check that we have reasonable spacing
|
|
129
|
-
assert.isAbove(avgDistance, 0);
|
|
130
|
-
assert.isBelow(avgDistance, 1);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should handle non-equidistant mode', () => {
|
|
134
|
-
const graph = completeGraph(12);
|
|
135
|
-
const positions = spiralLayout(graph, 1, [0, 0], 2, 0.35, false);
|
|
136
|
-
|
|
137
|
-
// In non-equidistant mode, nodes spread with equal angle
|
|
138
|
-
const nodes = graph.nodes();
|
|
139
|
-
const angles = nodes.map(node => {
|
|
140
|
-
const [x, y] = positions[node];
|
|
141
|
-
return Math.atan2(y, x);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Check angle differences (accounting for spiral growth)
|
|
145
|
-
assert.equal(angles.length, 12);
|
|
146
|
-
// Angles should show regular progression
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('Parameter variations', () => {
|
|
151
|
-
it('should respect scale parameter', () => {
|
|
152
|
-
const graph = cycleGraph(8);
|
|
153
|
-
|
|
154
|
-
const positions1 = spiralLayout(graph, 0.5);
|
|
155
|
-
const positions2 = spiralLayout(graph, 2.0);
|
|
156
|
-
|
|
157
|
-
// Calculate max distances
|
|
158
|
-
const maxDist1 = Math.max(...graph.nodes().map(node => {
|
|
159
|
-
const [x, y] = positions1[node];
|
|
160
|
-
return Math.sqrt(x * x + y * y);
|
|
161
|
-
}));
|
|
162
|
-
|
|
163
|
-
const maxDist2 = Math.max(...graph.nodes().map(node => {
|
|
164
|
-
const [x, y] = positions2[node];
|
|
165
|
-
return Math.sqrt(x * x + y * y);
|
|
166
|
-
}));
|
|
167
|
-
|
|
168
|
-
// Scale 2 should produce larger spiral
|
|
169
|
-
assert.approximately(maxDist2 / maxDist1, 4, 0.1); // Scale factor squared
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should respect center parameter', () => {
|
|
173
|
-
const graph = starGraph(7);
|
|
174
|
-
const center = [3, -2];
|
|
175
|
-
|
|
176
|
-
const positions = spiralLayout(graph, 1, center);
|
|
177
|
-
|
|
178
|
-
// Check that positions are shifted by center
|
|
179
|
-
const defaultPositions = spiralLayout(graph, 1, [0, 0]);
|
|
180
|
-
|
|
181
|
-
// Check center of mass is shifted
|
|
182
|
-
const centerX = graph.nodes().reduce((sum, node) => sum + positions[node][0], 0) / graph.nodes().length;
|
|
183
|
-
const centerY = graph.nodes().reduce((sum, node) => sum + positions[node][1], 0) / graph.nodes().length;
|
|
184
|
-
const defaultCenterX = graph.nodes().reduce((sum, node) => sum + defaultPositions[node][0], 0) / graph.nodes().length;
|
|
185
|
-
const defaultCenterY = graph.nodes().reduce((sum, node) => sum + defaultPositions[node][1], 0) / graph.nodes().length;
|
|
186
|
-
|
|
187
|
-
// Centers should be shifted by approximately the center parameter
|
|
188
|
-
assert.approximately(centerX - defaultCenterX, center[0], 0.5);
|
|
189
|
-
assert.approximately(centerY - defaultCenterY, center[1], 0.5);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should only support 2D layouts', () => {
|
|
193
|
-
const graph = completeGraph(5);
|
|
194
|
-
|
|
195
|
-
// 2D layout works
|
|
196
|
-
const positions2D = spiralLayout(graph, 1, [0, 0], 2);
|
|
197
|
-
graph.nodes().forEach(node => {
|
|
198
|
-
assert.equal(positions2D[node].length, 2);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// 3D layout should throw error
|
|
202
|
-
assert.throws(() => {
|
|
203
|
-
spiralLayout(graph, 1, [0, 0, 0], 3);
|
|
204
|
-
}, 'can only handle 2 dimensions');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should respect resolution parameter', () => {
|
|
208
|
-
const graph = gridGraph(3, 3);
|
|
209
|
-
|
|
210
|
-
const positions1 = spiralLayout(graph, 1, [0, 0], 2, 0.1, true);
|
|
211
|
-
const positions2 = spiralLayout(graph, 1, [0, 0], 2, 1.0, true);
|
|
212
|
-
|
|
213
|
-
// Different resolutions should produce different spirals
|
|
214
|
-
let different = false;
|
|
215
|
-
graph.nodes().forEach(node => {
|
|
216
|
-
if (Math.abs(positions1[node][0] - positions2[node][0]) > 0.01 ||
|
|
217
|
-
Math.abs(positions1[node][1] - positions2[node][1]) > 0.01) {
|
|
218
|
-
different = true;
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
assert.isTrue(different);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('Special cases and edge cases', () => {
|
|
226
|
-
it('should handle string node IDs', () => {
|
|
227
|
-
const graph = {
|
|
228
|
-
nodes: () => ['first', 'second', 'third', 'fourth', 'fifth'],
|
|
229
|
-
edges: () => [['first', 'second'], ['second', 'third'], ['third', 'fourth'], ['fourth', 'fifth']]
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const positions = spiralLayout(graph);
|
|
233
|
-
|
|
234
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
235
|
-
['first', 'second', 'third', 'fourth', 'fifth'].forEach(node => {
|
|
236
|
-
assert.isDefined(positions[node]);
|
|
237
|
-
assert.equal(positions[node].length, 2);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should produce consistent results', () => {
|
|
242
|
-
const graph = randomGraph(15, 0.2, 12345);
|
|
243
|
-
|
|
244
|
-
const positions1 = spiralLayout(graph, 1, [0, 0], 2, true, 0.5);
|
|
245
|
-
const positions2 = spiralLayout(graph, 1, [0, 0], 2, true, 0.5);
|
|
246
|
-
|
|
247
|
-
// Spiral layout is deterministic
|
|
248
|
-
graph.nodes().forEach(node => {
|
|
249
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should handle large graphs efficiently', () => {
|
|
254
|
-
const graph = gridGraph(20, 20); // 400 nodes
|
|
255
|
-
|
|
256
|
-
const startTime = performance.now();
|
|
257
|
-
const positions = spiralLayout(graph);
|
|
258
|
-
const endTime = performance.now();
|
|
259
|
-
|
|
260
|
-
assert.equal(Object.keys(positions).length, 400);
|
|
261
|
-
assert.isBelow(endTime - startTime, 100);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should create visually distinct layout from circular', () => {
|
|
265
|
-
const graph = cycleGraph(20);
|
|
266
|
-
const spiralPos = spiralLayout(graph);
|
|
267
|
-
|
|
268
|
-
// In spiral, consecutive nodes should have varying distances from center
|
|
269
|
-
const distances = graph.nodes().map(node => {
|
|
270
|
-
const [x, y] = spiralPos[node];
|
|
271
|
-
return Math.sqrt(x * x + y * y);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Calculate variance in distances
|
|
275
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
276
|
-
const variance = distances.reduce((sum, d) => sum + Math.pow(d - avgDist, 2), 0) / distances.length;
|
|
277
|
-
|
|
278
|
-
// Spiral should have some variance (unlike perfect circle)
|
|
279
|
-
assert.isAbove(variance, 0.001);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('Layout patterns', () => {
|
|
284
|
-
it('should create tight spiral for dense graphs', () => {
|
|
285
|
-
const graph = completeGraph(25);
|
|
286
|
-
const positions = spiralLayout(graph, 1, [0, 0], 2, 0.35, true);
|
|
287
|
-
|
|
288
|
-
// Check that nodes fill space efficiently
|
|
289
|
-
const boundingBox = {
|
|
290
|
-
minX: Math.min(...graph.nodes().map(n => positions[n][0])),
|
|
291
|
-
maxX: Math.max(...graph.nodes().map(n => positions[n][0])),
|
|
292
|
-
minY: Math.min(...graph.nodes().map(n => positions[n][1])),
|
|
293
|
-
maxY: Math.max(...graph.nodes().map(n => positions[n][1]))
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const width = boundingBox.maxX - boundingBox.minX;
|
|
297
|
-
const height = boundingBox.maxY - boundingBox.minY;
|
|
298
|
-
|
|
299
|
-
// Spiral should be roughly square (not too elongated)
|
|
300
|
-
const aspectRatio = Math.max(width, height) / Math.min(width, height);
|
|
301
|
-
assert.isBelow(aspectRatio, 2);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('should create logarithmic spiral pattern', () => {
|
|
305
|
-
const n = 50;
|
|
306
|
-
const graph = cycleGraph(n);
|
|
307
|
-
const positions = spiralLayout(graph, 1, [0, 0], 2, 0.35, false);
|
|
308
|
-
|
|
309
|
-
// For non-equidistant spiral, radius should grow exponentially
|
|
310
|
-
const radii = graph.nodes().map(node => {
|
|
311
|
-
const [x, y] = positions[node];
|
|
312
|
-
return Math.sqrt(x * x + y * y);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Filter out near-zero radii
|
|
316
|
-
const nonZeroRadii = radii.filter(r => r > 0.01);
|
|
317
|
-
|
|
318
|
-
// Check if growth is approximately exponential
|
|
319
|
-
// by checking if log(radii) grows linearly
|
|
320
|
-
if (nonZeroRadii.length > 10) {
|
|
321
|
-
const logRadii = nonZeroRadii.map(r => Math.log(r));
|
|
322
|
-
|
|
323
|
-
// Simple linear regression
|
|
324
|
-
const n = logRadii.length;
|
|
325
|
-
const indices = Array.from({length: n}, (_, i) => i);
|
|
326
|
-
const sumX = indices.reduce((a, b) => a + b);
|
|
327
|
-
const sumY = logRadii.reduce((a, b) => a + b);
|
|
328
|
-
const sumXY = indices.reduce((sum, x, i) => sum + x * logRadii[i], 0);
|
|
329
|
-
const sumX2 = indices.reduce((sum, x) => sum + x * x, 0);
|
|
330
|
-
|
|
331
|
-
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
332
|
-
|
|
333
|
-
// Positive slope indicates exponential growth
|
|
334
|
-
assert.isAbove(slope, 0);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
springLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
randomGraph
|
|
8
|
-
} from '../src';
|
|
9
|
-
|
|
10
|
-
describe('Spring Layout', () => {
|
|
11
|
-
describe('Basic functionality', () => {
|
|
12
|
-
it('should position all nodes', () => {
|
|
13
|
-
const graph = completeGraph(5);
|
|
14
|
-
const positions = springLayout(graph);
|
|
15
|
-
|
|
16
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
17
|
-
graph.nodes().forEach(node => {
|
|
18
|
-
assert.isDefined(positions[node]);
|
|
19
|
-
assert.equal(positions[node].length, 2);
|
|
20
|
-
assert.isNumber(positions[node][0]);
|
|
21
|
-
assert.isNumber(positions[node][1]);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should handle empty graph', () => {
|
|
26
|
-
const emptyGraph = { nodes: () => [], edges: () => [] };
|
|
27
|
-
const positions = springLayout(emptyGraph);
|
|
28
|
-
|
|
29
|
-
assert.equal(Object.keys(positions).length, 0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should handle single node', () => {
|
|
33
|
-
const singleNode = { nodes: () => [0], edges: () => [] };
|
|
34
|
-
const positions = springLayout(singleNode);
|
|
35
|
-
|
|
36
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
37
|
-
assert.deepEqual(positions[0], [0, 0]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should handle disconnected components', () => {
|
|
41
|
-
const disconnected = {
|
|
42
|
-
nodes: () => [0, 1, 2, 3],
|
|
43
|
-
edges: () => [[0, 1], [2, 3]] // Two separate components
|
|
44
|
-
};
|
|
45
|
-
const positions = springLayout(disconnected);
|
|
46
|
-
|
|
47
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
48
|
-
// All nodes should have positions
|
|
49
|
-
[0, 1, 2, 3].forEach(node => {
|
|
50
|
-
assert.isDefined(positions[node]);
|
|
51
|
-
assert.equal(positions[node].length, 2);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('Parameter variations', () => {
|
|
57
|
-
it('should handle different k values', () => {
|
|
58
|
-
const graph = completeGraph(6);
|
|
59
|
-
const positions1 = springLayout(graph, 0.5);
|
|
60
|
-
const positions2 = springLayout(graph, 2.0);
|
|
61
|
-
|
|
62
|
-
// Different k values should produce different layouts
|
|
63
|
-
// Check that at least some positions differ
|
|
64
|
-
let different = false;
|
|
65
|
-
graph.nodes().forEach(node => {
|
|
66
|
-
if (positions1[node][0] !== positions2[node][0] ||
|
|
67
|
-
positions1[node][1] !== positions2[node][1]) {
|
|
68
|
-
different = true;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
assert.isTrue(different);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should respect fixed positions', () => {
|
|
75
|
-
const graph = starGraph(5);
|
|
76
|
-
const initialPositions = {
|
|
77
|
-
0: [0, 0],
|
|
78
|
-
1: [1, 0],
|
|
79
|
-
2: [0.5, 0.5],
|
|
80
|
-
3: [-0.5, 0.5],
|
|
81
|
-
4: [0, -1]
|
|
82
|
-
};
|
|
83
|
-
const fixedNodes = [0, 1]; // Fix center and one leaf
|
|
84
|
-
|
|
85
|
-
const positions = springLayout(graph, null, initialPositions, fixedNodes);
|
|
86
|
-
|
|
87
|
-
// Fixed nodes should remain at their initial positions
|
|
88
|
-
assert.deepEqual(positions[0], [0, 0]);
|
|
89
|
-
assert.deepEqual(positions[1], [1, 0]);
|
|
90
|
-
|
|
91
|
-
// Other nodes should have moved from initial positions
|
|
92
|
-
[2, 3, 4].forEach(node => {
|
|
93
|
-
assert.isDefined(positions[node]);
|
|
94
|
-
assert.equal(positions[node].length, 2);
|
|
95
|
-
// They should have moved from initial positions
|
|
96
|
-
assert.notEqual(positions[node][0], initialPositions[node][0]);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should converge with more iterations', () => {
|
|
101
|
-
const graph = randomGraph(10, 0.3, 42);
|
|
102
|
-
|
|
103
|
-
// Run with different iteration counts
|
|
104
|
-
const positions10 = springLayout(graph, null, null, null, 10);
|
|
105
|
-
const positions100 = springLayout(graph, null, null, null, 100);
|
|
106
|
-
|
|
107
|
-
// Calculate total movement between iterations
|
|
108
|
-
const movement10to100 = graph.nodes().reduce((sum, node) => {
|
|
109
|
-
const dx = positions100[node][0] - positions10[node][0];
|
|
110
|
-
const dy = positions100[node][1] - positions10[node][1];
|
|
111
|
-
return sum + Math.sqrt(dx * dx + dy * dy);
|
|
112
|
-
}, 0);
|
|
113
|
-
|
|
114
|
-
// More iterations should lead to more stable layout
|
|
115
|
-
// (though initial random positions may vary)
|
|
116
|
-
assert.isAbove(movement10to100, 0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should handle different center positions', () => {
|
|
120
|
-
const graph = cycleGraph(4);
|
|
121
|
-
const center1 = [0, 0];
|
|
122
|
-
const center2 = [10, 10];
|
|
123
|
-
|
|
124
|
-
const positions1 = springLayout(graph, null, null, null, 50, 0.01, center1);
|
|
125
|
-
const positions2 = springLayout(graph, null, null, null, 50, 0.01, center2);
|
|
126
|
-
|
|
127
|
-
// Calculate center of mass for each layout
|
|
128
|
-
const com1 = graph.nodes().reduce((acc, node) => {
|
|
129
|
-
return [acc[0] + positions1[node][0], acc[1] + positions1[node][1]];
|
|
130
|
-
}, [0, 0]).map(v => v / 4);
|
|
131
|
-
|
|
132
|
-
const com2 = graph.nodes().reduce((acc, node) => {
|
|
133
|
-
return [acc[0] + positions2[node][0], acc[1] + positions2[node][1]];
|
|
134
|
-
}, [0, 0]).map(v => v / 4);
|
|
135
|
-
|
|
136
|
-
// Centers of mass should be different
|
|
137
|
-
assert.isAbove(Math.abs(com2[0] - com1[0]), 5);
|
|
138
|
-
assert.isAbove(Math.abs(com2[1] - com1[1]), 5);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('Graph-specific behaviors', () => {
|
|
143
|
-
it('should arrange complete graph symmetrically', () => {
|
|
144
|
-
const graph = completeGraph(4);
|
|
145
|
-
const positions = springLayout(graph, 1, null, null, 200);
|
|
146
|
-
|
|
147
|
-
// In a complete graph, all nodes should be roughly equidistant
|
|
148
|
-
const distances: number[] = [];
|
|
149
|
-
const nodes = graph.nodes();
|
|
150
|
-
|
|
151
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
152
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
153
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
154
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
155
|
-
distances.push(Math.sqrt(dx * dx + dy * dy));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Calculate variance in distances
|
|
160
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
161
|
-
const variance = distances.reduce((sum, d) => sum + Math.pow(d - avgDistance, 2), 0) / distances.length;
|
|
162
|
-
|
|
163
|
-
// Variance should be small for symmetric layout
|
|
164
|
-
assert.isBelow(variance / (avgDistance * avgDistance), 0.1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should stretch cycle graph into circle-like shape', () => {
|
|
168
|
-
const n = 8;
|
|
169
|
-
const graph = cycleGraph(n);
|
|
170
|
-
const positions = springLayout(graph, 1, null, null, 200);
|
|
171
|
-
|
|
172
|
-
// Calculate center of mass
|
|
173
|
-
const center = graph.nodes().reduce((acc, node) => {
|
|
174
|
-
return [acc[0] + positions[node][0], acc[1] + positions[node][1]];
|
|
175
|
-
}, [0, 0]).map(v => v / n);
|
|
176
|
-
|
|
177
|
-
// Calculate distances from center
|
|
178
|
-
const distances = graph.nodes().map(node => {
|
|
179
|
-
const dx = positions[node][0] - center[0];
|
|
180
|
-
const dy = positions[node][1] - center[1];
|
|
181
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// All nodes should be roughly equidistant from center
|
|
185
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
186
|
-
distances.forEach(d => {
|
|
187
|
-
assert.isBelow(Math.abs(d - avgDistance) / avgDistance, 0.6);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should position star graph with center near origin', () => {
|
|
192
|
-
const graph = starGraph(7);
|
|
193
|
-
const positions = springLayout(graph, 1, null, null, 100);
|
|
194
|
-
|
|
195
|
-
// Center node (0) should be near the center
|
|
196
|
-
const centerDist = Math.sqrt(positions[0][0] ** 2 + positions[0][1] ** 2);
|
|
197
|
-
assert.isBelow(centerDist, 0.2);
|
|
198
|
-
|
|
199
|
-
// Leaf nodes should be further from origin
|
|
200
|
-
for (let i = 1; i < 7; i++) {
|
|
201
|
-
const leafDist = Math.sqrt(positions[i][0] ** 2 + positions[i][1] ** 2);
|
|
202
|
-
assert.isAbove(leafDist, 0.5);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('Determinism and reproducibility', () => {
|
|
208
|
-
it('should produce consistent results with same seed', () => {
|
|
209
|
-
const graph = randomGraph(8, 0.4, 12345);
|
|
210
|
-
const seed = 42;
|
|
211
|
-
|
|
212
|
-
// Run layout twice with same parameters and seed
|
|
213
|
-
const positions1 = springLayout(graph, 1, null, null, 50, 1, [0, 0], 2, seed);
|
|
214
|
-
const positions2 = springLayout(graph, 1, null, null, 50, 1, [0, 0], 2, seed);
|
|
215
|
-
|
|
216
|
-
// Results should be identical with same seed
|
|
217
|
-
graph.nodes().forEach(node => {
|
|
218
|
-
assert.equal(positions1[node][0], positions2[node][0]);
|
|
219
|
-
assert.equal(positions1[node][1], positions2[node][1]);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should produce different results with different seeds', () => {
|
|
224
|
-
const graph = randomGraph(8, 0.4, 12345);
|
|
225
|
-
|
|
226
|
-
// Run layout with different seeds
|
|
227
|
-
const positions1 = springLayout(graph, 1, null, null, 50, 1, [0, 0], 2, 42);
|
|
228
|
-
const positions2 = springLayout(graph, 1, null, null, 50, 1, [0, 0], 2, 123);
|
|
229
|
-
|
|
230
|
-
// Results should be different
|
|
231
|
-
let different = false;
|
|
232
|
-
graph.nodes().forEach(node => {
|
|
233
|
-
if (positions1[node][0] !== positions2[node][0] ||
|
|
234
|
-
positions1[node][1] !== positions2[node][1]) {
|
|
235
|
-
different = true;
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
assert.isTrue(different);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|
package/test/test-utils.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test utilities and helper functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Graph, Node, Edge } from '../src/types';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a simple test graph for testing
|
|
9
|
-
*/
|
|
10
|
-
export function createTestGraph(nodes: Node[], edges: Edge[]): Graph {
|
|
11
|
-
return {
|
|
12
|
-
nodes: () => nodes,
|
|
13
|
-
edges: () => edges,
|
|
14
|
-
adjacency: function() {
|
|
15
|
-
const adj = new Map<Node, Map<Node, number>>();
|
|
16
|
-
|
|
17
|
-
// Initialize adjacency for all nodes
|
|
18
|
-
for (const node of nodes) {
|
|
19
|
-
adj.set(node, new Map());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Add edges
|
|
23
|
-
for (const [source, target] of edges) {
|
|
24
|
-
if (!adj.has(source)) adj.set(source, new Map());
|
|
25
|
-
if (!adj.has(target)) adj.set(target, new Map());
|
|
26
|
-
|
|
27
|
-
adj.get(source)!.set(target, 1);
|
|
28
|
-
adj.get(target)!.set(source, 1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return adj;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|