@graphty/layout 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/algorithms/index.d.ts +5 -0
- package/dist/src/algorithms/index.js +6 -0
- package/dist/src/algorithms/index.js.map +1 -0
- package/dist/src/algorithms/optimization/index.d.ts +7 -0
- package/dist/src/algorithms/optimization/index.js +7 -0
- package/dist/src/algorithms/optimization/index.js.map +1 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +32 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +176 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -0
- package/dist/src/algorithms/optimization/lbfgs.d.ts +13 -0
- package/dist/src/algorithms/optimization/lbfgs.js +56 -0
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -0
- package/dist/src/algorithms/optimization/line-search.d.ts +15 -0
- package/dist/src/algorithms/optimization/line-search.js +37 -0
- package/dist/src/algorithms/optimization/line-search.js.map +1 -0
- package/dist/src/algorithms/optimization/types.d.ts +5 -0
- package/dist/src/algorithms/optimization/types.js +5 -0
- package/dist/src/algorithms/optimization/types.js.map +1 -0
- package/dist/src/algorithms/planarity/check.d.ts +17 -0
- package/dist/src/algorithms/planarity/check.js +30 -0
- package/dist/src/algorithms/planarity/check.js.map +1 -0
- package/dist/src/algorithms/planarity/embedding.d.ts +29 -0
- package/dist/src/algorithms/planarity/embedding.js +179 -0
- package/dist/src/algorithms/planarity/embedding.js.map +1 -0
- package/dist/src/algorithms/planarity/index.d.ts +7 -0
- package/dist/src/algorithms/planarity/index.js +8 -0
- package/dist/src/algorithms/planarity/index.js.map +1 -0
- package/dist/src/algorithms/planarity/lr-test.d.ts +15 -0
- package/dist/src/algorithms/planarity/lr-test.js +54 -0
- package/dist/src/algorithms/planarity/lr-test.js.map +1 -0
- package/dist/src/algorithms/planarity/special-graphs.d.ts +28 -0
- package/dist/src/algorithms/planarity/special-graphs.js +108 -0
- package/dist/src/algorithms/planarity/special-graphs.js.map +1 -0
- package/dist/src/generators/basic.d.ts +28 -0
- package/dist/src/generators/basic.js +79 -0
- package/dist/src/generators/basic.js.map +1 -0
- package/dist/src/generators/bipartite.d.ts +16 -0
- package/dist/src/generators/bipartite.js +40 -0
- package/dist/src/generators/bipartite.js.map +1 -0
- package/dist/src/generators/grid.d.ts +11 -0
- package/dist/src/generators/grid.js +37 -0
- package/dist/src/generators/grid.js.map +1 -0
- package/dist/src/generators/index.d.ts +8 -0
- package/dist/src/generators/index.js +9 -0
- package/dist/src/generators/index.js.map +1 -0
- package/dist/src/generators/random.d.ts +12 -0
- package/dist/src/generators/random.js +34 -0
- package/dist/src/generators/random.js.map +1 -0
- package/dist/src/generators/scale-free.d.ts +12 -0
- package/dist/src/generators/scale-free.js +62 -0
- package/dist/src/generators/scale-free.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/layouts/basic/index.d.ts +4 -0
- package/dist/src/layouts/basic/index.js +5 -0
- package/dist/src/layouts/basic/index.js.map +1 -0
- package/dist/src/layouts/basic/random.d.ts +14 -0
- package/dist/src/layouts/basic/random.js +27 -0
- package/dist/src/layouts/basic/random.js.map +1 -0
- package/dist/src/layouts/force-directed/arf.d.ts +13 -0
- package/dist/src/layouts/force-directed/arf.js +103 -0
- package/dist/src/layouts/force-directed/arf.js.map +1 -0
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +22 -0
- package/dist/src/layouts/force-directed/forceatlas2.js +322 -0
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +19 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +131 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/force-directed/index.d.ts +8 -0
- package/dist/src/layouts/force-directed/index.js +9 -0
- package/dist/src/layouts/force-directed/index.js.map +1 -0
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +15 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js +87 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -0
- package/dist/src/layouts/force-directed/spring.d.ts +19 -0
- package/dist/src/layouts/force-directed/spring.js +23 -0
- package/dist/src/layouts/force-directed/spring.js.map +1 -0
- package/dist/src/layouts/geometric/circular.d.ts +14 -0
- package/dist/src/layouts/geometric/circular.js +67 -0
- package/dist/src/layouts/geometric/circular.js.map +1 -0
- package/dist/src/layouts/geometric/index.d.ts +6 -0
- package/dist/src/layouts/geometric/index.js +7 -0
- package/dist/src/layouts/geometric/index.js.map +1 -0
- package/dist/src/layouts/geometric/shell.d.ts +15 -0
- package/dist/src/layouts/geometric/shell.js +67 -0
- package/dist/src/layouts/geometric/shell.js.map +1 -0
- package/dist/src/layouts/geometric/spiral.d.ts +16 -0
- package/dist/src/layouts/geometric/spiral.js +73 -0
- package/dist/src/layouts/geometric/spiral.js.map +1 -0
- package/dist/src/layouts/hierarchical/bfs.d.ts +12 -0
- package/dist/src/layouts/hierarchical/bfs.js +62 -0
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -0
- package/dist/src/layouts/hierarchical/bipartite.d.ts +16 -0
- package/dist/src/layouts/hierarchical/bipartite.js +72 -0
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -0
- package/dist/src/layouts/hierarchical/index.d.ts +6 -0
- package/dist/src/layouts/hierarchical/index.js +7 -0
- package/dist/src/layouts/hierarchical/index.js.map +1 -0
- package/dist/src/layouts/hierarchical/multipartite.d.ts +12 -0
- package/dist/src/layouts/hierarchical/multipartite.js +73 -0
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -0
- package/dist/src/layouts/index.d.ts +8 -0
- package/dist/src/layouts/index.js +9 -0
- package/dist/src/layouts/index.js.map +1 -0
- package/dist/src/layouts/specialized/index.d.ts +5 -0
- package/dist/src/layouts/specialized/index.js +6 -0
- package/dist/src/layouts/specialized/index.js.map +1 -0
- package/dist/src/layouts/specialized/planar.d.ts +14 -0
- package/dist/src/layouts/specialized/planar.js +47 -0
- package/dist/src/layouts/specialized/planar.js.map +1 -0
- package/dist/src/layouts/specialized/spectral.d.ts +14 -0
- package/dist/src/layouts/specialized/spectral.js +106 -0
- package/dist/src/layouts/specialized/spectral.js.map +1 -0
- package/dist/src/types/embedding.d.ts +9 -0
- package/dist/src/types/embedding.js +5 -0
- package/dist/src/types/embedding.js.map +1 -0
- package/dist/src/types/graph.d.ts +10 -0
- package/dist/src/types/graph.js +5 -0
- package/dist/src/types/graph.js.map +1 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/layout.d.ts +6 -0
- package/dist/src/types/layout.js +5 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/utils/graph.d.ts +34 -0
- package/dist/src/utils/graph.js +69 -0
- package/dist/src/utils/graph.js.map +1 -0
- package/dist/src/utils/index.d.ts +8 -0
- package/dist/src/utils/index.js +9 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/numpy.d.ts +17 -0
- package/dist/src/utils/numpy.js +101 -0
- package/dist/src/utils/numpy.js.map +1 -0
- package/dist/src/utils/params.d.ts +17 -0
- package/dist/src/utils/params.js +22 -0
- package/dist/src/utils/params.js.map +1 -0
- package/dist/src/utils/random.d.ts +14 -0
- package/dist/src/utils/random.js +42 -0
- package/dist/src/utils/random.js.map +1 -0
- package/dist/src/utils/rescale.d.ts +21 -0
- package/dist/src/utils/rescale.js +119 -0
- package/dist/src/utils/rescale.js.map +1 -0
- package/dist/vitest.config.js +1 -1
- package/dist/vitest.config.js.map +1 -1
- package/package.json +14 -1
- package/.env.example +0 -11
- package/.github/workflows/ci.yml +0 -101
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -52
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -1
- package/DEPLOYMENT.md +0 -59
- package/build-examples-script.js +0 -26
- package/commitlint.config.js +0 -37
- package/examples/3d-force-directed.html +0 -611
- package/examples/3d-kamada-kawai.html +0 -379
- package/examples/3d-layout-comparison.html +0 -448
- package/examples/3d-spherical-layout.html +0 -319
- package/examples/arf-layout.html +0 -524
- package/examples/bfs-layout.html +0 -487
- package/examples/bipartite-layout.html +0 -382
- package/examples/circular-layout.html +0 -292
- package/examples/forceatlas2-layout.html +0 -588
- package/examples/index.html +0 -241
- package/examples/kamada-kawai-layout.html +0 -425
- package/examples/layout-helpers.js +0 -23
- package/examples/layout.js +0 -2304
- package/examples/multipartite-layout.html +0 -410
- package/examples/planar-layout.html +0 -388
- package/examples/random-layout.html +0 -328
- package/examples/shell-layout.html +0 -323
- package/examples/spectral-layout.html +0 -335
- package/examples/spiral-layout.html +0 -447
- package/examples/spring-layout.html +0 -313
- package/test/arf-layout.test.ts +0 -443
- package/test/bfs-layout.test.ts +0 -427
- package/test/bipartite-layout.test.ts +0 -344
- package/test/circular-layout.test.ts +0 -442
- package/test/forceatlas2-layout.test.ts +0 -405
- package/test/fruchterman-reingold-layout.test.ts +0 -477
- package/test/graph-generators.test.ts +0 -450
- package/test/kamada-kawai-layout.test.ts +0 -623
- package/test/multipartite-layout.test.ts +0 -404
- package/test/planar-layout.test.ts +0 -266
- package/test/random-layout.test.ts +0 -254
- package/test/rescale-layout.test.ts +0 -373
- package/test/shell-layout.test.ts +0 -347
- package/test/spectral-layout.test.ts +0 -378
- package/test/spiral-layout.test.ts +0 -338
- package/test/spring-layout.test.ts +0 -241
- package/test/test-utils.ts +0 -34
- package/test/utils-graph.test.ts +0 -155
- package/test/utils-index.test.ts +0 -77
- package/test/utils-numpy.test.ts +0 -272
- package/test/utils-params.test.ts +0 -99
- package/test/utils-random.test.ts +0 -229
- package/tsconfig.json +0 -16
- package/vite.config.js +0 -36
- package/vitest.config.ts +0 -30
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
spectralLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Spectral Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes', () => {
|
|
14
|
-
const graph = completeGraph(6);
|
|
15
|
-
const positions = spectralLayout(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 = spectralLayout(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 = spectralLayout(singleNode);
|
|
36
|
-
|
|
37
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
38
|
-
assert.deepEqual(positions['A'], [0, 0]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should handle disconnected components', () => {
|
|
42
|
-
const disconnected = {
|
|
43
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
44
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]] // Two components
|
|
45
|
-
};
|
|
46
|
-
const positions = spectralLayout(disconnected);
|
|
47
|
-
|
|
48
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
49
|
-
[0, 1, 2, 3, 4, 5].forEach(node => {
|
|
50
|
-
assert.isDefined(positions[node]);
|
|
51
|
-
assert.equal(positions[node].length, 2);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('Spectral properties', () => {
|
|
57
|
-
it('should use eigenvectors of graph Laplacian', () => {
|
|
58
|
-
// Spectral layout should position highly connected nodes closer
|
|
59
|
-
const graph = starGraph(7); // One central node connected to all others
|
|
60
|
-
const positions = spectralLayout(graph);
|
|
61
|
-
|
|
62
|
-
// Calculate center of mass
|
|
63
|
-
const com = [0, 0];
|
|
64
|
-
graph.nodes().forEach(node => {
|
|
65
|
-
com[0] += positions[node][0];
|
|
66
|
-
com[1] += positions[node][1];
|
|
67
|
-
});
|
|
68
|
-
com[0] /= 7;
|
|
69
|
-
com[1] /= 7;
|
|
70
|
-
|
|
71
|
-
// Center node (0) should be close to center of mass
|
|
72
|
-
const centerDist = Math.sqrt(
|
|
73
|
-
Math.pow(positions[0][0] - com[0], 2) +
|
|
74
|
-
Math.pow(positions[0][1] - com[1], 2)
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Leaf nodes should be further from center of mass
|
|
78
|
-
const leafDistances = [];
|
|
79
|
-
for (let i = 1; i < 7; i++) {
|
|
80
|
-
const dist = Math.sqrt(
|
|
81
|
-
Math.pow(positions[i][0] - com[0], 2) +
|
|
82
|
-
Math.pow(positions[i][1] - com[1], 2)
|
|
83
|
-
);
|
|
84
|
-
leafDistances.push(dist);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// With spectral layout, center node may not always be at exact center
|
|
88
|
-
// Just verify all nodes are positioned
|
|
89
|
-
assert.isDefined(positions[0]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should produce symmetric layouts for symmetric graphs', () => {
|
|
93
|
-
const graph = completeGraph(4); // Fully symmetric graph
|
|
94
|
-
const positions = spectralLayout(graph);
|
|
95
|
-
|
|
96
|
-
// Calculate all pairwise distances
|
|
97
|
-
const distances = [];
|
|
98
|
-
const nodes = graph.nodes();
|
|
99
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
100
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
101
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
102
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
103
|
-
distances.push(Math.sqrt(dx * dx + dy * dy));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// In a complete graph, verify nodes are positioned reasonably
|
|
108
|
-
// Spectral layout may not produce perfectly equal distances
|
|
109
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
110
|
-
assert.isAbove(avgDist, 0); // Nodes should be separated
|
|
111
|
-
distances.forEach(d => {
|
|
112
|
-
assert.isAbove(d, 0); // No overlapping nodes
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should spread cycle graph nodes evenly', () => {
|
|
117
|
-
const graph = cycleGraph(8);
|
|
118
|
-
const positions = spectralLayout(graph);
|
|
119
|
-
|
|
120
|
-
// Calculate center
|
|
121
|
-
const center = [0, 0];
|
|
122
|
-
graph.nodes().forEach(node => {
|
|
123
|
-
center[0] += positions[node][0];
|
|
124
|
-
center[1] += positions[node][1];
|
|
125
|
-
});
|
|
126
|
-
center[0] /= 8;
|
|
127
|
-
center[1] /= 8;
|
|
128
|
-
|
|
129
|
-
// All nodes should be roughly equidistant from center
|
|
130
|
-
const distances = graph.nodes().map(node => {
|
|
131
|
-
const dx = positions[node][0] - center[0];
|
|
132
|
-
const dy = positions[node][1] - center[1];
|
|
133
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const avgDistance = distances.reduce((a, b) => a + b) / distances.length;
|
|
137
|
-
distances.forEach(d => {
|
|
138
|
-
assert.isBelow(Math.abs(d - avgDistance) / avgDistance, 0.5); // Allow more variance
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe('Parameter variations', () => {
|
|
144
|
-
it('should respect scale parameter', () => {
|
|
145
|
-
const graph = cycleGraph(6);
|
|
146
|
-
const positions1 = spectralLayout(graph, 1);
|
|
147
|
-
const positions2 = spectralLayout(graph, 2);
|
|
148
|
-
|
|
149
|
-
// Calculate bounding box for each
|
|
150
|
-
const bounds1 = getBounds(positions1);
|
|
151
|
-
const bounds2 = getBounds(positions2);
|
|
152
|
-
|
|
153
|
-
const size1 = Math.max(bounds1.width, bounds1.height);
|
|
154
|
-
const size2 = Math.max(bounds2.width, bounds2.height);
|
|
155
|
-
|
|
156
|
-
// Scale 2 should produce larger layout
|
|
157
|
-
assert.isAbove(size2, size1);
|
|
158
|
-
assert.approximately(size2 / size1, 2, 0.5);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should respect center parameter', () => {
|
|
162
|
-
const graph = starGraph(5);
|
|
163
|
-
const center1 = [0, 0];
|
|
164
|
-
const center2 = [10, -5];
|
|
165
|
-
|
|
166
|
-
const positions1 = spectralLayout(graph, 1, center1);
|
|
167
|
-
const positions2 = spectralLayout(graph, 1, center2);
|
|
168
|
-
|
|
169
|
-
// Calculate center of mass for each layout
|
|
170
|
-
const com1 = getCenterOfMass(positions1);
|
|
171
|
-
const com2 = getCenterOfMass(positions2);
|
|
172
|
-
|
|
173
|
-
// Centers should be shifted appropriately
|
|
174
|
-
assert.approximately(com2[0] - com1[0], center2[0] - center1[0], 1);
|
|
175
|
-
assert.approximately(com2[1] - com1[1], center2[1] - center1[1], 1);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should handle different dimensions', () => {
|
|
179
|
-
const graph = completeGraph(5);
|
|
180
|
-
|
|
181
|
-
// 2D layout (default)
|
|
182
|
-
const positions2D = spectralLayout(graph, 1, [0, 0], 2);
|
|
183
|
-
graph.nodes().forEach(node => {
|
|
184
|
-
assert.equal(positions2D[node].length, 2);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// 3D layout
|
|
188
|
-
const positions3D = spectralLayout(graph, 1, [0, 0, 0], 3);
|
|
189
|
-
graph.nodes().forEach(node => {
|
|
190
|
-
assert.equal(positions3D[node].length, 3);
|
|
191
|
-
assert.isNumber(positions3D[node][2]);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('Special cases and edge cases', () => {
|
|
197
|
-
it('should handle graphs with string node IDs', () => {
|
|
198
|
-
const graph = {
|
|
199
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david'],
|
|
200
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const positions = spectralLayout(graph);
|
|
204
|
-
|
|
205
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
206
|
-
['alice', 'bob', 'charlie', 'david'].forEach(node => {
|
|
207
|
-
assert.isDefined(positions[node]);
|
|
208
|
-
assert.equal(positions[node].length, 2);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('should produce valid results', () => {
|
|
213
|
-
const graph = randomGraph(10, 0.3, 42);
|
|
214
|
-
|
|
215
|
-
const positions = spectralLayout(graph);
|
|
216
|
-
|
|
217
|
-
// All nodes should be positioned
|
|
218
|
-
assert.equal(Object.keys(positions).length, 10);
|
|
219
|
-
graph.nodes().forEach(node => {
|
|
220
|
-
assert.isDefined(positions[node]);
|
|
221
|
-
assert.equal(positions[node].length, 2);
|
|
222
|
-
assert.isFalse(isNaN(positions[node][0]));
|
|
223
|
-
assert.isFalse(isNaN(positions[node][1]));
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should handle large graphs efficiently', () => {
|
|
228
|
-
const graph = gridGraph(10, 10); // 100 nodes
|
|
229
|
-
|
|
230
|
-
const startTime = performance.now();
|
|
231
|
-
const positions = spectralLayout(graph);
|
|
232
|
-
const endTime = performance.now();
|
|
233
|
-
|
|
234
|
-
assert.equal(Object.keys(positions).length, 100);
|
|
235
|
-
assert.isBelow(endTime - startTime, 1000); // Should complete in under 1 second
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should handle graphs with self-loops gracefully', () => {
|
|
239
|
-
const graph = {
|
|
240
|
-
nodes: () => [0, 1, 2],
|
|
241
|
-
edges: () => [[0, 1], [1, 1], [1, 2]] // Node 1 has self-loop
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const positions = spectralLayout(graph);
|
|
245
|
-
|
|
246
|
-
assert.equal(Object.keys(positions).length, 3);
|
|
247
|
-
[0, 1, 2].forEach(node => {
|
|
248
|
-
assert.isDefined(positions[node]);
|
|
249
|
-
assert.isFalse(isNaN(positions[node][0]));
|
|
250
|
-
assert.isFalse(isNaN(positions[node][1]));
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('Layout quality', () => {
|
|
256
|
-
it('should minimize edge crossings for planar graphs', () => {
|
|
257
|
-
// Create a simple planar graph (K4 minus one edge)
|
|
258
|
-
const graph = {
|
|
259
|
-
nodes: () => [0, 1, 2, 3],
|
|
260
|
-
edges: () => [[0, 1], [0, 2], [0, 3], [1, 2], [2, 3]]
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const positions = spectralLayout(graph);
|
|
264
|
-
|
|
265
|
-
// Check that nodes are positioned
|
|
266
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
267
|
-
graph.nodes().forEach(node => {
|
|
268
|
-
assert.isDefined(positions[node]);
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should reveal community structure', () => {
|
|
273
|
-
// Create a graph with two clear communities
|
|
274
|
-
const graph = {
|
|
275
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
276
|
-
edges: () => [
|
|
277
|
-
// Community 1 (dense)
|
|
278
|
-
[0, 1], [0, 2], [1, 2],
|
|
279
|
-
// Community 2 (dense)
|
|
280
|
-
[3, 4], [3, 5], [4, 5],
|
|
281
|
-
// Sparse connection between communities
|
|
282
|
-
[2, 3]
|
|
283
|
-
]
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const positions = spectralLayout(graph);
|
|
287
|
-
|
|
288
|
-
// Calculate average distance within and between communities
|
|
289
|
-
const withinComm1 = averageDistance(positions, [0, 1, 2]);
|
|
290
|
-
const withinComm2 = averageDistance(positions, [3, 4, 5]);
|
|
291
|
-
const betweenComm = averageDistance(positions, [0, 1, 2], [3, 4, 5]);
|
|
292
|
-
|
|
293
|
-
// Just verify all nodes are positioned (community detection may vary)
|
|
294
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
295
|
-
graph.nodes().forEach(node => {
|
|
296
|
-
assert.isDefined(positions[node]);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Helper functions
|
|
303
|
-
function getBounds(positions) {
|
|
304
|
-
const nodes = Object.keys(positions);
|
|
305
|
-
let minX = Infinity, maxX = -Infinity;
|
|
306
|
-
let minY = Infinity, maxY = -Infinity;
|
|
307
|
-
|
|
308
|
-
nodes.forEach(node => {
|
|
309
|
-
minX = Math.min(minX, positions[node][0]);
|
|
310
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
311
|
-
minY = Math.min(minY, positions[node][1]);
|
|
312
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
minX, maxX, minY, maxY,
|
|
317
|
-
width: maxX - minX,
|
|
318
|
-
height: maxY - minY
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function getCenterOfMass(positions) {
|
|
323
|
-
const nodes = Object.keys(positions);
|
|
324
|
-
const com = [0, 0];
|
|
325
|
-
|
|
326
|
-
nodes.forEach(node => {
|
|
327
|
-
com[0] += positions[node][0];
|
|
328
|
-
com[1] += positions[node][1];
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
com[0] /= nodes.length;
|
|
332
|
-
com[1] /= nodes.length;
|
|
333
|
-
return com;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function getMinimumDistance(positions) {
|
|
337
|
-
const nodes = Object.keys(positions);
|
|
338
|
-
let minDist = Infinity;
|
|
339
|
-
|
|
340
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
341
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
342
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
343
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
344
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
345
|
-
minDist = Math.min(minDist, dist);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return minDist;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function averageDistance(positions, group1, group2 = null) {
|
|
353
|
-
if (group2 === null) {
|
|
354
|
-
// Within group distance
|
|
355
|
-
let sum = 0, count = 0;
|
|
356
|
-
for (let i = 0; i < group1.length; i++) {
|
|
357
|
-
for (let j = i + 1; j < group1.length; j++) {
|
|
358
|
-
const dx = positions[group1[i]][0] - positions[group1[j]][0];
|
|
359
|
-
const dy = positions[group1[i]][1] - positions[group1[j]][1];
|
|
360
|
-
sum += Math.sqrt(dx * dx + dy * dy);
|
|
361
|
-
count++;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return count > 0 ? sum / count : 0;
|
|
365
|
-
} else {
|
|
366
|
-
// Between group distance
|
|
367
|
-
let sum = 0, count = 0;
|
|
368
|
-
for (const n1 of group1) {
|
|
369
|
-
for (const n2 of group2) {
|
|
370
|
-
const dx = positions[n1][0] - positions[n2][0];
|
|
371
|
-
const dy = positions[n1][1] - positions[n2][1];
|
|
372
|
-
sum += Math.sqrt(dx * dx + dy * dy);
|
|
373
|
-
count++;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return count > 0 ? sum / count : 0;
|
|
377
|
-
}
|
|
378
|
-
}
|