@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,623 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
kamadaKawaiLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
randomGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Kamada-Kawai Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes', () => {
|
|
14
|
-
const graph = completeGraph(6);
|
|
15
|
-
const positions = kamadaKawaiLayout(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 = kamadaKawaiLayout(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 = kamadaKawaiLayout(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 = kamadaKawaiLayout(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('Distance matrix and weights', () => {
|
|
57
|
-
it('should accept custom distance matrix', () => {
|
|
58
|
-
const graph = completeGraph(4);
|
|
59
|
-
|
|
60
|
-
// Create custom distance matrix
|
|
61
|
-
const dist = {};
|
|
62
|
-
graph.nodes().forEach(u => {
|
|
63
|
-
dist[u] = {};
|
|
64
|
-
graph.nodes().forEach(v => {
|
|
65
|
-
dist[u][v] = u === v ? 0 : 1; // All pairs at distance 1
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const positions = kamadaKawaiLayout(graph, dist);
|
|
70
|
-
|
|
71
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should handle weighted graphs', () => {
|
|
75
|
-
const graph = {
|
|
76
|
-
nodes: () => [0, 1, 2, 3],
|
|
77
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 0]],
|
|
78
|
-
// Mock edge weight access
|
|
79
|
-
get_edge_data: (u, v) => ({ weight: 1 })
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight');
|
|
83
|
-
|
|
84
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should use initial positions if provided', () => {
|
|
88
|
-
const graph = cycleGraph(5);
|
|
89
|
-
const initialPos = {};
|
|
90
|
-
graph.nodes().forEach((node, i) => {
|
|
91
|
-
initialPos[node] = [i, 0]; // Line layout
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const positions = kamadaKawaiLayout(graph, null, initialPos);
|
|
95
|
-
|
|
96
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
97
|
-
// Should produce different layout than initial
|
|
98
|
-
let different = false;
|
|
99
|
-
graph.nodes().forEach(node => {
|
|
100
|
-
if (positions[node][0] !== initialPos[node][0] ||
|
|
101
|
-
positions[node][1] !== initialPos[node][1]) {
|
|
102
|
-
different = true;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
assert.isTrue(different);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('Parameter variations', () => {
|
|
110
|
-
it('should respect scale parameter', () => {
|
|
111
|
-
const graph = starGraph(6);
|
|
112
|
-
|
|
113
|
-
const positions1 = kamadaKawaiLayout(graph, null, null, 'weight', 1);
|
|
114
|
-
const positions2 = kamadaKawaiLayout(graph, null, null, 'weight', 2);
|
|
115
|
-
|
|
116
|
-
// Calculate spans
|
|
117
|
-
const span1 = getLayoutSpan(positions1);
|
|
118
|
-
const span2 = getLayoutSpan(positions2);
|
|
119
|
-
|
|
120
|
-
assert.approximately(span2.width / span1.width, 2, 0.1);
|
|
121
|
-
assert.approximately(span2.height / span1.height, 2, 0.1);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should respect center parameter', () => {
|
|
125
|
-
const graph = cycleGraph(6);
|
|
126
|
-
const center = [10, -5];
|
|
127
|
-
|
|
128
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, center);
|
|
129
|
-
|
|
130
|
-
// Calculate center of mass
|
|
131
|
-
const com = getCenterOfMass(positions);
|
|
132
|
-
|
|
133
|
-
assert.approximately(com[0], center[0], 1);
|
|
134
|
-
assert.approximately(com[1], center[1], 1);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should handle different dimensions', () => {
|
|
138
|
-
const graph = completeGraph(4);
|
|
139
|
-
|
|
140
|
-
// 2D layout
|
|
141
|
-
const positions2D = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0], 2);
|
|
142
|
-
graph.nodes().forEach(node => {
|
|
143
|
-
assert.equal(positions2D[node].length, 2);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// 3D layout
|
|
147
|
-
const positions3D = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
148
|
-
graph.nodes().forEach(node => {
|
|
149
|
-
assert.equal(positions3D[node].length, 3);
|
|
150
|
-
assert.isNumber(positions3D[node][2]);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('Layout properties', () => {
|
|
156
|
-
it('should minimize stress for regular graphs', () => {
|
|
157
|
-
const graph = cycleGraph(8);
|
|
158
|
-
const positions = kamadaKawaiLayout(graph);
|
|
159
|
-
|
|
160
|
-
// In a cycle, adjacent nodes should be closer than non-adjacent
|
|
161
|
-
const distances = [];
|
|
162
|
-
graph.nodes().forEach((node, i) => {
|
|
163
|
-
const nextNode = (i + 1) % 8;
|
|
164
|
-
const dx = positions[node][0] - positions[nextNode][0];
|
|
165
|
-
const dy = positions[node][1] - positions[nextNode][1];
|
|
166
|
-
distances.push(Math.sqrt(dx * dx + dy * dy));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// All edge lengths should be similar
|
|
170
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
171
|
-
distances.forEach(d => {
|
|
172
|
-
assert.approximately(d, avgDist, avgDist * 0.3);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should create symmetric layouts for symmetric graphs', () => {
|
|
177
|
-
const graph = completeGraph(5);
|
|
178
|
-
const positions = kamadaKawaiLayout(graph);
|
|
179
|
-
|
|
180
|
-
// Calculate all pairwise distances
|
|
181
|
-
const distances = [];
|
|
182
|
-
const nodes = graph.nodes();
|
|
183
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
184
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
185
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
186
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
187
|
-
distances.push(Math.sqrt(dx * dx + dy * dy));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// In complete graph, all distances should be similar
|
|
192
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
193
|
-
let maxDeviation = 0;
|
|
194
|
-
distances.forEach(d => {
|
|
195
|
-
maxDeviation = Math.max(maxDeviation, Math.abs(d - avgDist) / avgDist);
|
|
196
|
-
});
|
|
197
|
-
assert.isBelow(maxDeviation, 0.5);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('Special cases and edge cases', () => {
|
|
202
|
-
it('should handle string node IDs', () => {
|
|
203
|
-
const graph = {
|
|
204
|
-
nodes: () => ['alice', 'bob', 'charlie', 'david'],
|
|
205
|
-
edges: () => [['alice', 'bob'], ['bob', 'charlie'], ['charlie', 'david'], ['david', 'alice']]
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const positions = kamadaKawaiLayout(graph);
|
|
209
|
-
|
|
210
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
211
|
-
['alice', 'bob', 'charlie', 'david'].forEach(node => {
|
|
212
|
-
assert.isDefined(positions[node]);
|
|
213
|
-
assert.equal(positions[node].length, 2);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should handle large graphs reasonably', () => {
|
|
218
|
-
const graph = gridGraph(8, 8); // 64 nodes
|
|
219
|
-
|
|
220
|
-
const startTime = performance.now();
|
|
221
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0], 2);
|
|
222
|
-
const endTime = performance.now();
|
|
223
|
-
|
|
224
|
-
assert.equal(Object.keys(positions).length, 64);
|
|
225
|
-
assert.isBelow(endTime - startTime, 2000); // Should complete in reasonable time
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should produce different results with different initial positions', () => {
|
|
229
|
-
const graph = randomGraph(10, 0.3, 42);
|
|
230
|
-
|
|
231
|
-
// First with default initial positions
|
|
232
|
-
const positions1 = kamadaKawaiLayout(graph);
|
|
233
|
-
|
|
234
|
-
// Then with custom initial positions
|
|
235
|
-
const customPos = {};
|
|
236
|
-
graph.nodes().forEach((node, i) => {
|
|
237
|
-
customPos[node] = [Math.cos(2 * Math.PI * i / 10), Math.sin(2 * Math.PI * i / 10)];
|
|
238
|
-
});
|
|
239
|
-
const positions2 = kamadaKawaiLayout(graph, null, customPos);
|
|
240
|
-
|
|
241
|
-
// Results might be different (though both should be valid)
|
|
242
|
-
let different = false;
|
|
243
|
-
graph.nodes().forEach(node => {
|
|
244
|
-
if (Math.abs(positions1[node][0] - positions2[node][0]) > 0.1 ||
|
|
245
|
-
Math.abs(positions1[node][1] - positions2[node][1]) > 0.1) {
|
|
246
|
-
different = true;
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
// Note: They might converge to same solution, so we don't assert different
|
|
250
|
-
assert.equal(Object.keys(positions2).length, 10);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should handle star graph well', () => {
|
|
254
|
-
const graph = starGraph(10);
|
|
255
|
-
const positions = kamadaKawaiLayout(graph);
|
|
256
|
-
|
|
257
|
-
// Center should be distinguishable from leaves
|
|
258
|
-
const centerPos = positions[0];
|
|
259
|
-
const leafDistances = [];
|
|
260
|
-
|
|
261
|
-
for (let i = 1; i < 10; i++) {
|
|
262
|
-
const dx = positions[i][0] - centerPos[0];
|
|
263
|
-
const dy = positions[i][1] - centerPos[1];
|
|
264
|
-
leafDistances.push(Math.sqrt(dx * dx + dy * dy));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// All leaves should be positioned at reasonable distances
|
|
268
|
-
const avgDist = leafDistances.reduce((a, b) => a + b) / leafDistances.length;
|
|
269
|
-
leafDistances.forEach(d => {
|
|
270
|
-
assert.isAbove(d, 0); // All at positive distance
|
|
271
|
-
assert.isBelow(d, avgDist * 2); // Not too far
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe('Layout quality', () => {
|
|
277
|
-
it('should preserve graph distances', () => {
|
|
278
|
-
// For a path graph, layout distances should reflect graph distances
|
|
279
|
-
const path = {
|
|
280
|
-
nodes: () => [0, 1, 2, 3, 4],
|
|
281
|
-
edges: () => [[0, 1], [1, 2], [2, 3], [3, 4]]
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const positions = kamadaKawaiLayout(path);
|
|
285
|
-
|
|
286
|
-
// Distance 0-2 should be roughly twice distance 0-1
|
|
287
|
-
const d01 = getDistance(positions, 0, 1);
|
|
288
|
-
const d02 = getDistance(positions, 0, 2);
|
|
289
|
-
const d03 = getDistance(positions, 0, 3);
|
|
290
|
-
|
|
291
|
-
assert.isAbove(d02, d01);
|
|
292
|
-
assert.isAbove(d03, d02);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should create stable layouts', () => {
|
|
296
|
-
const graph = randomGraph(15, 0.3, 12345);
|
|
297
|
-
|
|
298
|
-
const positions1 = kamadaKawaiLayout(graph);
|
|
299
|
-
const positions2 = kamadaKawaiLayout(graph);
|
|
300
|
-
|
|
301
|
-
// Should produce similar results (allowing for some numerical differences)
|
|
302
|
-
let totalDiff = 0;
|
|
303
|
-
graph.nodes().forEach(node => {
|
|
304
|
-
const dx = positions1[node][0] - positions2[node][0];
|
|
305
|
-
const dy = positions1[node][1] - positions2[node][1];
|
|
306
|
-
totalDiff += Math.sqrt(dx * dx + dy * dy);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
assert.isBelow(totalDiff / graph.nodes().length, 0.1);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('3D Kamada-Kawai layout', () => {
|
|
314
|
-
it('should create 3D layout when dim=3', () => {
|
|
315
|
-
const graph = completeGraph(8);
|
|
316
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
317
|
-
|
|
318
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
319
|
-
graph.nodes().forEach(node => {
|
|
320
|
-
assert.isDefined(positions[node]);
|
|
321
|
-
assert.equal(positions[node].length, 3);
|
|
322
|
-
assert.isNumber(positions[node][0]);
|
|
323
|
-
assert.isNumber(positions[node][1]);
|
|
324
|
-
assert.isNumber(positions[node][2]);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should use spherical initialization for 3D', () => {
|
|
329
|
-
const graph = cycleGraph(6);
|
|
330
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
331
|
-
|
|
332
|
-
// Should produce reasonable edge lengths (not random)
|
|
333
|
-
const edgeLengths = [];
|
|
334
|
-
for (const [source, target] of graph.edges()) {
|
|
335
|
-
const pos1 = positions[source];
|
|
336
|
-
const pos2 = positions[target];
|
|
337
|
-
const dist = Math.sqrt(
|
|
338
|
-
(pos1[0] - pos2[0]) ** 2 +
|
|
339
|
-
(pos1[1] - pos2[1]) ** 2 +
|
|
340
|
-
(pos1[2] - pos2[2]) ** 2
|
|
341
|
-
);
|
|
342
|
-
edgeLengths.push(dist);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Calculate standard deviation
|
|
346
|
-
const mean = edgeLengths.reduce((a, b) => a + b) / edgeLengths.length;
|
|
347
|
-
const variance = edgeLengths.reduce((acc, len) => acc + (len - mean) ** 2, 0) / edgeLengths.length;
|
|
348
|
-
const stdDev = Math.sqrt(variance);
|
|
349
|
-
|
|
350
|
-
// Should have relatively low variance (not random)
|
|
351
|
-
assert.isBelow(stdDev / mean, 0.3, 'Edge lengths should be relatively uniform');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should produce consistent 3D results across runs', () => {
|
|
355
|
-
const graph = starGraph(7);
|
|
356
|
-
|
|
357
|
-
// Run multiple times
|
|
358
|
-
const runs = 3;
|
|
359
|
-
const allPositions = [];
|
|
360
|
-
|
|
361
|
-
for (let i = 0; i < runs; i++) {
|
|
362
|
-
allPositions.push(kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3));
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Check that results are consistent
|
|
366
|
-
graph.nodes().forEach(node => {
|
|
367
|
-
for (let i = 1; i < runs; i++) {
|
|
368
|
-
const pos1 = allPositions[0][node];
|
|
369
|
-
const pos2 = allPositions[i][node];
|
|
370
|
-
const diff = Math.sqrt(
|
|
371
|
-
(pos1[0] - pos2[0]) ** 2 +
|
|
372
|
-
(pos1[1] - pos2[1]) ** 2 +
|
|
373
|
-
(pos1[2] - pos2[2]) ** 2
|
|
374
|
-
);
|
|
375
|
-
assert.isBelow(diff, 1e-6, `Node ${node} should have consistent position across runs`);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('should respect scale in 3D', () => {
|
|
381
|
-
const graph = completeGraph(5);
|
|
382
|
-
|
|
383
|
-
const positions1 = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
384
|
-
const positions2 = kamadaKawaiLayout(graph, null, null, 'weight', 2, [0, 0, 0], 3);
|
|
385
|
-
|
|
386
|
-
// Calculate bounding box for both
|
|
387
|
-
const bbox1 = getBoundingBox3D(positions1);
|
|
388
|
-
const bbox2 = getBoundingBox3D(positions2);
|
|
389
|
-
|
|
390
|
-
// Scale 2 should be approximately twice scale 1
|
|
391
|
-
assert.approximately(bbox2.width / bbox1.width, 2, 0.1);
|
|
392
|
-
assert.approximately(bbox2.height / bbox1.height, 2, 0.1);
|
|
393
|
-
assert.approximately(bbox2.depth / bbox1.depth, 2, 0.1);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should respect center in 3D', () => {
|
|
397
|
-
const graph = cycleGraph(5);
|
|
398
|
-
const center = [10, -5, 7];
|
|
399
|
-
|
|
400
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, center, 3);
|
|
401
|
-
|
|
402
|
-
// Calculate center of mass
|
|
403
|
-
const com = [0, 0, 0];
|
|
404
|
-
const nodes = graph.nodes();
|
|
405
|
-
nodes.forEach(node => {
|
|
406
|
-
com[0] += positions[node][0];
|
|
407
|
-
com[1] += positions[node][1];
|
|
408
|
-
com[2] += positions[node][2];
|
|
409
|
-
});
|
|
410
|
-
com[0] /= nodes.length;
|
|
411
|
-
com[1] /= nodes.length;
|
|
412
|
-
com[2] /= nodes.length;
|
|
413
|
-
|
|
414
|
-
assert.approximately(com[0], center[0], 1);
|
|
415
|
-
assert.approximately(com[1], center[1], 1);
|
|
416
|
-
assert.approximately(com[2], center[2], 1);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('should optimize stress in 3D for regular graphs', () => {
|
|
420
|
-
const graph = completeGraph(6);
|
|
421
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
422
|
-
|
|
423
|
-
// In a complete graph, all pairwise distances should be similar
|
|
424
|
-
const distances = [];
|
|
425
|
-
const nodes = graph.nodes();
|
|
426
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
427
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
428
|
-
const pos1 = positions[nodes[i]];
|
|
429
|
-
const pos2 = positions[nodes[j]];
|
|
430
|
-
const dist = Math.sqrt(
|
|
431
|
-
(pos1[0] - pos2[0]) ** 2 +
|
|
432
|
-
(pos1[1] - pos2[1]) ** 2 +
|
|
433
|
-
(pos1[2] - pos2[2]) ** 2
|
|
434
|
-
);
|
|
435
|
-
distances.push(dist);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const avgDist = distances.reduce((a, b) => a + b) / distances.length;
|
|
440
|
-
const maxDeviation = Math.max(...distances.map(d => Math.abs(d - avgDist) / avgDist));
|
|
441
|
-
|
|
442
|
-
assert.isBelow(maxDeviation, 0.5, 'All pairwise distances should be relatively similar');
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('should handle custom initial 3D positions', () => {
|
|
446
|
-
const graph = completeGraph(4);
|
|
447
|
-
|
|
448
|
-
// Create custom initial positions (tetrahedron vertices)
|
|
449
|
-
const initial3D = {
|
|
450
|
-
0: [1, 1, 1],
|
|
451
|
-
1: [1, -1, -1],
|
|
452
|
-
2: [-1, 1, -1],
|
|
453
|
-
3: [-1, -1, 1]
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const positions = kamadaKawaiLayout(graph, null, initial3D, 'weight', 1, [0, 0, 0], 3);
|
|
457
|
-
|
|
458
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
459
|
-
|
|
460
|
-
// Should optimize from initial positions
|
|
461
|
-
let different = false;
|
|
462
|
-
graph.nodes().forEach(node => {
|
|
463
|
-
if (
|
|
464
|
-
Math.abs(positions[node][0] - initial3D[node][0]) > 0.01 ||
|
|
465
|
-
Math.abs(positions[node][1] - initial3D[node][1]) > 0.01 ||
|
|
466
|
-
Math.abs(positions[node][2] - initial3D[node][2]) > 0.01
|
|
467
|
-
) {
|
|
468
|
-
different = true;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
assert.isTrue(different, 'Should modify initial positions during optimization');
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('should use all three dimensions effectively', () => {
|
|
475
|
-
const graph = gridGraph(3, 3);
|
|
476
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
477
|
-
|
|
478
|
-
// Extract coordinate ranges
|
|
479
|
-
const coords = { x: [], y: [], z: [] };
|
|
480
|
-
graph.nodes().forEach(node => {
|
|
481
|
-
coords.x.push(positions[node][0]);
|
|
482
|
-
coords.y.push(positions[node][1]);
|
|
483
|
-
coords.z.push(positions[node][2]);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const ranges = {
|
|
487
|
-
x: Math.max(...coords.x) - Math.min(...coords.x),
|
|
488
|
-
y: Math.max(...coords.y) - Math.min(...coords.y),
|
|
489
|
-
z: Math.max(...coords.z) - Math.min(...coords.z)
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
// All dimensions should be utilized (not flat)
|
|
493
|
-
assert.isAbove(ranges.x, 0.5, 'X dimension should be utilized');
|
|
494
|
-
assert.isAbove(ranges.y, 0.5, 'Y dimension should be utilized');
|
|
495
|
-
assert.isAbove(ranges.z, 0.2, 'Z dimension should be utilized');
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it('should handle disconnected components in 3D', () => {
|
|
499
|
-
const graph = {
|
|
500
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
501
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]] // Two triangles
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const positions = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
505
|
-
|
|
506
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
507
|
-
|
|
508
|
-
// Each component should be laid out properly
|
|
509
|
-
const component1 = [0, 1, 2].map(n => positions[n]);
|
|
510
|
-
const component2 = [3, 4, 5].map(n => positions[n]);
|
|
511
|
-
|
|
512
|
-
// Check internal distances within each component
|
|
513
|
-
const dist01 = getDistance3D(positions[0], positions[1]);
|
|
514
|
-
const dist12 = getDistance3D(positions[1], positions[2]);
|
|
515
|
-
const dist34 = getDistance3D(positions[3], positions[4]);
|
|
516
|
-
const dist45 = getDistance3D(positions[4], positions[5]);
|
|
517
|
-
|
|
518
|
-
assert.isAbove(dist01, 0);
|
|
519
|
-
assert.isAbove(dist12, 0);
|
|
520
|
-
assert.isAbove(dist34, 0);
|
|
521
|
-
assert.isAbove(dist45, 0);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it('should produce better results than random initialization', () => {
|
|
525
|
-
const graph = cycleGraph(8);
|
|
526
|
-
|
|
527
|
-
// Force random initialization by using very high dimensions (will use random hypersphere)
|
|
528
|
-
const randomPos = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0, 0, 0], 5);
|
|
529
|
-
|
|
530
|
-
// Normal 3D with spherical initialization
|
|
531
|
-
const sphericalPos = kamadaKawaiLayout(graph, null, null, 'weight', 1, [0, 0, 0], 3);
|
|
532
|
-
|
|
533
|
-
// Compare edge length uniformity
|
|
534
|
-
function getEdgeVariance(positions, edges) {
|
|
535
|
-
const lengths = edges.map(([u, v]) => {
|
|
536
|
-
const pos1 = positions[u];
|
|
537
|
-
const pos2 = positions[v];
|
|
538
|
-
let sum = 0;
|
|
539
|
-
for (let i = 0; i < Math.min(pos1.length, pos2.length); i++) {
|
|
540
|
-
sum += (pos1[i] - pos2[i]) ** 2;
|
|
541
|
-
}
|
|
542
|
-
return Math.sqrt(sum);
|
|
543
|
-
});
|
|
544
|
-
const mean = lengths.reduce((a, b) => a + b) / lengths.length;
|
|
545
|
-
return lengths.reduce((acc, len) => acc + (len - mean) ** 2, 0) / lengths.length;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const edges = graph.edges();
|
|
549
|
-
const sphericalVariance = getEdgeVariance(sphericalPos, edges);
|
|
550
|
-
|
|
551
|
-
// Spherical initialization should produce lower variance
|
|
552
|
-
assert.isBelow(sphericalVariance, 0.5, 'Spherical initialization should produce uniform edge lengths');
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Helper functions
|
|
558
|
-
function getLayoutSpan(positions) {
|
|
559
|
-
const nodes = Object.keys(positions);
|
|
560
|
-
let minX = Infinity, maxX = -Infinity;
|
|
561
|
-
let minY = Infinity, maxY = -Infinity;
|
|
562
|
-
|
|
563
|
-
nodes.forEach(node => {
|
|
564
|
-
minX = Math.min(minX, positions[node][0]);
|
|
565
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
566
|
-
minY = Math.min(minY, positions[node][1]);
|
|
567
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
return {
|
|
571
|
-
width: maxX - minX,
|
|
572
|
-
height: maxY - minY
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function getCenterOfMass(positions) {
|
|
577
|
-
const nodes = Object.keys(positions);
|
|
578
|
-
const com = [0, 0];
|
|
579
|
-
|
|
580
|
-
nodes.forEach(node => {
|
|
581
|
-
com[0] += positions[node][0];
|
|
582
|
-
com[1] += positions[node][1];
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
com[0] /= nodes.length;
|
|
586
|
-
com[1] /= nodes.length;
|
|
587
|
-
return com;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function getDistance(positions, node1, node2) {
|
|
591
|
-
const dx = positions[node1][0] - positions[node2][0];
|
|
592
|
-
const dy = positions[node1][1] - positions[node2][1];
|
|
593
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function getBoundingBox3D(positions) {
|
|
597
|
-
const nodes = Object.keys(positions);
|
|
598
|
-
let minX = Infinity, maxX = -Infinity;
|
|
599
|
-
let minY = Infinity, maxY = -Infinity;
|
|
600
|
-
let minZ = Infinity, maxZ = -Infinity;
|
|
601
|
-
|
|
602
|
-
nodes.forEach(node => {
|
|
603
|
-
minX = Math.min(minX, positions[node][0]);
|
|
604
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
605
|
-
minY = Math.min(minY, positions[node][1]);
|
|
606
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
607
|
-
minZ = Math.min(minZ, positions[node][2]);
|
|
608
|
-
maxZ = Math.max(maxZ, positions[node][2]);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
return {
|
|
612
|
-
width: maxX - minX,
|
|
613
|
-
height: maxY - minY,
|
|
614
|
-
depth: maxZ - minZ
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function getDistance3D(pos1, pos2) {
|
|
619
|
-
const dx = pos1[0] - pos2[0];
|
|
620
|
-
const dy = pos1[1] - pos2[1];
|
|
621
|
-
const dz = pos1[2] - pos2[2];
|
|
622
|
-
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
623
|
-
}
|