@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,477 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
fruchtermanReingoldLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Fruchterman-Reingold Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes', () => {
|
|
14
|
-
const graph = completeGraph(6);
|
|
15
|
-
const positions = fruchtermanReingoldLayout(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 = fruchtermanReingoldLayout(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 = fruchtermanReingoldLayout(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 = fruchtermanReingoldLayout(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 variations', () => {
|
|
57
|
-
it('should respect k parameter (optimal distance)', () => {
|
|
58
|
-
const graph = cycleGraph(8);
|
|
59
|
-
|
|
60
|
-
const positions1 = fruchtermanReingoldLayout(graph, 0.5);
|
|
61
|
-
const positions2 = fruchtermanReingoldLayout(graph, 2.0);
|
|
62
|
-
|
|
63
|
-
// Both should produce valid layouts
|
|
64
|
-
assert.equal(Object.keys(positions1).length, 8);
|
|
65
|
-
assert.equal(Object.keys(positions2).length, 8);
|
|
66
|
-
|
|
67
|
-
// Larger k should generally produce more spread out layout
|
|
68
|
-
const avgDist1 = getAverageEdgeLength(graph, positions1);
|
|
69
|
-
const avgDist2 = getAverageEdgeLength(graph, positions2);
|
|
70
|
-
|
|
71
|
-
// k affects the ideal spring length
|
|
72
|
-
assert.isAbove(avgDist1, 0);
|
|
73
|
-
assert.isAbove(avgDist2, 0);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should use provided initial positions', () => {
|
|
77
|
-
const graph = starGraph(6);
|
|
78
|
-
const initialPos = {};
|
|
79
|
-
graph.nodes().forEach((node, i) => {
|
|
80
|
-
initialPos[node] = [i * 0.2, 0]; // Line layout
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const positions = fruchtermanReingoldLayout(graph, null, initialPos);
|
|
84
|
-
|
|
85
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
86
|
-
// Should evolve from initial positions
|
|
87
|
-
let different = false;
|
|
88
|
-
graph.nodes().forEach(node => {
|
|
89
|
-
if (positions[node][0] !== initialPos[node][0] ||
|
|
90
|
-
positions[node][1] !== initialPos[node][1]) {
|
|
91
|
-
different = true;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
assert.isTrue(different);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should respect fixed nodes', () => {
|
|
98
|
-
const graph = cycleGraph(5);
|
|
99
|
-
const initialPos = {};
|
|
100
|
-
graph.nodes().forEach((node, i) => {
|
|
101
|
-
const angle = (2 * Math.PI * i) / 5;
|
|
102
|
-
initialPos[node] = [Math.cos(angle), Math.sin(angle)];
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const fixed = [0, 2]; // Fix nodes 0 and 2
|
|
106
|
-
const positions = fruchtermanReingoldLayout(graph, null, initialPos, fixed);
|
|
107
|
-
|
|
108
|
-
// Fixed nodes should not move
|
|
109
|
-
assert.deepEqual(positions[0], initialPos[0]);
|
|
110
|
-
assert.deepEqual(positions[2], initialPos[2]);
|
|
111
|
-
|
|
112
|
-
// Other nodes should be able to move
|
|
113
|
-
assert.notDeepEqual(positions[1], initialPos[1]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should respect iterations parameter', () => {
|
|
117
|
-
const graph = randomGraph(15, 0.3, 42);
|
|
118
|
-
|
|
119
|
-
const startTime1 = performance.now();
|
|
120
|
-
const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 10);
|
|
121
|
-
const time1 = performance.now() - startTime1;
|
|
122
|
-
|
|
123
|
-
const startTime2 = performance.now();
|
|
124
|
-
const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
125
|
-
const time2 = performance.now() - startTime2;
|
|
126
|
-
|
|
127
|
-
// More iterations should take longer
|
|
128
|
-
assert.isAbove(time2, time1);
|
|
129
|
-
|
|
130
|
-
// Both should complete
|
|
131
|
-
assert.equal(Object.keys(positions1).length, 15);
|
|
132
|
-
assert.equal(Object.keys(positions2).length, 15);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should respect scale parameter', () => {
|
|
136
|
-
const graph = gridGraph(4, 4);
|
|
137
|
-
|
|
138
|
-
const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1);
|
|
139
|
-
const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 2);
|
|
140
|
-
|
|
141
|
-
// Calculate spans
|
|
142
|
-
const span1 = getLayoutSpan(positions1);
|
|
143
|
-
const span2 = getLayoutSpan(positions2);
|
|
144
|
-
|
|
145
|
-
// Scale parameter affects the final layout size
|
|
146
|
-
// but the exact ratio might vary due to force-directed nature
|
|
147
|
-
assert.isAbove(span2.width, span1.width);
|
|
148
|
-
assert.isAbove(span2.height, span1.height);
|
|
149
|
-
|
|
150
|
-
// Both should produce reasonable layouts
|
|
151
|
-
assert.isAbove(span1.width, 0);
|
|
152
|
-
assert.isAbove(span1.height, 0);
|
|
153
|
-
assert.isAbove(span2.width, 0);
|
|
154
|
-
assert.isAbove(span2.height, 0);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should respect center parameter', () => {
|
|
158
|
-
const graph = completeGraph(5);
|
|
159
|
-
const center = [10, -5];
|
|
160
|
-
|
|
161
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, center);
|
|
162
|
-
|
|
163
|
-
// Calculate center of mass
|
|
164
|
-
const com = getCenterOfMass(positions);
|
|
165
|
-
|
|
166
|
-
assert.approximately(com[0], center[0], 1);
|
|
167
|
-
assert.approximately(com[1], center[1], 1);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should handle different dimensions', () => {
|
|
171
|
-
const graph = cycleGraph(6);
|
|
172
|
-
|
|
173
|
-
// 2D layout
|
|
174
|
-
const positions2D = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0], 2);
|
|
175
|
-
graph.nodes().forEach(node => {
|
|
176
|
-
assert.equal(positions2D[node].length, 2);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// 3D layout
|
|
180
|
-
const positions3D = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0, 0], 3);
|
|
181
|
-
graph.nodes().forEach(node => {
|
|
182
|
-
assert.equal(positions3D[node].length, 3);
|
|
183
|
-
assert.isNumber(positions3D[node][2]);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
describe('Force-directed properties', () => {
|
|
189
|
-
it('should separate non-connected nodes', () => {
|
|
190
|
-
const graph = {
|
|
191
|
-
nodes: () => [0, 1, 2, 3],
|
|
192
|
-
edges: () => [[0, 1], [2, 3]] // Two separate edges
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
196
|
-
|
|
197
|
-
// Connected pairs should be at reasonable distance
|
|
198
|
-
const d01 = getDistance(positions, 0, 1);
|
|
199
|
-
const d23 = getDistance(positions, 2, 3);
|
|
200
|
-
|
|
201
|
-
// Non-connected nodes should be further apart
|
|
202
|
-
const d02 = getDistance(positions, 0, 2);
|
|
203
|
-
const d13 = getDistance(positions, 1, 3);
|
|
204
|
-
|
|
205
|
-
assert.isAbove(d01, 0);
|
|
206
|
-
assert.isAbove(d23, 0);
|
|
207
|
-
assert.isAbove(d02, d01);
|
|
208
|
-
assert.isAbove(d13, d23);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should create symmetric layouts for symmetric graphs', () => {
|
|
212
|
-
const graph = completeGraph(6);
|
|
213
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
214
|
-
|
|
215
|
-
// All pairwise distances should be similar in complete graph
|
|
216
|
-
const distances = [];
|
|
217
|
-
const nodes = graph.nodes();
|
|
218
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
219
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
220
|
-
distances.push(getDistance(positions, nodes[i], nodes[j]));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
225
|
-
const maxDeviation = Math.max(...distances.map(d => Math.abs(d - avgDist) / avgDist));
|
|
226
|
-
|
|
227
|
-
// Allow some variation but should be roughly symmetric
|
|
228
|
-
assert.isBelow(maxDeviation, 0.5);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should handle star graphs well', () => {
|
|
232
|
-
const graph = starGraph(8);
|
|
233
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
234
|
-
|
|
235
|
-
// Center node should be roughly in the middle
|
|
236
|
-
const centerNode = 0;
|
|
237
|
-
const leafNodes = Array.from({length: 7}, (_, i) => i + 1);
|
|
238
|
-
|
|
239
|
-
// Leaves should be roughly equidistant from center
|
|
240
|
-
const leafDistances = leafNodes.map(leaf => getDistance(positions, centerNode, leaf));
|
|
241
|
-
const avgDist = leafDistances.reduce((a, b) => a + b) / leafDistances.length;
|
|
242
|
-
|
|
243
|
-
leafDistances.forEach(d => {
|
|
244
|
-
assert.approximately(d, avgDist, avgDist * 0.3);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('Convergence and stability', () => {
|
|
250
|
-
it('should converge to stable layout', () => {
|
|
251
|
-
const graph = randomGraph(10, 0.3, 123);
|
|
252
|
-
|
|
253
|
-
// Run with many iterations
|
|
254
|
-
const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 200);
|
|
255
|
-
|
|
256
|
-
// Run a few more iterations from the result
|
|
257
|
-
const positions2 = fruchtermanReingoldLayout(graph, null, positions1, null, 20);
|
|
258
|
-
|
|
259
|
-
// Should not change much
|
|
260
|
-
let totalMovement = 0;
|
|
261
|
-
graph.nodes().forEach(node => {
|
|
262
|
-
totalMovement += getDistance(positions1, positions2, node, node);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
assert.isBelow(totalMovement / graph.nodes().length, 0.1);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('should produce reasonable layouts for grid graphs', () => {
|
|
269
|
-
const graph = gridGraph(5, 5);
|
|
270
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
271
|
-
|
|
272
|
-
// Should maintain some grid-like structure
|
|
273
|
-
// Check that adjacent nodes in grid are relatively close
|
|
274
|
-
for (let i = 0; i < 5; i++) {
|
|
275
|
-
for (let j = 0; j < 4; j++) {
|
|
276
|
-
const node1 = `${i},${j}`;
|
|
277
|
-
const node2 = `${i},${j+1}`;
|
|
278
|
-
const dist = getDistance(positions, node1, node2);
|
|
279
|
-
|
|
280
|
-
assert.isAbove(dist, 0);
|
|
281
|
-
assert.isBelow(dist, 2); // Should be reasonably close
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe('Seed consistency', () => {
|
|
288
|
-
it('should produce consistent results with same seed', () => {
|
|
289
|
-
const graph = randomGraph(12, 0.3, 999);
|
|
290
|
-
|
|
291
|
-
const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 42);
|
|
292
|
-
const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 42);
|
|
293
|
-
|
|
294
|
-
// Same seed should produce same initial random positions
|
|
295
|
-
graph.nodes().forEach(node => {
|
|
296
|
-
assert.approximately(positions1[node][0], positions2[node][0], 0.1);
|
|
297
|
-
assert.approximately(positions1[node][1], positions2[node][1], 0.1);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should produce different results with different seeds', () => {
|
|
302
|
-
const graph = cycleGraph(8);
|
|
303
|
-
|
|
304
|
-
const positions1 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 123);
|
|
305
|
-
const positions2 = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, null, 2, 456);
|
|
306
|
-
|
|
307
|
-
// Different seeds should produce different layouts
|
|
308
|
-
let totalDiff = 0;
|
|
309
|
-
graph.nodes().forEach(node => {
|
|
310
|
-
totalDiff += Math.abs(positions1[node][0] - positions2[node][0]);
|
|
311
|
-
totalDiff += Math.abs(positions1[node][1] - positions2[node][1]);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
assert.isAbove(totalDiff, 1);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('Special cases and edge cases', () => {
|
|
319
|
-
it('should handle string node IDs', () => {
|
|
320
|
-
const graph = {
|
|
321
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david'],
|
|
322
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const positions = fruchtermanReingoldLayout(graph);
|
|
326
|
-
|
|
327
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
328
|
-
['alice', 'bob', 'charlie', 'david'].forEach(node => {
|
|
329
|
-
assert.isDefined(positions[node]);
|
|
330
|
-
assert.equal(positions[node].length, 2);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should handle large graphs reasonably', () => {
|
|
335
|
-
const graph = gridGraph(10, 10); // 100 nodes
|
|
336
|
-
|
|
337
|
-
const startTime = performance.now();
|
|
338
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 50, 1, [0, 0], 2);
|
|
339
|
-
const endTime = performance.now();
|
|
340
|
-
|
|
341
|
-
assert.equal(Object.keys(positions).length, 100);
|
|
342
|
-
assert.isBelow(endTime - startTime, 2000); // Should complete quickly
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('should handle dense graphs', () => {
|
|
346
|
-
const graph = completeGraph(15);
|
|
347
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 50);
|
|
348
|
-
|
|
349
|
-
// Should spread nodes out reasonably
|
|
350
|
-
const minDist = getMinimumDistance(positions);
|
|
351
|
-
assert.isAbove(minDist, 0.01);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should handle path graphs', () => {
|
|
355
|
-
const path = {
|
|
356
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6, 7],
|
|
357
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const positions = fruchtermanReingoldLayout(path, null, null, null, 100);
|
|
361
|
-
|
|
362
|
-
// Path should stretch out somewhat
|
|
363
|
-
const startEnd = getDistance(positions, 0, 7);
|
|
364
|
-
const avgEdgeLength = getAverageEdgeLength(path, positions);
|
|
365
|
-
|
|
366
|
-
// End-to-end distance should be several times the average edge length
|
|
367
|
-
assert.isAbove(startEnd, avgEdgeLength * 3);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
describe('Layout quality', () => {
|
|
372
|
-
it('should minimize edge crossings for planar graphs', () => {
|
|
373
|
-
const graph = cycleGraph(10);
|
|
374
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 200);
|
|
375
|
-
|
|
376
|
-
// For a cycle, adjacent nodes should be among the closest
|
|
377
|
-
graph.nodes().forEach((node, i) => {
|
|
378
|
-
const nextNode = (i + 1) % 10;
|
|
379
|
-
const prevNode = (i + 9) % 10;
|
|
380
|
-
|
|
381
|
-
const distNext = getDistance(positions, node, nextNode);
|
|
382
|
-
const distPrev = getDistance(positions, node, prevNode);
|
|
383
|
-
|
|
384
|
-
// Check distances to all other nodes
|
|
385
|
-
const otherDists = [];
|
|
386
|
-
graph.nodes().forEach((other, j) => {
|
|
387
|
-
if (j !== i && j !== (i + 1) % 10 && j !== (i + 9) % 10) {
|
|
388
|
-
otherDists.push(getDistance(positions, node, other));
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const minOtherDist = Math.min(...otherDists);
|
|
393
|
-
|
|
394
|
-
// Adjacent nodes should generally be closer than non-adjacent
|
|
395
|
-
assert.isBelow(Math.min(distNext, distPrev), minOtherDist * 1.6);
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('should avoid node overlap', () => {
|
|
400
|
-
const graph = completeGraph(10);
|
|
401
|
-
const positions = fruchtermanReingoldLayout(graph, null, null, null, 100);
|
|
402
|
-
|
|
403
|
-
// Check minimum distance between nodes
|
|
404
|
-
const minDist = getMinimumDistance(positions);
|
|
405
|
-
|
|
406
|
-
// Nodes should be separated
|
|
407
|
-
assert.isAbove(minDist, 0.05);
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// Helper functions
|
|
413
|
-
function getLayoutSpan(positions) {
|
|
414
|
-
const nodes = Object.keys(positions);
|
|
415
|
-
let minX = Infinity, maxX = -Infinity;
|
|
416
|
-
let minY = Infinity, maxY = -Infinity;
|
|
417
|
-
|
|
418
|
-
nodes.forEach(node => {
|
|
419
|
-
minX = Math.min(minX, positions[node][0]);
|
|
420
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
421
|
-
minY = Math.min(minY, positions[node][1]);
|
|
422
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
width: maxX - minX,
|
|
427
|
-
height: maxY - minY
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function getCenterOfMass(positions) {
|
|
432
|
-
const nodes = Object.keys(positions);
|
|
433
|
-
const com = [0, 0];
|
|
434
|
-
|
|
435
|
-
nodes.forEach(node => {
|
|
436
|
-
com[0] += positions[node][0];
|
|
437
|
-
com[1] += positions[node][1];
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
com[0] /= nodes.length;
|
|
441
|
-
com[1] /= nodes.length;
|
|
442
|
-
return com;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function getDistance(positions, node1, node2, node1Alt?, node2Alt?) {
|
|
446
|
-
const n1 = node1Alt !== undefined ? node1Alt : node1;
|
|
447
|
-
const n2 = node2Alt !== undefined ? node2Alt : node2;
|
|
448
|
-
|
|
449
|
-
const dx = positions[n1][0] - positions[n2][0];
|
|
450
|
-
const dy = positions[n1][1] - positions[n2][1];
|
|
451
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function getMinimumDistance(positions) {
|
|
455
|
-
const nodes = Object.keys(positions);
|
|
456
|
-
let minDist = Infinity;
|
|
457
|
-
|
|
458
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
459
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
460
|
-
const dist = getDistance(positions, nodes[i], nodes[j]);
|
|
461
|
-
minDist = Math.min(minDist, dist);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return minDist;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function getAverageEdgeLength(graph, positions) {
|
|
469
|
-
const edges = graph.edges();
|
|
470
|
-
let totalLength = 0;
|
|
471
|
-
|
|
472
|
-
edges.forEach(edge => {
|
|
473
|
-
totalLength += getDistance(positions, edge[0], edge[1]);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
return totalLength / edges.length;
|
|
477
|
-
}
|