@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,254 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
randomLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph
|
|
8
|
-
} from '../src';
|
|
9
|
-
|
|
10
|
-
describe('Random Layout', () => {
|
|
11
|
-
describe('Basic functionality', () => {
|
|
12
|
-
it('should position all nodes', () => {
|
|
13
|
-
const graph = completeGraph(8);
|
|
14
|
-
const positions = randomLayout(graph);
|
|
15
|
-
|
|
16
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
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 = randomLayout(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 = randomLayout(singleNode);
|
|
35
|
-
|
|
36
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
37
|
-
assert.isDefined(positions[0]);
|
|
38
|
-
assert.equal(positions[0].length, 2);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should handle disconnected components', () => {
|
|
42
|
-
const disconnected = {
|
|
43
|
-
nodes: () => [0, 1, 2, 3],
|
|
44
|
-
edges: () => [[0, 1], [2, 3]]
|
|
45
|
-
};
|
|
46
|
-
const positions = randomLayout(disconnected);
|
|
47
|
-
|
|
48
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
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('Random distribution properties', () => {
|
|
57
|
-
it('should position nodes within [0, 1] range by default', () => {
|
|
58
|
-
const graph = cycleGraph(20);
|
|
59
|
-
const positions = randomLayout(graph);
|
|
60
|
-
|
|
61
|
-
graph.nodes().forEach(node => {
|
|
62
|
-
const [x, y] = positions[node];
|
|
63
|
-
assert.isAtLeast(x, -1);
|
|
64
|
-
assert.isAtMost(x, 1);
|
|
65
|
-
assert.isAtLeast(y, -1);
|
|
66
|
-
assert.isAtMost(y, 1);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should distribute nodes across the full range', () => {
|
|
71
|
-
const graph = gridGraph(10, 10); // 100 nodes
|
|
72
|
-
const positions = randomLayout(graph);
|
|
73
|
-
|
|
74
|
-
const xValues = graph.nodes().map(node => positions[node][0]);
|
|
75
|
-
const yValues = graph.nodes().map(node => positions[node][1]);
|
|
76
|
-
|
|
77
|
-
// With 100 nodes, we should have good coverage
|
|
78
|
-
const minX = Math.min(...xValues);
|
|
79
|
-
const maxX = Math.max(...xValues);
|
|
80
|
-
const minY = Math.min(...yValues);
|
|
81
|
-
const maxY = Math.max(...yValues);
|
|
82
|
-
|
|
83
|
-
// Should use a good portion of the [-1, 1] range
|
|
84
|
-
// With random distribution, we can't guarantee 80% coverage
|
|
85
|
-
assert.isAbove(maxX - minX, 0.5);
|
|
86
|
-
assert.isAbove(maxY - minY, 0.5);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle default range', () => {
|
|
90
|
-
const graph = starGraph(10);
|
|
91
|
-
const positions = randomLayout(graph);
|
|
92
|
-
|
|
93
|
-
graph.nodes().forEach(node => {
|
|
94
|
-
const [x, y] = positions[node];
|
|
95
|
-
assert.isAtLeast(x, -1);
|
|
96
|
-
assert.isAtMost(x, 1);
|
|
97
|
-
assert.isAtLeast(y, -1);
|
|
98
|
-
assert.isAtMost(y, 1);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('Parameter variations', () => {
|
|
104
|
-
it('should produce different random layouts', () => {
|
|
105
|
-
const graph = completeGraph(5);
|
|
106
|
-
|
|
107
|
-
// Get multiple random layouts
|
|
108
|
-
const positions1 = randomLayout(graph);
|
|
109
|
-
const positions2 = randomLayout(graph);
|
|
110
|
-
const positions3 = randomLayout(graph);
|
|
111
|
-
|
|
112
|
-
// At least some positions should differ
|
|
113
|
-
let different = false;
|
|
114
|
-
graph.nodes().forEach(node => {
|
|
115
|
-
if (positions1[node][0] !== positions2[node][0] ||
|
|
116
|
-
positions1[node][1] !== positions2[node][1] ||
|
|
117
|
-
positions2[node][0] !== positions3[node][0] ||
|
|
118
|
-
positions2[node][1] !== positions3[node][1]) {
|
|
119
|
-
different = true;
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
assert.isTrue(different);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should respect center parameter', () => {
|
|
126
|
-
const graph = cycleGraph(6);
|
|
127
|
-
const center = [10, 20];
|
|
128
|
-
|
|
129
|
-
const positions = randomLayout(graph, center);
|
|
130
|
-
|
|
131
|
-
// Positions should be around the center
|
|
132
|
-
graph.nodes().forEach(node => {
|
|
133
|
-
const [x, y] = positions[node];
|
|
134
|
-
assert.isAtLeast(x, center[0] - 1);
|
|
135
|
-
assert.isAtMost(x, center[0] + 1);
|
|
136
|
-
assert.isAtLeast(y, center[1] - 1);
|
|
137
|
-
assert.isAtMost(y, center[1] + 1);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle different dimensions', () => {
|
|
142
|
-
const graph = completeGraph(4);
|
|
143
|
-
|
|
144
|
-
// 2D layout (default)
|
|
145
|
-
const positions2D = randomLayout(graph, [0, 0], 2);
|
|
146
|
-
graph.nodes().forEach(node => {
|
|
147
|
-
assert.equal(positions2D[node].length, 2);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// 3D layout
|
|
151
|
-
const positions3D = randomLayout(graph, [0, 0, 0], 3);
|
|
152
|
-
graph.nodes().forEach(node => {
|
|
153
|
-
assert.equal(positions3D[node].length, 3);
|
|
154
|
-
assert.isNumber(positions3D[node][2]);
|
|
155
|
-
assert.isAtLeast(positions3D[node][2], -1);
|
|
156
|
-
assert.isAtMost(positions3D[node][2], 1);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should produce different layouts with different seeds', () => {
|
|
161
|
-
const graph = starGraph(8);
|
|
162
|
-
|
|
163
|
-
const positions1 = randomLayout(graph, [0, 0], 2, 42);
|
|
164
|
-
const positions2 = randomLayout(graph, [0, 0], 2, 123);
|
|
165
|
-
|
|
166
|
-
// At least some positions should differ
|
|
167
|
-
let different = false;
|
|
168
|
-
graph.nodes().forEach(node => {
|
|
169
|
-
if (positions1[node][0] !== positions2[node][0] ||
|
|
170
|
-
positions1[node][1] !== positions2[node][1]) {
|
|
171
|
-
different = true;
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
assert.isTrue(different);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should produce same layout with same seed', () => {
|
|
178
|
-
const graph = completeGraph(10);
|
|
179
|
-
const seed = 12345;
|
|
180
|
-
|
|
181
|
-
const positions1 = randomLayout(graph, [0, 0], 2, seed);
|
|
182
|
-
const positions2 = randomLayout(graph, [0, 0], 2, seed);
|
|
183
|
-
|
|
184
|
-
// Should be identical
|
|
185
|
-
graph.nodes().forEach(node => {
|
|
186
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe('Special cases', () => {
|
|
192
|
-
it('should handle string node IDs', () => {
|
|
193
|
-
const graph = {
|
|
194
|
-
nodes: () => ['A', 'B', 'C', 'D'],
|
|
195
|
-
edges: () => [['A', 'B'], ['B', 'C'], ['C', 'D']]
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const positions = randomLayout(graph);
|
|
199
|
-
|
|
200
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
201
|
-
['A', 'B', 'C', 'D'].forEach(node => {
|
|
202
|
-
assert.isDefined(positions[node]);
|
|
203
|
-
assert.isAtLeast(positions[node][0], -1);
|
|
204
|
-
assert.isAtMost(positions[node][0], 1);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should handle negative center values', () => {
|
|
209
|
-
const graph = cycleGraph(4);
|
|
210
|
-
const positions = randomLayout(graph, [-5, -5]);
|
|
211
|
-
|
|
212
|
-
// Should place nodes around negative center
|
|
213
|
-
graph.nodes().forEach(node => {
|
|
214
|
-
const [x, y] = positions[node];
|
|
215
|
-
assert.isAtLeast(x, -6);
|
|
216
|
-
assert.isAtMost(x, -4);
|
|
217
|
-
assert.isAtLeast(y, -6);
|
|
218
|
-
assert.isAtMost(y, -4);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should be fast for large graphs', () => {
|
|
223
|
-
const graph = gridGraph(20, 20); // 400 nodes
|
|
224
|
-
|
|
225
|
-
const startTime = performance.now();
|
|
226
|
-
const positions = randomLayout(graph);
|
|
227
|
-
const endTime = performance.now();
|
|
228
|
-
|
|
229
|
-
assert.equal(Object.keys(positions).length, 400);
|
|
230
|
-
assert.isBelow(endTime - startTime, 50); // Should be very fast
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should avoid node overlap statistically', () => {
|
|
234
|
-
const graph = completeGraph(20);
|
|
235
|
-
const positions = randomLayout(graph);
|
|
236
|
-
|
|
237
|
-
// Calculate minimum distance between any two nodes
|
|
238
|
-
let minDistance = Infinity;
|
|
239
|
-
const nodes = graph.nodes();
|
|
240
|
-
|
|
241
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
242
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
243
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
244
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
245
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
246
|
-
minDistance = Math.min(minDistance, distance);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// With 20 nodes in 10x10 space, minimum distance should be > 0
|
|
251
|
-
assert.isAbove(minDistance, 0);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
});
|
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
rescaleLayout,
|
|
4
|
-
rescaleLayoutDict,
|
|
5
|
-
circularLayout,
|
|
6
|
-
randomLayout,
|
|
7
|
-
completeGraph,
|
|
8
|
-
starGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Rescale Layout', () => {
|
|
12
|
-
describe('Dictionary format (rescaleLayout)', () => {
|
|
13
|
-
it('should rescale positions dictionary', () => {
|
|
14
|
-
const positions = {
|
|
15
|
-
0: [1, 1],
|
|
16
|
-
1: [-1, 1],
|
|
17
|
-
2: [-1, -1],
|
|
18
|
-
3: [1, -1]
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const rescaled = rescaleLayout(positions, 2);
|
|
22
|
-
|
|
23
|
-
// The function scales based on max distance from center
|
|
24
|
-
// Max distance is sqrt(2), so scale factor is 2/sqrt(2) = sqrt(2)
|
|
25
|
-
const expectedScale = Math.sqrt(2);
|
|
26
|
-
assert.approximately(rescaled[0][0], expectedScale, 0.01);
|
|
27
|
-
assert.approximately(rescaled[0][1], expectedScale, 0.01);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should center positions around specified point', () => {
|
|
31
|
-
const positions = {
|
|
32
|
-
A: [1, 1],
|
|
33
|
-
B: [-1, 1],
|
|
34
|
-
C: [-1, -1],
|
|
35
|
-
D: [1, -1]
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const center = [10, 5];
|
|
39
|
-
const rescaled = rescaleLayout(positions, 1, center);
|
|
40
|
-
|
|
41
|
-
// The max distance is sqrt(2), scale is 1/sqrt(2)
|
|
42
|
-
const scale = 1 / Math.sqrt(2);
|
|
43
|
-
assert.approximately(rescaled['A'][0], 10 + scale, 0.01);
|
|
44
|
-
assert.approximately(rescaled['A'][1], 5 + scale, 0.01);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should handle empty positions', () => {
|
|
48
|
-
const positions = {};
|
|
49
|
-
const rescaled = rescaleLayout(positions, 2, [5, 5]);
|
|
50
|
-
|
|
51
|
-
assert.deepEqual(rescaled, {});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should handle single node', () => {
|
|
55
|
-
const positions = { node1: [3, 4] };
|
|
56
|
-
const rescaled = rescaleLayout(positions, 2, [0, 0]);
|
|
57
|
-
|
|
58
|
-
// Single node should be placed at center
|
|
59
|
-
assert.deepEqual(rescaled['node1'], [0, 0]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should scale and center combined', () => {
|
|
63
|
-
const positions = {
|
|
64
|
-
0: [2, 0],
|
|
65
|
-
1: [0, 2],
|
|
66
|
-
2: [-2, 0],
|
|
67
|
-
3: [0, -2]
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const rescaled = rescaleLayout(positions, 0.5, [10, 10]);
|
|
71
|
-
|
|
72
|
-
// Max distance is 2, scale factor is 0.5/2 = 0.25
|
|
73
|
-
assert.approximately(rescaled[0][0], 10.5, 0.01);
|
|
74
|
-
assert.approximately(rescaled[0][1], 10, 0.01);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('Array format (rescaleLayout)', () => {
|
|
79
|
-
it('should rescale positions array', () => {
|
|
80
|
-
const positions = [
|
|
81
|
-
[1, 1],
|
|
82
|
-
[-1, 1],
|
|
83
|
-
[-1, -1],
|
|
84
|
-
[1, -1]
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const rescaled = rescaleLayout(positions, 3);
|
|
88
|
-
|
|
89
|
-
// Max distance is sqrt(2), scale factor is 3/sqrt(2)
|
|
90
|
-
const expectedScale = 3 / Math.sqrt(2);
|
|
91
|
-
assert.approximately(rescaled[0][0], expectedScale, 0.01);
|
|
92
|
-
assert.approximately(rescaled[0][1], expectedScale, 0.01);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should center array positions', () => {
|
|
96
|
-
const positions = [
|
|
97
|
-
[1, 0],
|
|
98
|
-
[0, 1],
|
|
99
|
-
[-1, 0],
|
|
100
|
-
[0, -1]
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const rescaled = rescaleLayout(positions, 1, [5, 5]);
|
|
104
|
-
|
|
105
|
-
assert.deepEqual(rescaled[0], [6, 5]);
|
|
106
|
-
assert.deepEqual(rescaled[1], [5, 6]);
|
|
107
|
-
assert.deepEqual(rescaled[2], [4, 5]);
|
|
108
|
-
assert.deepEqual(rescaled[3], [5, 4]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should handle empty array', () => {
|
|
112
|
-
const positions: number[][] = [];
|
|
113
|
-
const rescaled = rescaleLayout(positions, 2, [5, 5]);
|
|
114
|
-
|
|
115
|
-
assert.deepEqual(rescaled, []);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should handle 3D positions', () => {
|
|
119
|
-
const positions = [
|
|
120
|
-
[1, 1, 1],
|
|
121
|
-
[-1, -1, -1]
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
const rescaled = rescaleLayout(positions, 2, [0, 0, 0]);
|
|
125
|
-
|
|
126
|
-
// Max distance is sqrt(3), scale factor is 2/sqrt(3)
|
|
127
|
-
const scale = 2 / Math.sqrt(3);
|
|
128
|
-
assert.approximately(rescaled[0][0], scale, 0.01);
|
|
129
|
-
assert.approximately(rescaled[0][1], scale, 0.01);
|
|
130
|
-
assert.approximately(rescaled[0][2], scale, 0.01);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('Integration with layouts', () => {
|
|
135
|
-
it('should rescale circular layout', () => {
|
|
136
|
-
const graph = completeGraph(6);
|
|
137
|
-
const positions = circularLayout(graph, 1); // radius 1
|
|
138
|
-
|
|
139
|
-
const rescaled = rescaleLayout(positions, 5); // scale up to radius 5
|
|
140
|
-
|
|
141
|
-
// Check that all nodes are now at radius ~5
|
|
142
|
-
Object.values(rescaled).forEach(pos => {
|
|
143
|
-
const radius = Math.sqrt(pos[0] ** 2 + pos[1] ** 2);
|
|
144
|
-
assert.approximately(radius, 5, 0.01);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should recenter random layout', () => {
|
|
149
|
-
const graph = starGraph(5);
|
|
150
|
-
const positions = randomLayout(graph, [0, 0]); // centered at origin
|
|
151
|
-
|
|
152
|
-
const newCenter = [20, 30];
|
|
153
|
-
const rescaled = rescaleLayout(positions, 1, newCenter);
|
|
154
|
-
|
|
155
|
-
// Calculate new center of mass
|
|
156
|
-
const com = [0, 0];
|
|
157
|
-
Object.values(rescaled).forEach(pos => {
|
|
158
|
-
com[0] += pos[0];
|
|
159
|
-
com[1] += pos[1];
|
|
160
|
-
});
|
|
161
|
-
com[0] /= 5;
|
|
162
|
-
com[1] /= 5;
|
|
163
|
-
|
|
164
|
-
assert.approximately(com[0], newCenter[0], 1);
|
|
165
|
-
assert.approximately(com[1], newCenter[1], 1);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should maintain relative positions', () => {
|
|
169
|
-
const positions = {
|
|
170
|
-
A: [1, 0],
|
|
171
|
-
B: [0, 1],
|
|
172
|
-
C: [-1, 0],
|
|
173
|
-
D: [0, -1]
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const rescaled = rescaleLayout(positions, 2.5, [10, 10]);
|
|
177
|
-
|
|
178
|
-
// Check that relative positions are maintained
|
|
179
|
-
// A-C should still be horizontal, B-D vertical
|
|
180
|
-
assert.approximately(rescaled['A'][1], rescaled['C'][1], 0.001); // Same Y
|
|
181
|
-
assert.approximately(rescaled['B'][0], rescaled['D'][0], 0.001); // Same X
|
|
182
|
-
|
|
183
|
-
// Check distance is scaled properly
|
|
184
|
-
const originalMaxDist = 1; // from center to any point
|
|
185
|
-
const newMaxDist = Math.sqrt((rescaled['A'][0] - 10) ** 2 + (rescaled['A'][1] - 10) ** 2);
|
|
186
|
-
assert.approximately(newMaxDist, 2.5, 0.01);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('rescaleLayoutDict specific', () => {
|
|
191
|
-
it('should be an alias for rescaleLayout with dictionary', () => {
|
|
192
|
-
const positions = {
|
|
193
|
-
node1: [1, 1],
|
|
194
|
-
node2: [-1, -1]
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const result1 = rescaleLayout(positions, 2);
|
|
198
|
-
const result2 = rescaleLayoutDict(positions, 2);
|
|
199
|
-
|
|
200
|
-
// Compare values with tolerance for floating point
|
|
201
|
-
Object.keys(result1).forEach(key => {
|
|
202
|
-
assert.approximately(result1[key][0], result2[key][0], 0.001);
|
|
203
|
-
assert.approximately(result1[key][1], result2[key][1], 0.001);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should handle string node IDs', () => {
|
|
208
|
-
const positions = {
|
|
209
|
-
'user:1': [2, 3],
|
|
210
|
-
'user:2': [4, 5],
|
|
211
|
-
'item:1': [1, 2]
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const rescaled = rescaleLayoutDict(positions, 0.5, [0, 0]);
|
|
215
|
-
|
|
216
|
-
assert.isDefined(rescaled['user:1']);
|
|
217
|
-
assert.isDefined(rescaled['user:2']);
|
|
218
|
-
assert.isDefined(rescaled['item:1']);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('Edge cases', () => {
|
|
223
|
-
it('should handle zero scale', () => {
|
|
224
|
-
const positions = {
|
|
225
|
-
A: [5, 5],
|
|
226
|
-
B: [10, 10],
|
|
227
|
-
C: [-5, -5]
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const rescaled = rescaleLayout(positions, 0, [7, 7]);
|
|
231
|
-
|
|
232
|
-
// All nodes should collapse to center
|
|
233
|
-
assert.deepEqual(rescaled['A'], [7, 7]);
|
|
234
|
-
assert.deepEqual(rescaled['B'], [7, 7]);
|
|
235
|
-
assert.deepEqual(rescaled['C'], [7, 7]);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should handle negative scale', () => {
|
|
239
|
-
const positions = {
|
|
240
|
-
0: [1, 0],
|
|
241
|
-
1: [0, 1]
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const rescaled = rescaleLayout(positions, -1);
|
|
245
|
-
|
|
246
|
-
// Center is at (0.5, 0.5)
|
|
247
|
-
// Centered positions: [0.5, -0.5] and [-0.5, 0.5]
|
|
248
|
-
// Max distance = sqrt(0.5² + 0.5²) = 0.707...
|
|
249
|
-
// Scale factor = -1/0.707... = -1.414...
|
|
250
|
-
// Should flip and scale positions
|
|
251
|
-
assert.approximately(rescaled[0][0], -0.707, 0.01);
|
|
252
|
-
assert.approximately(rescaled[0][1], 0.707, 0.01);
|
|
253
|
-
assert.approximately(rescaled[1][0], 0.707, 0.01);
|
|
254
|
-
assert.approximately(rescaled[1][1], -0.707, 0.01);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('should preserve original positions object', () => {
|
|
258
|
-
const positions = {
|
|
259
|
-
A: [1, 2],
|
|
260
|
-
B: [3, 4]
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const original = JSON.parse(JSON.stringify(positions));
|
|
264
|
-
const rescaled = rescaleLayout(positions, 2);
|
|
265
|
-
|
|
266
|
-
// Original should be unchanged
|
|
267
|
-
assert.deepEqual(positions, original);
|
|
268
|
-
|
|
269
|
-
// Rescaled should be different
|
|
270
|
-
assert.notDeepEqual(rescaled['A'], positions['A']);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should handle positions with different dimensions', () => {
|
|
274
|
-
const positions = {
|
|
275
|
-
A: [1, 2],
|
|
276
|
-
B: [3, 4, 5], // 3D
|
|
277
|
-
C: [6] // 1D
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const rescaled = rescaleLayout(positions, 2, [0, 0, 0]);
|
|
281
|
-
|
|
282
|
-
// rescaleLayout uses dimension of first position (A has 2D)
|
|
283
|
-
// But when center has more dimensions, the result extends to match center
|
|
284
|
-
assert.equal(rescaled['A'].length, 3);
|
|
285
|
-
assert.equal(rescaled['A'][2], 0); // Third dimension from center
|
|
286
|
-
assert.equal(rescaled['B'].length, 3);
|
|
287
|
-
assert.equal(rescaled['C'].length, 3);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should handle very large scale factors', () => {
|
|
291
|
-
const positions = {
|
|
292
|
-
0: [0.001, 0],
|
|
293
|
-
1: [-0.001, 0]
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const rescaled = rescaleLayout(positions, 1000);
|
|
297
|
-
|
|
298
|
-
// Max distance is 0.001, scale factor is 1000/0.001 = 1,000,000
|
|
299
|
-
assert.approximately(rescaled[0][0], 1000, 0.1);
|
|
300
|
-
assert.approximately(rescaled[1][0], -1000, 0.1);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should handle positions at origin', () => {
|
|
304
|
-
const positions = {
|
|
305
|
-
A: [0, 0],
|
|
306
|
-
B: [1, 0],
|
|
307
|
-
C: [0, 1]
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const rescaled = rescaleLayout(positions, 3, [10, 10]);
|
|
311
|
-
|
|
312
|
-
// Center is at (1/3, 1/3)
|
|
313
|
-
// After centering: A=[-1/3,-1/3], B=[2/3,-1/3], C=[-1/3,2/3]
|
|
314
|
-
// Max distance = sqrt((2/3)^2 + (2/3)^2) = sqrt(8/9) = 0.9428...
|
|
315
|
-
// Scale factor = 3 / 0.9428 = 3.182...
|
|
316
|
-
// B after scaling = [2/3 * 3.182, -1/3 * 3.182] + [10, 10] = [12.12, 8.94]
|
|
317
|
-
const dist = Math.sqrt((rescaled['B'][0] - 10) ** 2 + (rescaled['B'][1] - 10) ** 2);
|
|
318
|
-
assert.approximately(dist, 3, 0.1);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe('Practical usage patterns', () => {
|
|
323
|
-
it('should normalize layout to unit square', () => {
|
|
324
|
-
const positions = {
|
|
325
|
-
0: [10, 20],
|
|
326
|
-
1: [30, 40],
|
|
327
|
-
2: [20, 10],
|
|
328
|
-
3: [40, 30]
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// Scale to fit in unit square centered at (0.5, 0.5)
|
|
332
|
-
const rescaled = rescaleLayout(positions, 0.5, [0.5, 0.5]);
|
|
333
|
-
|
|
334
|
-
// Check that layout fits in reasonable bounds
|
|
335
|
-
const values = Object.values(rescaled);
|
|
336
|
-
const xCoords = values.map(p => p[0]);
|
|
337
|
-
const yCoords = values.map(p => p[1]);
|
|
338
|
-
|
|
339
|
-
assert.isAtLeast(Math.min(...xCoords), 0);
|
|
340
|
-
assert.isAtMost(Math.max(...xCoords), 1);
|
|
341
|
-
assert.isAtLeast(Math.min(...yCoords), 0);
|
|
342
|
-
assert.isAtMost(Math.max(...yCoords), 1);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('should combine multiple layouts with different scales', () => {
|
|
346
|
-
const layout1 = {
|
|
347
|
-
A: [1, 1],
|
|
348
|
-
B: [-1, 1]
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const layout2 = {
|
|
352
|
-
C: [0.5, 0.5],
|
|
353
|
-
D: [-0.5, 0.5]
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
// Scale second layout and offset it
|
|
357
|
-
const rescaled2 = rescaleLayout(layout2, 2, [5, 0]);
|
|
358
|
-
|
|
359
|
-
// Combine layouts
|
|
360
|
-
const combined = { ...layout1, ...rescaled2 };
|
|
361
|
-
|
|
362
|
-
assert.equal(Object.keys(combined).length, 4);
|
|
363
|
-
assert.deepEqual(combined['A'], [1, 1]);
|
|
364
|
-
|
|
365
|
-
// Check rescaled layout is positioned correctly
|
|
366
|
-
const centerX = (rescaled2['C'][0] + rescaled2['D'][0]) / 2;
|
|
367
|
-
assert.approximately(centerX, 5, 0.1);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Note: rescaleLayoutDict is tested implicitly through rescaleLayout tests
|
|
373
|
-
// since they share the same implementation for dictionary inputs
|