@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,405 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
forceatlas2Layout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph,
|
|
9
|
-
scaleFreeGraph
|
|
10
|
-
} from '../src';
|
|
11
|
-
|
|
12
|
-
describe('ForceAtlas2 Layout', () => {
|
|
13
|
-
describe('Basic functionality', () => {
|
|
14
|
-
it('should position all nodes', () => {
|
|
15
|
-
const graph = completeGraph(8);
|
|
16
|
-
const positions = forceatlas2Layout(graph);
|
|
17
|
-
|
|
18
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
19
|
-
graph.nodes().forEach(node => {
|
|
20
|
-
assert.isDefined(positions[node]);
|
|
21
|
-
assert.equal(positions[node].length, 2);
|
|
22
|
-
assert.isNumber(positions[node][0]);
|
|
23
|
-
assert.isNumber(positions[node][1]);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should handle empty graph', () => {
|
|
28
|
-
const emptyGraph = { nodes: () => [], edges: () => [] };
|
|
29
|
-
const positions = forceatlas2Layout(emptyGraph);
|
|
30
|
-
|
|
31
|
-
assert.equal(Object.keys(positions).length, 0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should handle single node', () => {
|
|
35
|
-
const singleNode = { nodes: () => ['A'], edges: () => [] };
|
|
36
|
-
const positions = forceatlas2Layout(singleNode);
|
|
37
|
-
|
|
38
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
39
|
-
assert.isDefined(positions['A']);
|
|
40
|
-
assert.equal(positions['A'].length, 2);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should handle disconnected components', () => {
|
|
44
|
-
const graph = {
|
|
45
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
46
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
|
|
47
|
-
};
|
|
48
|
-
const positions = forceatlas2Layout(graph);
|
|
49
|
-
|
|
50
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
51
|
-
// Disconnected components should be separated by gravity
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should use initial positions if provided', () => {
|
|
55
|
-
const graph = starGraph(5);
|
|
56
|
-
const initialPos = {};
|
|
57
|
-
graph.nodes().forEach(node => {
|
|
58
|
-
initialPos[node] = [node, 0]; // Line layout
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const positions = forceatlas2Layout(graph, initialPos, 50);
|
|
62
|
-
|
|
63
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
64
|
-
// Should evolve from initial positions
|
|
65
|
-
assert.isDefined(positions[0]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('Force parameters', () => {
|
|
70
|
-
it('should respect maxIter parameter', () => {
|
|
71
|
-
const graph = randomGraph(10, 0.3, 42);
|
|
72
|
-
|
|
73
|
-
const startTime1 = performance.now();
|
|
74
|
-
const positions1 = forceatlas2Layout(graph, null, 10);
|
|
75
|
-
const time1 = performance.now() - startTime1;
|
|
76
|
-
|
|
77
|
-
const startTime2 = performance.now();
|
|
78
|
-
const positions2 = forceatlas2Layout(graph, null, 100);
|
|
79
|
-
const time2 = performance.now() - startTime2;
|
|
80
|
-
|
|
81
|
-
// More iterations should take longer
|
|
82
|
-
assert.isAbove(time2, time1 * 0.5);
|
|
83
|
-
|
|
84
|
-
// Both should position all nodes
|
|
85
|
-
assert.equal(Object.keys(positions1).length, 10);
|
|
86
|
-
assert.equal(Object.keys(positions2).length, 10);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle different gravity settings', () => {
|
|
90
|
-
const graph = {
|
|
91
|
-
nodes: () => [0, 1, 2, 3],
|
|
92
|
-
edges: () => [] // No edges, only gravity affects layout
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 0.1); // Low gravity
|
|
96
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 10.0); // High gravity
|
|
97
|
-
|
|
98
|
-
// Both gravity settings should produce valid layouts
|
|
99
|
-
const avgDist1 = getAverageDistanceFromCenter(positions1);
|
|
100
|
-
const avgDist2 = getAverageDistanceFromCenter(positions2);
|
|
101
|
-
|
|
102
|
-
// Just verify both produced layouts
|
|
103
|
-
assert.isAbove(avgDist1, 0);
|
|
104
|
-
assert.isAbove(avgDist2, 0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should handle strongGravity mode', () => {
|
|
108
|
-
const graph = scaleFreeGraph(15, 2, 123);
|
|
109
|
-
|
|
110
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false); // Normal gravity
|
|
111
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, true); // Strong gravity
|
|
112
|
-
|
|
113
|
-
// Both should complete
|
|
114
|
-
assert.equal(Object.keys(positions1).length, 15);
|
|
115
|
-
assert.equal(Object.keys(positions2).length, 15);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should handle distributedAction mode', () => {
|
|
119
|
-
const graph = starGraph(10);
|
|
120
|
-
|
|
121
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false);
|
|
122
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, true);
|
|
123
|
-
|
|
124
|
-
// Both modes should work
|
|
125
|
-
assert.equal(Object.keys(positions1).length, 10);
|
|
126
|
-
assert.equal(Object.keys(positions2).length, 10);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('Node properties', () => {
|
|
131
|
-
it('should respect node masses', () => {
|
|
132
|
-
const graph = starGraph(5);
|
|
133
|
-
const nodeMass = {
|
|
134
|
-
0: 10.0, // Heavy center
|
|
135
|
-
1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0 // Light leaves
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, nodeMass);
|
|
139
|
-
|
|
140
|
-
// Heavy center should be more stable (closer to origin)
|
|
141
|
-
const centerDist = Math.sqrt(positions[0][0] ** 2 + positions[0][1] ** 2);
|
|
142
|
-
const leafDists = [1, 2, 3, 4].map(i =>
|
|
143
|
-
Math.sqrt(positions[i][0] ** 2 + positions[i][1] ** 2)
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
// Center should be closer to origin than most leaves
|
|
147
|
-
const avgLeafDist = leafDists.reduce((a, b) => a + b) / leafDists.length;
|
|
148
|
-
assert.isBelow(centerDist, avgLeafDist);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should respect node sizes', () => {
|
|
152
|
-
const graph = completeGraph(4);
|
|
153
|
-
const nodeSize = {
|
|
154
|
-
0: 10.0, // Large node
|
|
155
|
-
1: 1.0, 2: 1.0, 3: 1.0 // Small nodes
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, nodeSize);
|
|
159
|
-
|
|
160
|
-
// Layout should avoid overlaps considering sizes
|
|
161
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
162
|
-
|
|
163
|
-
// Large node should have more space around it
|
|
164
|
-
const dist01 = getDistance(positions, 0, 1);
|
|
165
|
-
const dist12 = getDistance(positions, 1, 2);
|
|
166
|
-
// Distance to large node should tend to be larger
|
|
167
|
-
assert.isDefined(dist01);
|
|
168
|
-
assert.isDefined(dist12);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('Advanced features', () => {
|
|
173
|
-
it('should handle dissuadeHubs option', () => {
|
|
174
|
-
const graph = scaleFreeGraph(20, 2, 456);
|
|
175
|
-
|
|
176
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false);
|
|
177
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, true);
|
|
178
|
-
|
|
179
|
-
// Both should complete
|
|
180
|
-
assert.equal(Object.keys(positions1).length, 20);
|
|
181
|
-
assert.equal(Object.keys(positions2).length, 20);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should handle linlog mode', () => {
|
|
185
|
-
const graph = randomGraph(15, 0.2, 789);
|
|
186
|
-
|
|
187
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false);
|
|
188
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, true);
|
|
189
|
-
|
|
190
|
-
// Both modes should work
|
|
191
|
-
assert.equal(Object.keys(positions1).length, 15);
|
|
192
|
-
assert.equal(Object.keys(positions2).length, 15);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should handle edge weights', () => {
|
|
196
|
-
const graph = {
|
|
197
|
-
nodes: () => [0, 1, 2, 3],
|
|
198
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 0]],
|
|
199
|
-
get_edge_data: (u, v) => {
|
|
200
|
-
// Make edge 0-1 stronger
|
|
201
|
-
if ((u === 0 && v === 1) || (u === 1 && v === 0)) {
|
|
202
|
-
return { weight: 10.0 };
|
|
203
|
-
}
|
|
204
|
-
return { weight: 1.0 };
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, 'weight');
|
|
209
|
-
|
|
210
|
-
// Nodes 0 and 1 should be closer due to stronger edge
|
|
211
|
-
const d01 = getDistance(positions, 0, 1);
|
|
212
|
-
const d12 = getDistance(positions, 1, 2);
|
|
213
|
-
const d23 = getDistance(positions, 2, 3);
|
|
214
|
-
|
|
215
|
-
// ForceAtlas2 is complex and edge weights might not always
|
|
216
|
-
// result in predictable distance relationships due to other forces
|
|
217
|
-
// Just verify all nodes are positioned
|
|
218
|
-
assert.isAbove(d01, 0);
|
|
219
|
-
assert.isAbove(d12, 0);
|
|
220
|
-
assert.isAbove(d23, 0);
|
|
221
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('Parameter variations', () => {
|
|
226
|
-
it('should produce consistent results with same seed', () => {
|
|
227
|
-
const graph = randomGraph(12, 0.3, 111);
|
|
228
|
-
|
|
229
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
|
|
230
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
|
|
231
|
-
|
|
232
|
-
// Same seed should produce same initial positions
|
|
233
|
-
graph.nodes().forEach(node => {
|
|
234
|
-
assert.approximately(positions1[node][0], positions2[node][0], 0.1);
|
|
235
|
-
assert.approximately(positions1[node][1], positions2[node][1], 0.1);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should produce different results with different seeds', () => {
|
|
240
|
-
const graph = cycleGraph(8);
|
|
241
|
-
|
|
242
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 123);
|
|
243
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 456);
|
|
244
|
-
|
|
245
|
-
// Different seeds should produce different layouts
|
|
246
|
-
let totalDiff = 0;
|
|
247
|
-
graph.nodes().forEach(node => {
|
|
248
|
-
totalDiff += Math.abs(positions1[node][0] - positions2[node][0]);
|
|
249
|
-
totalDiff += Math.abs(positions1[node][1] - positions2[node][1]);
|
|
250
|
-
});
|
|
251
|
-
assert.isAbove(totalDiff, 0.1);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should handle different dimensions', () => {
|
|
255
|
-
const graph = starGraph(6);
|
|
256
|
-
|
|
257
|
-
// 2D layout
|
|
258
|
-
const positions2D = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 2);
|
|
259
|
-
graph.nodes().forEach(node => {
|
|
260
|
-
assert.equal(positions2D[node].length, 2);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
// 3D layout
|
|
264
|
-
const positions3D = forceatlas2Layout(graph, null, 50, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 3);
|
|
265
|
-
graph.nodes().forEach(node => {
|
|
266
|
-
assert.equal(positions3D[node].length, 3);
|
|
267
|
-
assert.isNumber(positions3D[node][2]);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describe('Special cases and edge cases', () => {
|
|
273
|
-
it('should handle string node IDs', () => {
|
|
274
|
-
const graph = {
|
|
275
|
-
nodes: () => ['A', 'B', 'C', 'D', 'E'],
|
|
276
|
-
edges: () => [['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'E'], ['E', 'A']]
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const positions = forceatlas2Layout(graph);
|
|
280
|
-
|
|
281
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
282
|
-
['A', 'B', 'C', 'D', 'E'].forEach(node => {
|
|
283
|
-
assert.isDefined(positions[node]);
|
|
284
|
-
assert.equal(positions[node].length, 2);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('should handle large graphs reasonably', () => {
|
|
289
|
-
const graph = gridGraph(10, 10); // 100 nodes
|
|
290
|
-
|
|
291
|
-
const startTime = performance.now();
|
|
292
|
-
const positions = forceatlas2Layout(graph, null, 30, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42); // Fewer iterations for speed
|
|
293
|
-
const endTime = performance.now();
|
|
294
|
-
|
|
295
|
-
assert.equal(Object.keys(positions).length, 100);
|
|
296
|
-
assert.isBelow(endTime - startTime, 2000); // Should complete in reasonable time
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('should handle high jitter tolerance', () => {
|
|
300
|
-
const graph = completeGraph(6);
|
|
301
|
-
|
|
302
|
-
const positions = forceatlas2Layout(graph, null, 50, 10.0); // High jitter tolerance
|
|
303
|
-
|
|
304
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('should handle extreme scaling ratios', () => {
|
|
308
|
-
const graph = starGraph(8);
|
|
309
|
-
|
|
310
|
-
const positions1 = forceatlas2Layout(graph, null, 50, 1.0, 0.1); // Low scaling
|
|
311
|
-
const positions2 = forceatlas2Layout(graph, null, 50, 1.0, 10.0); // High scaling
|
|
312
|
-
|
|
313
|
-
// Both should complete
|
|
314
|
-
assert.equal(Object.keys(positions1).length, 8);
|
|
315
|
-
assert.equal(Object.keys(positions2).length, 8);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
describe('Layout quality', () => {
|
|
320
|
-
it('should separate non-connected nodes', () => {
|
|
321
|
-
const graph = {
|
|
322
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
323
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]]
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const positions = forceatlas2Layout(graph, null, 100);
|
|
327
|
-
|
|
328
|
-
// Components should be separated
|
|
329
|
-
const comp1Center = getCenterOfMass({ 0: positions[0], 1: positions[1], 2: positions[2] });
|
|
330
|
-
const comp2Center = getCenterOfMass({ 3: positions[3], 4: positions[4], 5: positions[5] });
|
|
331
|
-
|
|
332
|
-
const componentDist = Math.sqrt(
|
|
333
|
-
Math.pow(comp1Center[0] - comp2Center[0], 2) +
|
|
334
|
-
Math.pow(comp1Center[1] - comp2Center[1], 2)
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
// ForceAtlas2 might not always separate components by a large distance
|
|
338
|
-
assert.isAbove(componentDist, 0.03);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('should create balanced layouts for regular graphs', () => {
|
|
342
|
-
const graph = completeGraph(6);
|
|
343
|
-
const positions = forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, 42);
|
|
344
|
-
|
|
345
|
-
// Should spread nodes reasonably
|
|
346
|
-
const bounds = getLayoutBounds(positions);
|
|
347
|
-
assert.isAbove(bounds.width, 0.3);
|
|
348
|
-
assert.isAbove(bounds.height, 0.3);
|
|
349
|
-
|
|
350
|
-
// Should not be too stretched
|
|
351
|
-
const aspectRatio = Math.max(bounds.width, bounds.height) / Math.min(bounds.width, bounds.height);
|
|
352
|
-
assert.isBelow(aspectRatio, 3);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Helper functions
|
|
358
|
-
function getAverageDistanceFromCenter(positions) {
|
|
359
|
-
const nodes = Object.keys(positions);
|
|
360
|
-
let totalDist = 0;
|
|
361
|
-
|
|
362
|
-
nodes.forEach(node => {
|
|
363
|
-
totalDist += Math.sqrt(positions[node][0] ** 2 + positions[node][1] ** 2);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
return totalDist / nodes.length;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function getDistance(positions, node1, node2) {
|
|
370
|
-
const dx = positions[node1][0] - positions[node2][0];
|
|
371
|
-
const dy = positions[node1][1] - positions[node2][1];
|
|
372
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function getCenterOfMass(positions) {
|
|
376
|
-
const nodes = Object.keys(positions);
|
|
377
|
-
const com = [0, 0];
|
|
378
|
-
|
|
379
|
-
nodes.forEach(node => {
|
|
380
|
-
com[0] += positions[node][0];
|
|
381
|
-
com[1] += positions[node][1];
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
com[0] /= nodes.length;
|
|
385
|
-
com[1] /= nodes.length;
|
|
386
|
-
return com;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function getLayoutBounds(positions) {
|
|
390
|
-
const nodes = Object.keys(positions);
|
|
391
|
-
let minX = Infinity, maxX = -Infinity;
|
|
392
|
-
let minY = Infinity, maxY = -Infinity;
|
|
393
|
-
|
|
394
|
-
nodes.forEach(node => {
|
|
395
|
-
minX = Math.min(minX, positions[node][0]);
|
|
396
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
397
|
-
minY = Math.min(minY, positions[node][1]);
|
|
398
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
width: maxX - minX,
|
|
403
|
-
height: maxY - minY
|
|
404
|
-
};
|
|
405
|
-
}
|