@graphty/layout 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/algorithms/index.d.ts +5 -0
- package/dist/src/algorithms/index.js +6 -0
- package/dist/src/algorithms/index.js.map +1 -0
- package/dist/src/algorithms/optimization/index.d.ts +7 -0
- package/dist/src/algorithms/optimization/index.js +7 -0
- package/dist/src/algorithms/optimization/index.js.map +1 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +32 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +176 -0
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -0
- package/dist/src/algorithms/optimization/lbfgs.d.ts +13 -0
- package/dist/src/algorithms/optimization/lbfgs.js +56 -0
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -0
- package/dist/src/algorithms/optimization/line-search.d.ts +15 -0
- package/dist/src/algorithms/optimization/line-search.js +37 -0
- package/dist/src/algorithms/optimization/line-search.js.map +1 -0
- package/dist/src/algorithms/optimization/types.d.ts +5 -0
- package/dist/src/algorithms/optimization/types.js +5 -0
- package/dist/src/algorithms/optimization/types.js.map +1 -0
- package/dist/src/algorithms/planarity/check.d.ts +17 -0
- package/dist/src/algorithms/planarity/check.js +30 -0
- package/dist/src/algorithms/planarity/check.js.map +1 -0
- package/dist/src/algorithms/planarity/embedding.d.ts +29 -0
- package/dist/src/algorithms/planarity/embedding.js +179 -0
- package/dist/src/algorithms/planarity/embedding.js.map +1 -0
- package/dist/src/algorithms/planarity/index.d.ts +7 -0
- package/dist/src/algorithms/planarity/index.js +8 -0
- package/dist/src/algorithms/planarity/index.js.map +1 -0
- package/dist/src/algorithms/planarity/lr-test.d.ts +15 -0
- package/dist/src/algorithms/planarity/lr-test.js +54 -0
- package/dist/src/algorithms/planarity/lr-test.js.map +1 -0
- package/dist/src/algorithms/planarity/special-graphs.d.ts +28 -0
- package/dist/src/algorithms/planarity/special-graphs.js +108 -0
- package/dist/src/algorithms/planarity/special-graphs.js.map +1 -0
- package/dist/src/generators/basic.d.ts +28 -0
- package/dist/src/generators/basic.js +79 -0
- package/dist/src/generators/basic.js.map +1 -0
- package/dist/src/generators/bipartite.d.ts +16 -0
- package/dist/src/generators/bipartite.js +40 -0
- package/dist/src/generators/bipartite.js.map +1 -0
- package/dist/src/generators/grid.d.ts +11 -0
- package/dist/src/generators/grid.js +37 -0
- package/dist/src/generators/grid.js.map +1 -0
- package/dist/src/generators/index.d.ts +8 -0
- package/dist/src/generators/index.js +9 -0
- package/dist/src/generators/index.js.map +1 -0
- package/dist/src/generators/random.d.ts +12 -0
- package/dist/src/generators/random.js +34 -0
- package/dist/src/generators/random.js.map +1 -0
- package/dist/src/generators/scale-free.d.ts +12 -0
- package/dist/src/generators/scale-free.js +62 -0
- package/dist/src/generators/scale-free.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/layouts/basic/index.d.ts +4 -0
- package/dist/src/layouts/basic/index.js +5 -0
- package/dist/src/layouts/basic/index.js.map +1 -0
- package/dist/src/layouts/basic/random.d.ts +14 -0
- package/dist/src/layouts/basic/random.js +27 -0
- package/dist/src/layouts/basic/random.js.map +1 -0
- package/dist/src/layouts/force-directed/arf.d.ts +13 -0
- package/dist/src/layouts/force-directed/arf.js +103 -0
- package/dist/src/layouts/force-directed/arf.js.map +1 -0
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +22 -0
- package/dist/src/layouts/force-directed/forceatlas2.js +322 -0
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +19 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +131 -0
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/force-directed/index.d.ts +8 -0
- package/dist/src/layouts/force-directed/index.js +9 -0
- package/dist/src/layouts/force-directed/index.js.map +1 -0
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +15 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js +87 -0
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -0
- package/dist/src/layouts/force-directed/spring.d.ts +19 -0
- package/dist/src/layouts/force-directed/spring.js +23 -0
- package/dist/src/layouts/force-directed/spring.js.map +1 -0
- package/dist/src/layouts/geometric/circular.d.ts +14 -0
- package/dist/src/layouts/geometric/circular.js +67 -0
- package/dist/src/layouts/geometric/circular.js.map +1 -0
- package/dist/src/layouts/geometric/index.d.ts +6 -0
- package/dist/src/layouts/geometric/index.js +7 -0
- package/dist/src/layouts/geometric/index.js.map +1 -0
- package/dist/src/layouts/geometric/shell.d.ts +15 -0
- package/dist/src/layouts/geometric/shell.js +67 -0
- package/dist/src/layouts/geometric/shell.js.map +1 -0
- package/dist/src/layouts/geometric/spiral.d.ts +16 -0
- package/dist/src/layouts/geometric/spiral.js +73 -0
- package/dist/src/layouts/geometric/spiral.js.map +1 -0
- package/dist/src/layouts/hierarchical/bfs.d.ts +12 -0
- package/dist/src/layouts/hierarchical/bfs.js +62 -0
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -0
- package/dist/src/layouts/hierarchical/bipartite.d.ts +16 -0
- package/dist/src/layouts/hierarchical/bipartite.js +72 -0
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -0
- package/dist/src/layouts/hierarchical/index.d.ts +6 -0
- package/dist/src/layouts/hierarchical/index.js +7 -0
- package/dist/src/layouts/hierarchical/index.js.map +1 -0
- package/dist/src/layouts/hierarchical/multipartite.d.ts +12 -0
- package/dist/src/layouts/hierarchical/multipartite.js +73 -0
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -0
- package/dist/src/layouts/index.d.ts +8 -0
- package/dist/src/layouts/index.js +9 -0
- package/dist/src/layouts/index.js.map +1 -0
- package/dist/src/layouts/specialized/index.d.ts +5 -0
- package/dist/src/layouts/specialized/index.js +6 -0
- package/dist/src/layouts/specialized/index.js.map +1 -0
- package/dist/src/layouts/specialized/planar.d.ts +14 -0
- package/dist/src/layouts/specialized/planar.js +47 -0
- package/dist/src/layouts/specialized/planar.js.map +1 -0
- package/dist/src/layouts/specialized/spectral.d.ts +14 -0
- package/dist/src/layouts/specialized/spectral.js +106 -0
- package/dist/src/layouts/specialized/spectral.js.map +1 -0
- package/dist/src/types/embedding.d.ts +9 -0
- package/dist/src/types/embedding.js +5 -0
- package/dist/src/types/embedding.js.map +1 -0
- package/dist/src/types/graph.d.ts +10 -0
- package/dist/src/types/graph.js +5 -0
- package/dist/src/types/graph.js.map +1 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/layout.d.ts +6 -0
- package/dist/src/types/layout.js +5 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/utils/graph.d.ts +34 -0
- package/dist/src/utils/graph.js +69 -0
- package/dist/src/utils/graph.js.map +1 -0
- package/dist/src/utils/index.d.ts +8 -0
- package/dist/src/utils/index.js +9 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/numpy.d.ts +17 -0
- package/dist/src/utils/numpy.js +101 -0
- package/dist/src/utils/numpy.js.map +1 -0
- package/dist/src/utils/params.d.ts +17 -0
- package/dist/src/utils/params.js +22 -0
- package/dist/src/utils/params.js.map +1 -0
- package/dist/src/utils/random.d.ts +14 -0
- package/dist/src/utils/random.js +42 -0
- package/dist/src/utils/random.js.map +1 -0
- package/dist/src/utils/rescale.d.ts +21 -0
- package/dist/src/utils/rescale.js +119 -0
- package/dist/src/utils/rescale.js.map +1 -0
- package/dist/vitest.config.js +1 -1
- package/dist/vitest.config.js.map +1 -1
- package/package.json +14 -1
- package/.env.example +0 -11
- package/.github/workflows/ci.yml +0 -101
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -52
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -1
- package/DEPLOYMENT.md +0 -59
- package/build-examples-script.js +0 -26
- package/commitlint.config.js +0 -37
- package/examples/3d-force-directed.html +0 -611
- package/examples/3d-kamada-kawai.html +0 -379
- package/examples/3d-layout-comparison.html +0 -448
- package/examples/3d-spherical-layout.html +0 -319
- package/examples/arf-layout.html +0 -524
- package/examples/bfs-layout.html +0 -487
- package/examples/bipartite-layout.html +0 -382
- package/examples/circular-layout.html +0 -292
- package/examples/forceatlas2-layout.html +0 -588
- package/examples/index.html +0 -241
- package/examples/kamada-kawai-layout.html +0 -425
- package/examples/layout-helpers.js +0 -23
- package/examples/layout.js +0 -2304
- package/examples/multipartite-layout.html +0 -410
- package/examples/planar-layout.html +0 -388
- package/examples/random-layout.html +0 -328
- package/examples/shell-layout.html +0 -323
- package/examples/spectral-layout.html +0 -335
- package/examples/spiral-layout.html +0 -447
- package/examples/spring-layout.html +0 -313
- package/test/arf-layout.test.ts +0 -443
- package/test/bfs-layout.test.ts +0 -427
- package/test/bipartite-layout.test.ts +0 -344
- package/test/circular-layout.test.ts +0 -442
- package/test/forceatlas2-layout.test.ts +0 -405
- package/test/fruchterman-reingold-layout.test.ts +0 -477
- package/test/graph-generators.test.ts +0 -450
- package/test/kamada-kawai-layout.test.ts +0 -623
- package/test/multipartite-layout.test.ts +0 -404
- package/test/planar-layout.test.ts +0 -266
- package/test/random-layout.test.ts +0 -254
- package/test/rescale-layout.test.ts +0 -373
- package/test/shell-layout.test.ts +0 -347
- package/test/spectral-layout.test.ts +0 -378
- package/test/spiral-layout.test.ts +0 -338
- package/test/spring-layout.test.ts +0 -241
- package/test/test-utils.ts +0 -34
- package/test/utils-graph.test.ts +0 -155
- package/test/utils-index.test.ts +0 -77
- package/test/utils-numpy.test.ts +0 -272
- package/test/utils-params.test.ts +0 -99
- package/test/utils-random.test.ts +0 -229
- package/tsconfig.json +0 -16
- package/vite.config.js +0 -36
- package/vitest.config.ts +0 -30
package/test/bfs-layout.test.ts
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
bfsLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
wheelGraph,
|
|
8
|
-
gridGraph,
|
|
9
|
-
randomGraph
|
|
10
|
-
} from '../src';
|
|
11
|
-
|
|
12
|
-
describe('BFS Layout', () => {
|
|
13
|
-
describe('Basic functionality', () => {
|
|
14
|
-
it('should position all nodes', () => {
|
|
15
|
-
const graph = starGraph(7);
|
|
16
|
-
const positions = bfsLayout(graph, 0);
|
|
17
|
-
|
|
18
|
-
assert.equal(Object.keys(positions).length, 7);
|
|
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 = bfsLayout(emptyGraph, null);
|
|
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 = bfsLayout(singleNode, 'A');
|
|
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 throw error for disconnected components', () => {
|
|
44
|
-
const graph = {
|
|
45
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
46
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]] // Two components
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// BFS layout throws error for disconnected graphs
|
|
50
|
-
assert.throws(() => {
|
|
51
|
-
bfsLayout(graph, 0);
|
|
52
|
-
}, "bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should use first node as root if not specified', () => {
|
|
56
|
-
const graph = cycleGraph(5);
|
|
57
|
-
const positions = bfsLayout(graph, graph.nodes()[0]); // Use first node as root
|
|
58
|
-
|
|
59
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
60
|
-
graph.nodes().forEach(node => {
|
|
61
|
-
assert.isDefined(positions[node]);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('BFS tree structure', () => {
|
|
67
|
-
it('should arrange nodes in levels based on BFS distance', () => {
|
|
68
|
-
const graph = wheelGraph(7); // Center connected to 6 rim nodes
|
|
69
|
-
const positions = bfsLayout(graph, 0); // Start from center
|
|
70
|
-
|
|
71
|
-
// Root node should be positioned
|
|
72
|
-
assert.isDefined(positions[0]);
|
|
73
|
-
assert.equal(positions[0].length, 2);
|
|
74
|
-
|
|
75
|
-
// All rim nodes should be at a similar level (distance 1)
|
|
76
|
-
const rimY = [1, 2, 3, 4, 5, 6].map(node => positions[node][1]);
|
|
77
|
-
// Check they are all positioned
|
|
78
|
-
rimY.forEach(y => {
|
|
79
|
-
assert.isNumber(y);
|
|
80
|
-
assert.isFalse(isNaN(y));
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should create proper tree hierarchy', () => {
|
|
85
|
-
// Create a simple tree structure
|
|
86
|
-
const graph = {
|
|
87
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6],
|
|
88
|
-
edges: () => [
|
|
89
|
-
[0, 1], [0, 2], // Level 1: 1, 2
|
|
90
|
-
[1, 3], [1, 4], // Level 2: 3, 4
|
|
91
|
-
[2, 5], [2, 6] // Level 2: 5, 6
|
|
92
|
-
]
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const positions = bfsLayout(graph, 0);
|
|
96
|
-
|
|
97
|
-
// Check levels
|
|
98
|
-
// Root should be positioned
|
|
99
|
-
assert.isDefined(positions[0]);
|
|
100
|
-
assert.equal(positions[0].length, 2);
|
|
101
|
-
|
|
102
|
-
// Level 1 nodes should be positioned
|
|
103
|
-
assert.isDefined(positions[1]);
|
|
104
|
-
assert.isDefined(positions[2]);
|
|
105
|
-
|
|
106
|
-
// Level 2 nodes should all be positioned
|
|
107
|
-
for (let i = 3; i <= 6; i++) {
|
|
108
|
-
assert.isDefined(positions[i]);
|
|
109
|
-
assert.equal(positions[i].length, 2);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should handle cycles by treating as tree from root', () => {
|
|
114
|
-
const graph = cycleGraph(6);
|
|
115
|
-
const positions = bfsLayout(graph, 0);
|
|
116
|
-
|
|
117
|
-
// In a cycle, BFS creates a tree structure
|
|
118
|
-
// Node 0 is root, nodes 1 and 5 are children, etc.
|
|
119
|
-
assert.isDefined(positions[0]);
|
|
120
|
-
assert.equal(positions[0].length, 2);
|
|
121
|
-
|
|
122
|
-
// Direct neighbors should be positioned
|
|
123
|
-
assert.isDefined(positions[1]);
|
|
124
|
-
assert.isDefined(positions[5]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should space nodes horizontally within each level', () => {
|
|
128
|
-
const graph = starGraph(9); // 1 center + 8 leaves
|
|
129
|
-
const positions = bfsLayout(graph, 0);
|
|
130
|
-
|
|
131
|
-
// Get x-coordinates of leaf nodes
|
|
132
|
-
const leafX = [];
|
|
133
|
-
for (let i = 1; i <= 8; i++) {
|
|
134
|
-
leafX.push(positions[i][0]);
|
|
135
|
-
}
|
|
136
|
-
leafX.sort((a, b) => a - b);
|
|
137
|
-
|
|
138
|
-
// Check that leaves are positioned
|
|
139
|
-
// In multipartite layout, they might all have same x
|
|
140
|
-
assert.equal(leafX.length, 8);
|
|
141
|
-
|
|
142
|
-
// Check spacing if nodes have different x coordinates
|
|
143
|
-
const uniqueX = [...new Set(leafX)];
|
|
144
|
-
if (uniqueX.length > 1) {
|
|
145
|
-
const spacings = [];
|
|
146
|
-
for (let i = 1; i < leafX.length; i++) {
|
|
147
|
-
spacings.push(leafX[i] - leafX[i-1]);
|
|
148
|
-
}
|
|
149
|
-
const avgSpacing = spacings.reduce((a, b) => a + b) / spacings.length;
|
|
150
|
-
spacings.forEach(s => {
|
|
151
|
-
assert.isAtLeast(s, 0); // Allow 0 spacing
|
|
152
|
-
assert.isBelow(s, avgSpacing * 3); // Not too uneven
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('Parameter variations', () => {
|
|
159
|
-
it('should respect scale parameter', () => {
|
|
160
|
-
const graph = wheelGraph(5);
|
|
161
|
-
|
|
162
|
-
const positions1 = bfsLayout(graph, 0, 'vertical', 1);
|
|
163
|
-
const positions2 = bfsLayout(graph, 0, 'vertical', 2);
|
|
164
|
-
|
|
165
|
-
// Calculate spans
|
|
166
|
-
const span1 = getLayoutSpan(positions1);
|
|
167
|
-
const span2 = getLayoutSpan(positions2);
|
|
168
|
-
|
|
169
|
-
assert.approximately(span2.width / span1.width, 2, 0.1);
|
|
170
|
-
assert.approximately(span2.height / span1.height, 2, 0.1);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('should respect center parameter', () => {
|
|
174
|
-
const graph = starGraph(5);
|
|
175
|
-
const center = [10, -5];
|
|
176
|
-
|
|
177
|
-
const positions = bfsLayout(graph, 0, 'vertical', 1, center);
|
|
178
|
-
|
|
179
|
-
// Layout should be centered around specified point
|
|
180
|
-
const avgX = Object.values(positions).reduce((sum, pos) => sum + pos[0], 0) / 5;
|
|
181
|
-
const avgY = Object.values(positions).reduce((sum, pos) => sum + pos[1], 0) / 5;
|
|
182
|
-
assert.approximately(avgX, center[0], 1);
|
|
183
|
-
assert.approximately(avgY, center[1], 1);
|
|
184
|
-
|
|
185
|
-
// Nodes should be positioned around center
|
|
186
|
-
for (let i = 0; i < 5; i++) {
|
|
187
|
-
assert.isDefined(positions[i]);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should handle different root nodes', () => {
|
|
192
|
-
const graph = cycleGraph(6);
|
|
193
|
-
|
|
194
|
-
const positions0 = bfsLayout(graph, 0);
|
|
195
|
-
const positions3 = bfsLayout(graph, 3);
|
|
196
|
-
|
|
197
|
-
// Different roots should be positioned
|
|
198
|
-
assert.isDefined(positions0[0]);
|
|
199
|
-
assert.isDefined(positions3[3]);
|
|
200
|
-
|
|
201
|
-
// The tree structure should be different
|
|
202
|
-
assert.notDeepEqual(positions0[1], positions3[1]);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('Special cases and edge cases', () => {
|
|
207
|
-
it('should handle string node IDs', () => {
|
|
208
|
-
const graph = {
|
|
209
|
-
nodes: () => ['root', 'child1', 'child2', 'grandchild1', 'grandchild2'],
|
|
210
|
-
edges: () => [
|
|
211
|
-
['root', 'child1'],
|
|
212
|
-
['root', 'child2'],
|
|
213
|
-
['child1', 'grandchild1'],
|
|
214
|
-
['child2', 'grandchild2']
|
|
215
|
-
]
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const positions = bfsLayout(graph, 'root');
|
|
219
|
-
|
|
220
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
221
|
-
assert.isDefined(positions['root']);
|
|
222
|
-
assert.equal(positions['root'].length, 2);
|
|
223
|
-
|
|
224
|
-
// Children should be positioned
|
|
225
|
-
assert.isDefined(positions['child1']);
|
|
226
|
-
assert.isDefined(positions['child2']);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should handle star graph with leaf as root', () => {
|
|
230
|
-
const graph = starGraph(7);
|
|
231
|
-
const positions = bfsLayout(graph, 1); // Use leaf as root
|
|
232
|
-
|
|
233
|
-
// Leaf 1 should be positioned
|
|
234
|
-
assert.isDefined(positions[1]);
|
|
235
|
-
assert.equal(positions[1].length, 2);
|
|
236
|
-
|
|
237
|
-
// Center node should be positioned differently from root
|
|
238
|
-
assert.notDeepEqual(positions[0], positions[1]);
|
|
239
|
-
|
|
240
|
-
// Other leaves should be at deeper level than center
|
|
241
|
-
for (let i = 2; i < 7; i++) {
|
|
242
|
-
assert.isDefined(positions[i]);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should produce consistent results', () => {
|
|
247
|
-
const graph = randomGraph(15, 0.3, 42);
|
|
248
|
-
const root = graph.nodes()[0];
|
|
249
|
-
|
|
250
|
-
const positions1 = bfsLayout(graph, root);
|
|
251
|
-
const positions2 = bfsLayout(graph, root);
|
|
252
|
-
|
|
253
|
-
// BFS layout is deterministic for same root
|
|
254
|
-
graph.nodes().forEach(node => {
|
|
255
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('should handle complete binary tree', () => {
|
|
260
|
-
// Create a complete binary tree
|
|
261
|
-
const graph = {
|
|
262
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6],
|
|
263
|
-
edges: () => [
|
|
264
|
-
[0, 1], [0, 2], // Level 1
|
|
265
|
-
[1, 3], [1, 4], // Level 2
|
|
266
|
-
[2, 5], [2, 6] // Level 2
|
|
267
|
-
]
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const positions = bfsLayout(graph, 0);
|
|
271
|
-
|
|
272
|
-
// Root should be positioned
|
|
273
|
-
assert.isDefined(positions[0]);
|
|
274
|
-
assert.equal(positions[0].length, 2);
|
|
275
|
-
|
|
276
|
-
// Children should be positioned differently
|
|
277
|
-
const x1 = positions[1][0];
|
|
278
|
-
const x2 = positions[2][0];
|
|
279
|
-
// Note: In some layouts, x1 might equal x2
|
|
280
|
-
|
|
281
|
-
// Check that all nodes are positioned
|
|
282
|
-
for (let i = 0; i <= 6; i++) {
|
|
283
|
-
assert.isDefined(positions[i]);
|
|
284
|
-
assert.equal(positions[i].length, 2);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Tree should have some structure
|
|
288
|
-
const xCoords = Object.values(positions).map(p => p[0]);
|
|
289
|
-
const yCoords = Object.values(positions).map(p => p[1]);
|
|
290
|
-
const layoutWidth = Math.max(...xCoords) - Math.min(...xCoords);
|
|
291
|
-
const layoutHeight = Math.max(...yCoords) - Math.min(...yCoords);
|
|
292
|
-
assert.isAbove(layoutWidth + layoutHeight, 0);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should handle grid graph efficiently', () => {
|
|
296
|
-
const graph = gridGraph(5, 5); // 25 nodes
|
|
297
|
-
|
|
298
|
-
const startTime = performance.now();
|
|
299
|
-
const positions = bfsLayout(graph, '0,0');
|
|
300
|
-
const endTime = performance.now();
|
|
301
|
-
|
|
302
|
-
assert.equal(Object.keys(positions).length, 25);
|
|
303
|
-
assert.isBelow(endTime - startTime, 100);
|
|
304
|
-
|
|
305
|
-
// Check that BFS creates proper levels
|
|
306
|
-
// Corner node should be positioned
|
|
307
|
-
assert.isDefined(positions['0,0']);
|
|
308
|
-
assert.equal(positions['0,0'].length, 2);
|
|
309
|
-
|
|
310
|
-
// Adjacent nodes should be positioned
|
|
311
|
-
assert.isDefined(positions['0,1']);
|
|
312
|
-
assert.isDefined(positions['1,0']);
|
|
313
|
-
// They should be positioned
|
|
314
|
-
assert.isNumber(positions['0,1'][1]);
|
|
315
|
-
assert.isNumber(positions['1,0'][1]);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('should handle dense graphs', () => {
|
|
319
|
-
const graph = completeGraph(10);
|
|
320
|
-
const positions = bfsLayout(graph, 0);
|
|
321
|
-
|
|
322
|
-
// In complete graph, all non-root nodes are at level 1
|
|
323
|
-
// Just check they are all positioned
|
|
324
|
-
for (let i = 1; i < 10; i++) {
|
|
325
|
-
assert.isDefined(positions[i]);
|
|
326
|
-
assert.equal(positions[i].length, 2);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Should position nodes (they might all have same x in multipartite layout)
|
|
330
|
-
const xCoords = [];
|
|
331
|
-
for (let i = 1; i < 10; i++) {
|
|
332
|
-
xCoords.push(positions[i][0]);
|
|
333
|
-
}
|
|
334
|
-
// Just verify all nodes are positioned
|
|
335
|
-
assert.equal(xCoords.length, 9);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
describe('Layout quality', () => {
|
|
340
|
-
it('should minimize edge crossings in tree layout', () => {
|
|
341
|
-
// Binary tree should have no crossings
|
|
342
|
-
const graph = {
|
|
343
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6],
|
|
344
|
-
edges: () => [
|
|
345
|
-
[0, 1], [0, 2],
|
|
346
|
-
[1, 3], [1, 4],
|
|
347
|
-
[2, 5], [2, 6]
|
|
348
|
-
]
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const positions = bfsLayout(graph, 0);
|
|
352
|
-
|
|
353
|
-
// Check that the tree layout is reasonable
|
|
354
|
-
// All nodes should be positioned
|
|
355
|
-
for (let i = 0; i <= 6; i++) {
|
|
356
|
-
assert.isDefined(positions[i]);
|
|
357
|
-
assert.equal(positions[i].length, 2);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Tree should have hierarchical structure
|
|
361
|
-
const levels = [
|
|
362
|
-
[0], // root
|
|
363
|
-
[1, 2], // level 1
|
|
364
|
-
[3, 4, 5, 6] // level 2
|
|
365
|
-
];
|
|
366
|
-
|
|
367
|
-
// Each level should be positioned
|
|
368
|
-
levels.forEach(level => {
|
|
369
|
-
level.forEach(node => {
|
|
370
|
-
assert.isDefined(positions[node]);
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('should create balanced layout for balanced trees', () => {
|
|
376
|
-
// Perfect binary tree
|
|
377
|
-
const graph = {
|
|
378
|
-
nodes: () => Array.from({ length: 7 }, (_, i) => i),
|
|
379
|
-
edges: () => [
|
|
380
|
-
[0, 1], [0, 2],
|
|
381
|
-
[1, 3], [1, 4],
|
|
382
|
-
[2, 5], [2, 6]
|
|
383
|
-
]
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const positions = bfsLayout(graph, 0);
|
|
387
|
-
|
|
388
|
-
// Check symmetry
|
|
389
|
-
const rootX = positions[0][0];
|
|
390
|
-
|
|
391
|
-
// Level 1 should be symmetric
|
|
392
|
-
assert.approximately(
|
|
393
|
-
Math.abs(positions[1][0] - rootX),
|
|
394
|
-
Math.abs(positions[2][0] - rootX),
|
|
395
|
-
0.1
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
// Level 2 should maintain balance
|
|
399
|
-
const leftSubtree = [3, 4].map(n => positions[n][0]);
|
|
400
|
-
const rightSubtree = [5, 6].map(n => positions[n][0]);
|
|
401
|
-
|
|
402
|
-
const leftSpan = Math.max(...leftSubtree) - Math.min(...leftSubtree);
|
|
403
|
-
const rightSpan = Math.max(...rightSubtree) - Math.min(...rightSubtree);
|
|
404
|
-
|
|
405
|
-
assert.approximately(leftSpan, rightSpan, 0.1);
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Helper functions
|
|
411
|
-
function getLayoutSpan(positions) {
|
|
412
|
-
const nodes = Object.keys(positions);
|
|
413
|
-
let minX = Infinity, maxX = -Infinity;
|
|
414
|
-
let minY = Infinity, maxY = -Infinity;
|
|
415
|
-
|
|
416
|
-
nodes.forEach(node => {
|
|
417
|
-
minX = Math.min(minX, positions[node][0]);
|
|
418
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
419
|
-
minY = Math.min(minY, positions[node][1]);
|
|
420
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
return {
|
|
424
|
-
width: maxX - minX,
|
|
425
|
-
height: maxY - minY
|
|
426
|
-
};
|
|
427
|
-
}
|