@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,347 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
shellLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
wheelGraph,
|
|
8
|
-
gridGraph,
|
|
9
|
-
randomGraph
|
|
10
|
-
} from '../src';
|
|
11
|
-
|
|
12
|
-
describe('Shell Layout', () => {
|
|
13
|
-
describe('Basic functionality', () => {
|
|
14
|
-
it('should position all nodes', () => {
|
|
15
|
-
const graph = completeGraph(8);
|
|
16
|
-
const positions = shellLayout(graph);
|
|
17
|
-
|
|
18
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
19
|
-
graph.nodes().forEach(node => {
|
|
20
|
-
assert.isDefined(positions[node]);
|
|
21
|
-
assert.equal(positions[node].length, 2);
|
|
22
|
-
assert.isNumber(positions[node][0]);
|
|
23
|
-
assert.isNumber(positions[node][1]);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should handle empty graph', () => {
|
|
28
|
-
const emptyGraph = { nodes: () => [], edges: () => [] };
|
|
29
|
-
const positions = shellLayout(emptyGraph);
|
|
30
|
-
|
|
31
|
-
assert.equal(Object.keys(positions).length, 0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should handle single node', () => {
|
|
35
|
-
const singleNode = { nodes: () => ['node1'], edges: () => [] };
|
|
36
|
-
const positions = shellLayout(singleNode);
|
|
37
|
-
|
|
38
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
39
|
-
assert.deepEqual(positions['node1'], [0, 0]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should default to single shell when no shells specified', () => {
|
|
43
|
-
const graph = cycleGraph(6);
|
|
44
|
-
const positions = shellLayout(graph);
|
|
45
|
-
|
|
46
|
-
// All nodes should be on a circle (single shell)
|
|
47
|
-
const distances = graph.nodes().map(node => {
|
|
48
|
-
const [x, y] = positions[node];
|
|
49
|
-
return Math.sqrt(x * x + y * y);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const firstDist = distances[0];
|
|
53
|
-
distances.forEach(d => {
|
|
54
|
-
assert.approximately(d, firstDist, 1e-10);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('Multi-shell arrangements', () => {
|
|
60
|
-
it('should arrange nodes in specified shells', () => {
|
|
61
|
-
const graph = completeGraph(9);
|
|
62
|
-
const shells = [
|
|
63
|
-
[0], // Center node
|
|
64
|
-
[1, 2, 3], // Inner shell
|
|
65
|
-
[4, 5, 6, 7, 8] // Outer shell
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const positions = shellLayout(graph, shells);
|
|
69
|
-
|
|
70
|
-
// Center node at origin
|
|
71
|
-
assert.deepEqual(positions[0], [0, 0]);
|
|
72
|
-
|
|
73
|
-
// Inner shell at same radius
|
|
74
|
-
const innerRadii = [1, 2, 3].map(node => {
|
|
75
|
-
const [x, y] = positions[node];
|
|
76
|
-
return Math.sqrt(x * x + y * y);
|
|
77
|
-
});
|
|
78
|
-
const innerRadius = innerRadii[0];
|
|
79
|
-
innerRadii.forEach(r => {
|
|
80
|
-
assert.approximately(r, innerRadius, 1e-10);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Outer shell at larger radius
|
|
84
|
-
const outerRadii = [4, 5, 6, 7, 8].map(node => {
|
|
85
|
-
const [x, y] = positions[node];
|
|
86
|
-
return Math.sqrt(x * x + y * y);
|
|
87
|
-
});
|
|
88
|
-
const outerRadius = outerRadii[0];
|
|
89
|
-
outerRadii.forEach(r => {
|
|
90
|
-
assert.approximately(r, outerRadius, 1e-10);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Outer radius should be larger than inner
|
|
94
|
-
assert.isAbove(outerRadius, innerRadius);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should space shells evenly', () => {
|
|
98
|
-
const graph = starGraph(13); // 1 center + 12 leaves
|
|
99
|
-
const shells = [
|
|
100
|
-
[0], // Center
|
|
101
|
-
[1, 2, 3, 4], // Shell 1
|
|
102
|
-
[5, 6, 7, 8], // Shell 2
|
|
103
|
-
[9, 10, 11, 12] // Shell 3
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
const positions = shellLayout(graph, shells);
|
|
107
|
-
|
|
108
|
-
// Calculate radii for each shell
|
|
109
|
-
const radii = shells.map((shell, idx) => {
|
|
110
|
-
if (idx === 0) return 0; // Center
|
|
111
|
-
const [x, y] = positions[shell[0]];
|
|
112
|
-
return Math.sqrt(x * x + y * y);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Check spacing between consecutive shells
|
|
116
|
-
const spacing1 = radii[2] - radii[1];
|
|
117
|
-
const spacing2 = radii[3] - radii[2];
|
|
118
|
-
|
|
119
|
-
assert.approximately(spacing1, spacing2, 1e-5);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should arrange nodes evenly within each shell', () => {
|
|
123
|
-
const graph = wheelGraph(9); // 1 center + 8 rim
|
|
124
|
-
const shells = [
|
|
125
|
-
[0],
|
|
126
|
-
[1, 2, 3, 4, 5, 6, 7, 8]
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
const positions = shellLayout(graph, shells);
|
|
130
|
-
|
|
131
|
-
// Calculate angles for outer shell nodes
|
|
132
|
-
const angles = shells[1].map(node => {
|
|
133
|
-
const [x, y] = positions[node];
|
|
134
|
-
return Math.atan2(y, x);
|
|
135
|
-
}).sort((a, b) => a - b);
|
|
136
|
-
|
|
137
|
-
// Check angle differences
|
|
138
|
-
const expectedAngle = 2 * Math.PI / 8;
|
|
139
|
-
for (let i = 0; i < angles.length; i++) {
|
|
140
|
-
const diff = i < angles.length - 1
|
|
141
|
-
? angles[i + 1] - angles[i]
|
|
142
|
-
: angles[0] + 2 * Math.PI - angles[i];
|
|
143
|
-
assert.approximately(diff, expectedAngle, 1e-5);
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('Parameter variations', () => {
|
|
149
|
-
it('should respect scale parameter', () => {
|
|
150
|
-
const graph = completeGraph(7);
|
|
151
|
-
const shells = [[0, 1, 2], [3, 4, 5, 6]];
|
|
152
|
-
|
|
153
|
-
const positions1 = shellLayout(graph, shells, 1);
|
|
154
|
-
const positions2 = shellLayout(graph, shells, 2);
|
|
155
|
-
|
|
156
|
-
// All distances should be scaled
|
|
157
|
-
shells[1].forEach(node => {
|
|
158
|
-
const r1 = Math.sqrt(positions1[node][0] ** 2 + positions1[node][1] ** 2);
|
|
159
|
-
const r2 = Math.sqrt(positions2[node][0] ** 2 + positions2[node][1] ** 2);
|
|
160
|
-
assert.approximately(r2 / r1, 2, 1e-5);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should respect center parameter', () => {
|
|
165
|
-
const graph = cycleGraph(5);
|
|
166
|
-
const shells = [[0, 1, 2, 3, 4]];
|
|
167
|
-
const center = [5, -3];
|
|
168
|
-
|
|
169
|
-
const positions = shellLayout(graph, shells, 1, center);
|
|
170
|
-
|
|
171
|
-
// Calculate center of mass
|
|
172
|
-
const com = graph.nodes().reduce((acc, node) => {
|
|
173
|
-
return [acc[0] + positions[node][0], acc[1] + positions[node][1]];
|
|
174
|
-
}, [0, 0]).map(v => v / 5);
|
|
175
|
-
|
|
176
|
-
assert.approximately(com[0], center[0], 1e-10);
|
|
177
|
-
assert.approximately(com[1], center[1], 1e-10);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should only support 2D layouts', () => {
|
|
181
|
-
const graph = completeGraph(6);
|
|
182
|
-
const shells = [[0, 1], [2, 3, 4, 5]];
|
|
183
|
-
|
|
184
|
-
// 2D layout works
|
|
185
|
-
const positions2D = shellLayout(graph, shells, 1, [0, 0], 2);
|
|
186
|
-
assert.equal(Object.keys(positions2D).length, 6);
|
|
187
|
-
|
|
188
|
-
// 3D layout should throw error
|
|
189
|
-
assert.throws(() => {
|
|
190
|
-
shellLayout(graph, shells, 1, [0, 0, 0], 3);
|
|
191
|
-
}, 'can only handle 2 dimensions');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should handle empty shells gracefully', () => {
|
|
195
|
-
const graph = cycleGraph(5);
|
|
196
|
-
const shells = [
|
|
197
|
-
[0, 1],
|
|
198
|
-
[], // Empty shell
|
|
199
|
-
[2, 3, 4]
|
|
200
|
-
];
|
|
201
|
-
|
|
202
|
-
const positions = shellLayout(graph, shells);
|
|
203
|
-
|
|
204
|
-
// Should still position all nodes
|
|
205
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
206
|
-
|
|
207
|
-
// First and last shell should have different radii
|
|
208
|
-
const r1 = Math.sqrt(positions[0][0] ** 2 + positions[0][1] ** 2);
|
|
209
|
-
const r3 = Math.sqrt(positions[2][0] ** 2 + positions[2][1] ** 2);
|
|
210
|
-
assert.isAbove(r3, r1);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('Special cases and edge cases', () => {
|
|
215
|
-
it('should handle string node IDs', () => {
|
|
216
|
-
const graph = {
|
|
217
|
-
nodes: () => ['center', 'a', 'b', 'c', 'd'],
|
|
218
|
-
edges: () => [['center', 'a'], ['center', 'b'], ['center', 'c'], ['center', 'd']]
|
|
219
|
-
};
|
|
220
|
-
const shells = [['center'], ['a', 'b', 'c', 'd']];
|
|
221
|
-
|
|
222
|
-
const positions = shellLayout(graph, shells);
|
|
223
|
-
|
|
224
|
-
assert.deepEqual(positions['center'], [0, 0]);
|
|
225
|
-
['a', 'b', 'c', 'd'].forEach(node => {
|
|
226
|
-
const [x, y] = positions[node];
|
|
227
|
-
const r = Math.sqrt(x * x + y * y);
|
|
228
|
-
assert.isAbove(r, 0);
|
|
229
|
-
assert.approximately(r, 0.5, 1e-10); // Second shell at radius 0.5 (scale=1, 2 shells)
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should handle nodes not in any shell', () => {
|
|
234
|
-
const graph = completeGraph(6);
|
|
235
|
-
const shells = [[0, 1], [2, 3]]; // Nodes 4, 5 not included
|
|
236
|
-
|
|
237
|
-
const positions = shellLayout(graph, shells);
|
|
238
|
-
|
|
239
|
-
// Only nodes in shells should be positioned
|
|
240
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
241
|
-
|
|
242
|
-
// Nodes not in shells won't be positioned
|
|
243
|
-
assert.isUndefined(positions[4]);
|
|
244
|
-
assert.isUndefined(positions[5]);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should handle duplicate nodes in shells', () => {
|
|
248
|
-
const graph = cycleGraph(4);
|
|
249
|
-
const shells = [[0, 1], [1, 2, 3]]; // Node 1 in two shells
|
|
250
|
-
|
|
251
|
-
const positions = shellLayout(graph, shells);
|
|
252
|
-
|
|
253
|
-
// Should handle gracefully
|
|
254
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
255
|
-
|
|
256
|
-
// Check node positions
|
|
257
|
-
const r0 = Math.sqrt(positions[0][0] ** 2 + positions[0][1] ** 2);
|
|
258
|
-
const r1 = Math.sqrt(positions[1][0] ** 2 + positions[1][1] ** 2);
|
|
259
|
-
const r2 = Math.sqrt(positions[2][0] ** 2 + positions[2][1] ** 2);
|
|
260
|
-
const r3 = Math.sqrt(positions[3][0] ** 2 + positions[3][1] ** 2);
|
|
261
|
-
|
|
262
|
-
// Node 0 in first shell at radius 0.5
|
|
263
|
-
assert.approximately(r0, 0.5, 1e-10);
|
|
264
|
-
|
|
265
|
-
// Nodes 1, 2, 3 in second shell at radius 1.0
|
|
266
|
-
// (Node 1 appears in second shell, not first)
|
|
267
|
-
assert.approximately(r1, 1.0, 1e-10);
|
|
268
|
-
assert.approximately(r2, 1.0, 1e-10);
|
|
269
|
-
assert.approximately(r3, 1.0, 1e-10);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should produce consistent results', () => {
|
|
273
|
-
const graph = randomGraph(10, 0.3, 42);
|
|
274
|
-
const shells = [
|
|
275
|
-
graph.nodes().slice(0, 3),
|
|
276
|
-
graph.nodes().slice(3, 7),
|
|
277
|
-
graph.nodes().slice(7)
|
|
278
|
-
];
|
|
279
|
-
|
|
280
|
-
const positions1 = shellLayout(graph, shells);
|
|
281
|
-
const positions2 = shellLayout(graph, shells);
|
|
282
|
-
|
|
283
|
-
// Shell layout is deterministic
|
|
284
|
-
graph.nodes().forEach(node => {
|
|
285
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should handle very large shells efficiently', () => {
|
|
290
|
-
const graph = gridGraph(10, 10); // 100 nodes
|
|
291
|
-
const shells = [
|
|
292
|
-
graph.nodes().slice(0, 1), // 1 node
|
|
293
|
-
graph.nodes().slice(1, 10), // 9 nodes
|
|
294
|
-
graph.nodes().slice(10, 30), // 20 nodes
|
|
295
|
-
graph.nodes().slice(30) // 70 nodes
|
|
296
|
-
];
|
|
297
|
-
|
|
298
|
-
const startTime = performance.now();
|
|
299
|
-
const positions = shellLayout(graph, shells);
|
|
300
|
-
const endTime = performance.now();
|
|
301
|
-
|
|
302
|
-
assert.equal(Object.keys(positions).length, 100);
|
|
303
|
-
assert.isBelow(endTime - startTime, 100);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('Layout patterns', () => {
|
|
308
|
-
it('should create concentric circles for appropriate graphs', () => {
|
|
309
|
-
// Create a graph with natural shell structure
|
|
310
|
-
const nodes = Array.from({ length: 13 }, (_, i) => i);
|
|
311
|
-
const edges: [number, number][] = [];
|
|
312
|
-
|
|
313
|
-
// Connect center to inner ring
|
|
314
|
-
for (let i = 1; i <= 4; i++) {
|
|
315
|
-
edges.push([0, i]);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Connect inner ring to outer ring
|
|
319
|
-
for (let i = 1; i <= 4; i++) {
|
|
320
|
-
edges.push([i, i + 4]);
|
|
321
|
-
edges.push([i, i + 8]);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const graph = { nodes: () => nodes, edges: () => edges };
|
|
325
|
-
const shells = [
|
|
326
|
-
[0],
|
|
327
|
-
[1, 2, 3, 4],
|
|
328
|
-
[5, 6, 7, 8],
|
|
329
|
-
[9, 10, 11, 12]
|
|
330
|
-
];
|
|
331
|
-
|
|
332
|
-
const positions = shellLayout(graph, shells);
|
|
333
|
-
|
|
334
|
-
// Verify concentric structure
|
|
335
|
-
const radii = shells.map(shell => {
|
|
336
|
-
if (shell.length === 0) return 0;
|
|
337
|
-
const [x, y] = positions[shell[0]];
|
|
338
|
-
return Math.sqrt(x * x + y * y);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
assert.equal(radii[0], 0); // Center
|
|
342
|
-
assert.isAbove(radii[1], 0);
|
|
343
|
-
assert.isAbove(radii[2], radii[1]);
|
|
344
|
-
assert.isAbove(radii[3], radii[2]);
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
});
|