@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
package/test/arf-layout.test.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
arfLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('ARF Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes', () => {
|
|
14
|
-
const graph = completeGraph(6);
|
|
15
|
-
const positions = arfLayout(graph);
|
|
16
|
-
|
|
17
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
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 = arfLayout(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 = arfLayout(singleNode);
|
|
36
|
-
|
|
37
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
38
|
-
assert.isDefined(positions['A']);
|
|
39
|
-
assert.equal(positions['A'].length, 2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should handle disconnected components', () => {
|
|
43
|
-
const graph = {
|
|
44
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
45
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
|
|
46
|
-
};
|
|
47
|
-
const positions = arfLayout(graph);
|
|
48
|
-
|
|
49
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
50
|
-
graph.nodes().forEach(node => {
|
|
51
|
-
assert.isDefined(positions[node]);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('Parameter validation', () => {
|
|
57
|
-
it('should throw error when a parameter is not greater than 1', () => {
|
|
58
|
-
const graph = cycleGraph(5);
|
|
59
|
-
|
|
60
|
-
assert.throws(() => {
|
|
61
|
-
arfLayout(graph, null, 1, 1.0); // a = 1.0 should throw
|
|
62
|
-
}, 'The parameter a should be larger than 1');
|
|
63
|
-
|
|
64
|
-
assert.throws(() => {
|
|
65
|
-
arfLayout(graph, null, 1, 0.5); // a < 1 should throw
|
|
66
|
-
}, 'The parameter a should be larger than 1');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should accept valid a parameter', () => {
|
|
70
|
-
const graph = cycleGraph(5);
|
|
71
|
-
|
|
72
|
-
assert.doesNotThrow(() => {
|
|
73
|
-
arfLayout(graph, null, 1, 1.1); // a > 1 is valid
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
assert.doesNotThrow(() => {
|
|
77
|
-
arfLayout(graph, null, 1, 2.0); // a > 1 is valid
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('Initial positions', () => {
|
|
83
|
-
it('should use provided initial positions', () => {
|
|
84
|
-
const graph = starGraph(5);
|
|
85
|
-
const initialPos = {};
|
|
86
|
-
graph.nodes().forEach((node, i) => {
|
|
87
|
-
initialPos[node] = [i * 0.1, 0]; // Line layout
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const positions = arfLayout(graph, initialPos, 1, 1.5, 50);
|
|
91
|
-
|
|
92
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
93
|
-
// Layout should use initial positions
|
|
94
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
95
|
-
// Just verify all nodes are positioned
|
|
96
|
-
graph.nodes().forEach(node => {
|
|
97
|
-
assert.isDefined(positions[node]);
|
|
98
|
-
assert.equal(positions[node].length, 2);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should generate random initial positions if not provided', () => {
|
|
103
|
-
const graph = cycleGraph(6);
|
|
104
|
-
|
|
105
|
-
const positions1 = arfLayout(graph, null, 1, 1.2, 50, 123);
|
|
106
|
-
const positions2 = arfLayout(graph, null, 1, 1.2, 50, 456);
|
|
107
|
-
|
|
108
|
-
// Different seeds should produce different layouts
|
|
109
|
-
let different = false;
|
|
110
|
-
graph.nodes().forEach(node => {
|
|
111
|
-
if (Math.abs(positions1[node][0] - positions2[node][0]) > 0.01 ||
|
|
112
|
-
Math.abs(positions1[node][1] - positions2[node][1]) > 0.01) {
|
|
113
|
-
different = true;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
assert.isTrue(different);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('Force parameters', () => {
|
|
121
|
-
it('should respect scaling parameter', () => {
|
|
122
|
-
const graph = gridGraph(4, 4);
|
|
123
|
-
|
|
124
|
-
// Use same seed for consistency
|
|
125
|
-
const positions1 = arfLayout(graph, null, 0.5, 1.2, 100, 42);
|
|
126
|
-
const positions2 = arfLayout(graph, null, 2.0, 1.2, 100, 42);
|
|
127
|
-
|
|
128
|
-
// Calculate layout spans
|
|
129
|
-
const span1 = getLayoutSpan(positions1);
|
|
130
|
-
const span2 = getLayoutSpan(positions2);
|
|
131
|
-
|
|
132
|
-
// Both should produce valid layouts
|
|
133
|
-
assert.isAbove(span1.width, 0);
|
|
134
|
-
assert.isAbove(span2.width, 0);
|
|
135
|
-
|
|
136
|
-
// The scaling parameter affects the initial force strength
|
|
137
|
-
// but final layout may be normalized by rescaleLayout
|
|
138
|
-
// Just verify both produce reasonable layouts
|
|
139
|
-
assert.isDefined(positions1);
|
|
140
|
-
assert.isDefined(positions2);
|
|
141
|
-
assert.equal(Object.keys(positions1).length, 16);
|
|
142
|
-
assert.equal(Object.keys(positions2).length, 16);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should handle different a parameter values', () => {
|
|
146
|
-
const graph = completeGraph(5);
|
|
147
|
-
|
|
148
|
-
const positions1 = arfLayout(graph, null, 1, 1.1, 100); // Weak spring
|
|
149
|
-
const positions2 = arfLayout(graph, null, 1, 2.0, 100); // Strong spring
|
|
150
|
-
|
|
151
|
-
// Both should complete successfully
|
|
152
|
-
assert.equal(Object.keys(positions1).length, 5);
|
|
153
|
-
assert.equal(Object.keys(positions2).length, 5);
|
|
154
|
-
|
|
155
|
-
// Stronger springs might produce more compact layout
|
|
156
|
-
const span1 = getLayoutSpan(positions1);
|
|
157
|
-
const span2 = getLayoutSpan(positions2);
|
|
158
|
-
|
|
159
|
-
// Just verify both produce valid layouts
|
|
160
|
-
assert.isAbove(span1.width, 0);
|
|
161
|
-
assert.isAbove(span2.width, 0);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should respect maxIter parameter', () => {
|
|
165
|
-
const graph = randomGraph(15, 0.3, 42);
|
|
166
|
-
|
|
167
|
-
const startTime1 = performance.now();
|
|
168
|
-
const positions1 = arfLayout(graph, null, 1, 1.2, 10);
|
|
169
|
-
const time1 = performance.now() - startTime1;
|
|
170
|
-
|
|
171
|
-
const startTime2 = performance.now();
|
|
172
|
-
const positions2 = arfLayout(graph, null, 1, 1.2, 1000);
|
|
173
|
-
const time2 = performance.now() - startTime2;
|
|
174
|
-
|
|
175
|
-
// More iterations should take longer
|
|
176
|
-
assert.isAbove(time2, time1);
|
|
177
|
-
|
|
178
|
-
// Both should complete
|
|
179
|
-
assert.equal(Object.keys(positions1).length, 15);
|
|
180
|
-
assert.equal(Object.keys(positions2).length, 15);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('Layout properties', () => {
|
|
185
|
-
it('should separate non-connected nodes', () => {
|
|
186
|
-
const graph = {
|
|
187
|
-
nodes: () => [0, 1, 2, 3],
|
|
188
|
-
edges: () => [[0, 1], [2, 3]] // Two separate edges
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const positions = arfLayout(graph, null, 1, 1.5, 500);
|
|
192
|
-
|
|
193
|
-
// Connected pairs should be closer than non-connected
|
|
194
|
-
const d01 = getDistance(positions, 0, 1);
|
|
195
|
-
const d23 = getDistance(positions, 2, 3);
|
|
196
|
-
const d02 = getDistance(positions, 0, 2);
|
|
197
|
-
const d13 = getDistance(positions, 1, 3);
|
|
198
|
-
|
|
199
|
-
// Check that layout is reasonable
|
|
200
|
-
assert.isAbove(d01, 0);
|
|
201
|
-
assert.isAbove(d23, 0);
|
|
202
|
-
assert.isAbove(d02, 0);
|
|
203
|
-
assert.isAbove(d13, 0);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should create reasonable layouts for regular graphs', () => {
|
|
207
|
-
const graph = cycleGraph(8);
|
|
208
|
-
const positions = arfLayout(graph, null, 1, 1.2, 500);
|
|
209
|
-
|
|
210
|
-
// Adjacent nodes in cycle should have similar distances
|
|
211
|
-
const edgeLengths = [];
|
|
212
|
-
for (let i = 0; i < 8; i++) {
|
|
213
|
-
const next = (i + 1) % 8;
|
|
214
|
-
edgeLengths.push(getDistance(positions, i, next));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const avgLength = edgeLengths.reduce((a, b) => a + b) / edgeLengths.length;
|
|
218
|
-
|
|
219
|
-
// Edge lengths should be reasonable
|
|
220
|
-
edgeLengths.forEach(len => {
|
|
221
|
-
assert.isAbove(len, 0);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should handle star graphs well', () => {
|
|
226
|
-
const graph = starGraph(7);
|
|
227
|
-
const positions = arfLayout(graph, null, 1, 1.3, 500);
|
|
228
|
-
|
|
229
|
-
// Leaves should be roughly equidistant from center
|
|
230
|
-
const centerPos = positions[0];
|
|
231
|
-
const leafDistances = [];
|
|
232
|
-
|
|
233
|
-
for (let i = 1; i < 7; i++) {
|
|
234
|
-
leafDistances.push(getDistance(positions, 0, i));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const avgDist = leafDistances.reduce((a, b) => a + b) / leafDistances.length;
|
|
238
|
-
|
|
239
|
-
leafDistances.forEach(d => {
|
|
240
|
-
assert.approximately(d, avgDist, avgDist * 0.75);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe('Seed consistency', () => {
|
|
246
|
-
it('should produce consistent results with same seed', () => {
|
|
247
|
-
const graph = randomGraph(10, 0.3, 999);
|
|
248
|
-
|
|
249
|
-
const positions1 = arfLayout(graph, null, 1, 1.2, 100, 42);
|
|
250
|
-
const positions2 = arfLayout(graph, null, 1, 1.2, 100, 42);
|
|
251
|
-
|
|
252
|
-
// Same seed should produce very similar results
|
|
253
|
-
graph.nodes().forEach(node => {
|
|
254
|
-
assert.approximately(positions1[node][0], positions2[node][0], 0.01);
|
|
255
|
-
assert.approximately(positions1[node][1], positions2[node][1], 0.01);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('should produce different results with different seeds', () => {
|
|
260
|
-
const graph = completeGraph(6);
|
|
261
|
-
|
|
262
|
-
const positions1 = arfLayout(graph, null, 1, 1.2, 100, 123);
|
|
263
|
-
const positions2 = arfLayout(graph, null, 1, 1.2, 100, 456);
|
|
264
|
-
|
|
265
|
-
// Different seeds should produce different layouts
|
|
266
|
-
let totalDiff = 0;
|
|
267
|
-
graph.nodes().forEach(node => {
|
|
268
|
-
totalDiff += Math.abs(positions1[node][0] - positions2[node][0]);
|
|
269
|
-
totalDiff += Math.abs(positions1[node][1] - positions2[node][1]);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
assert.isAbove(totalDiff, 0.1);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe('Special cases and edge cases', () => {
|
|
277
|
-
it('should handle string node IDs', () => {
|
|
278
|
-
const graph = {
|
|
279
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david'],
|
|
280
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const positions = arfLayout(graph);
|
|
284
|
-
|
|
285
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
286
|
-
['alice', 'bob', 'charlie', 'david'].forEach(node => {
|
|
287
|
-
assert.isDefined(positions[node]);
|
|
288
|
-
assert.equal(positions[node].length, 2);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('should handle large graphs reasonably', () => {
|
|
293
|
-
const graph = gridGraph(8, 8); // 64 nodes
|
|
294
|
-
|
|
295
|
-
const startTime = performance.now();
|
|
296
|
-
const positions = arfLayout(graph, null, 1, 1.1, 100, 42); // Fewer iterations for speed
|
|
297
|
-
const endTime = performance.now();
|
|
298
|
-
|
|
299
|
-
assert.equal(Object.keys(positions).length, 64);
|
|
300
|
-
assert.isBelow(endTime - startTime, 2000); // Should complete quickly
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('should handle complete graphs', () => {
|
|
304
|
-
const graph = completeGraph(8);
|
|
305
|
-
const positions = arfLayout(graph, null, 1, 1.2, 200);
|
|
306
|
-
|
|
307
|
-
// Should produce a layout
|
|
308
|
-
const bounds = getLayoutBounds(positions);
|
|
309
|
-
assert.isAbove(bounds.width, 0);
|
|
310
|
-
assert.isAbove(bounds.height, 0);
|
|
311
|
-
|
|
312
|
-
// Should produce a layout (nodes may be close in complete graph)
|
|
313
|
-
const minDist = getMinimumDistance(positions);
|
|
314
|
-
assert.isAbove(minDist, 0);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('should handle path graphs', () => {
|
|
318
|
-
const path = {
|
|
319
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
320
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const positions = arfLayout(path, null, 1, 1.2, 500);
|
|
324
|
-
|
|
325
|
-
// Path should stretch out
|
|
326
|
-
const bounds = getLayoutBounds(positions);
|
|
327
|
-
const aspectRatio = Math.max(bounds.width, bounds.height) / Math.min(bounds.width, bounds.height);
|
|
328
|
-
|
|
329
|
-
// Path graphs should have some structure
|
|
330
|
-
assert.isAbove(aspectRatio, 1.0);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
describe('Layout quality', () => {
|
|
335
|
-
it('should avoid node overlap', () => {
|
|
336
|
-
const graph = completeGraph(10);
|
|
337
|
-
const positions = arfLayout(graph, null, 1, 1.5, 500);
|
|
338
|
-
|
|
339
|
-
// Check minimum distance between nodes
|
|
340
|
-
const minDist = getMinimumDistance(positions);
|
|
341
|
-
|
|
342
|
-
// Nodes should be positioned (may be close in dense graphs)
|
|
343
|
-
assert.isAbove(minDist, 0);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should create stable layouts with enough iterations', () => {
|
|
347
|
-
const graph = randomGraph(12, 0.3, 777);
|
|
348
|
-
|
|
349
|
-
// Run with many iterations
|
|
350
|
-
const positions = arfLayout(graph, null, 1, 1.2, 1000);
|
|
351
|
-
|
|
352
|
-
// Run a few more iterations from the result
|
|
353
|
-
const positions2 = arfLayout(graph, positions, 1, 1.2, 50);
|
|
354
|
-
|
|
355
|
-
// Should not change much (layout is stable)
|
|
356
|
-
let totalChange = 0;
|
|
357
|
-
graph.nodes().forEach(node => {
|
|
358
|
-
totalChange += Math.abs(positions[node][0] - positions2[node][0]);
|
|
359
|
-
totalChange += Math.abs(positions[node][1] - positions2[node][1]);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
assert.isBelow(totalChange / graph.nodes().length, 0.1);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('should handle different graph densities', () => {
|
|
366
|
-
const sparse = randomGraph(15, 0.1, 111);
|
|
367
|
-
const dense = randomGraph(15, 0.5, 111);
|
|
368
|
-
|
|
369
|
-
const posSparse = arfLayout(sparse, null, 1, 1.2, 300);
|
|
370
|
-
const posDense = arfLayout(dense, null, 1, 1.2, 300);
|
|
371
|
-
|
|
372
|
-
// Both should produce valid layouts
|
|
373
|
-
assert.equal(Object.keys(posSparse).length, 15);
|
|
374
|
-
assert.equal(Object.keys(posDense).length, 15);
|
|
375
|
-
|
|
376
|
-
// Dense graphs might be more compact
|
|
377
|
-
const spanSparse = getLayoutSpan(posSparse);
|
|
378
|
-
const spanDense = getLayoutSpan(posDense);
|
|
379
|
-
|
|
380
|
-
// Both should have reasonable spans
|
|
381
|
-
assert.isAbove(spanSparse.width, 0);
|
|
382
|
-
assert.isAbove(spanDense.width, 0);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Helper functions
|
|
388
|
-
function getLayoutSpan(positions) {
|
|
389
|
-
const nodes = Object.keys(positions);
|
|
390
|
-
let minX = Infinity, maxX = -Infinity;
|
|
391
|
-
let minY = Infinity, maxY = -Infinity;
|
|
392
|
-
|
|
393
|
-
nodes.forEach(node => {
|
|
394
|
-
minX = Math.min(minX, positions[node][0]);
|
|
395
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
396
|
-
minY = Math.min(minY, positions[node][1]);
|
|
397
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
width: maxX - minX,
|
|
402
|
-
height: maxY - minY
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function getDistance(positions, node1, node2) {
|
|
407
|
-
const dx = positions[node1][0] - positions[node2][0];
|
|
408
|
-
const dy = positions[node1][1] - positions[node2][1];
|
|
409
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function getLayoutBounds(positions) {
|
|
413
|
-
const nodes = Object.keys(positions);
|
|
414
|
-
let minX = Infinity, maxX = -Infinity;
|
|
415
|
-
let minY = Infinity, maxY = -Infinity;
|
|
416
|
-
|
|
417
|
-
nodes.forEach(node => {
|
|
418
|
-
minX = Math.min(minX, positions[node][0]);
|
|
419
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
420
|
-
minY = Math.min(minY, positions[node][1]);
|
|
421
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
minX, maxX, minY, maxY,
|
|
426
|
-
width: maxX - minX,
|
|
427
|
-
height: maxY - minY
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function getMinimumDistance(positions) {
|
|
432
|
-
const nodes = Object.keys(positions);
|
|
433
|
-
let minDist = Infinity;
|
|
434
|
-
|
|
435
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
436
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
437
|
-
const dist = getDistance(positions, nodes[i], nodes[j]);
|
|
438
|
-
minDist = Math.min(minDist, dist);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return minDist;
|
|
443
|
-
}
|