@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,404 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
multipartiteLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
randomGraph
|
|
7
|
-
} from '../src';
|
|
8
|
-
|
|
9
|
-
describe('Multipartite Layout', () => {
|
|
10
|
-
describe('Basic functionality', () => {
|
|
11
|
-
it('should position all nodes', () => {
|
|
12
|
-
const graph = completeGraph(12);
|
|
13
|
-
const subsets = [
|
|
14
|
-
[0, 1, 2, 3],
|
|
15
|
-
[4, 5, 6, 7],
|
|
16
|
-
[8, 9, 10, 11]
|
|
17
|
-
];
|
|
18
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
19
|
-
|
|
20
|
-
assert.equal(Object.keys(positions).length, 12);
|
|
21
|
-
graph.nodes().forEach(node => {
|
|
22
|
-
assert.isDefined(positions[node]);
|
|
23
|
-
assert.equal(positions[node].length, 2);
|
|
24
|
-
assert.isNumber(positions[node][0]);
|
|
25
|
-
assert.isNumber(positions[node][1]);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should handle empty graph', () => {
|
|
30
|
-
const emptyGraph = { nodes: () => [], edges: () => [] };
|
|
31
|
-
const positions = multipartiteLayout(emptyGraph, []);
|
|
32
|
-
|
|
33
|
-
assert.equal(Object.keys(positions).length, 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should handle single node', () => {
|
|
37
|
-
const singleNode = { nodes: () => ['A'], edges: () => [] };
|
|
38
|
-
const positions = multipartiteLayout(singleNode, [['A']]);
|
|
39
|
-
|
|
40
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
41
|
-
assert.deepEqual(positions['A'], [0, 0]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should handle disconnected multipartite graph', () => {
|
|
45
|
-
const graph = {
|
|
46
|
-
nodes: () => ['A0', 'A1', 'B0', 'B1', 'C0', 'C1'],
|
|
47
|
-
edges: () => [['A0', 'B0'], ['A1', 'B1']] // C nodes disconnected
|
|
48
|
-
};
|
|
49
|
-
const subsets = [['A0', 'A1'], ['B0', 'B1'], ['C0', 'C1']];
|
|
50
|
-
|
|
51
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
52
|
-
|
|
53
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
54
|
-
graph.nodes().forEach(node => {
|
|
55
|
-
assert.isDefined(positions[node]);
|
|
56
|
-
assert.equal(positions[node].length, 2);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('Multipartite arrangement', () => {
|
|
62
|
-
it('should arrange subsets in layers for vertical alignment', () => {
|
|
63
|
-
const graph = completeGraph(9);
|
|
64
|
-
const subsets = [
|
|
65
|
-
[0, 1, 2],
|
|
66
|
-
[3, 4, 5],
|
|
67
|
-
[6, 7, 8]
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
const positions = multipartiteLayout(graph, subsets, 'vertical');
|
|
71
|
-
|
|
72
|
-
// Calculate average x-coordinate for each subset
|
|
73
|
-
const avgX0 = subsets[0].reduce((sum, n) => sum + positions[n][0], 0) / 3;
|
|
74
|
-
const avgX1 = subsets[1].reduce((sum, n) => sum + positions[n][0], 0) / 3;
|
|
75
|
-
const avgX2 = subsets[2].reduce((sum, n) => sum + positions[n][0], 0) / 3;
|
|
76
|
-
|
|
77
|
-
// Subsets should be separated horizontally
|
|
78
|
-
assert.notEqual(avgX0, avgX1);
|
|
79
|
-
assert.notEqual(avgX1, avgX2);
|
|
80
|
-
assert.notEqual(avgX0, avgX2);
|
|
81
|
-
|
|
82
|
-
// Should be evenly spaced
|
|
83
|
-
const spacing1 = Math.abs(avgX1 - avgX0);
|
|
84
|
-
const spacing2 = Math.abs(avgX2 - avgX1);
|
|
85
|
-
assert.approximately(spacing1, spacing2, 0.1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should arrange subsets in layers for horizontal alignment', () => {
|
|
89
|
-
const graph = completeGraph(8);
|
|
90
|
-
const subsets = [
|
|
91
|
-
[0, 1],
|
|
92
|
-
[2, 3, 4],
|
|
93
|
-
[5, 6, 7]
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
const positions = multipartiteLayout(graph, subsets, 'horizontal');
|
|
97
|
-
|
|
98
|
-
// Calculate average y-coordinate for each subset
|
|
99
|
-
const avgY0 = subsets[0].reduce((sum, n) => sum + positions[n][1], 0) / subsets[0].length;
|
|
100
|
-
const avgY1 = subsets[1].reduce((sum, n) => sum + positions[n][1], 0) / subsets[1].length;
|
|
101
|
-
const avgY2 = subsets[2].reduce((sum, n) => sum + positions[n][1], 0) / subsets[2].length;
|
|
102
|
-
|
|
103
|
-
// Subsets should be separated vertically
|
|
104
|
-
assert.notEqual(avgY0, avgY1);
|
|
105
|
-
assert.notEqual(avgY1, avgY2);
|
|
106
|
-
assert.notEqual(avgY0, avgY2);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should evenly space nodes within each subset', () => {
|
|
110
|
-
const graph = {
|
|
111
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
112
|
-
edges: () => [[0, 3], [1, 4], [2, 5], [3, 6], [4, 7], [5, 8]]
|
|
113
|
-
};
|
|
114
|
-
const subsets = [
|
|
115
|
-
[0, 1, 2],
|
|
116
|
-
[3, 4, 5],
|
|
117
|
-
[6, 7, 8, 9]
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
const positions = multipartiteLayout(graph, subsets, 'vertical');
|
|
121
|
-
|
|
122
|
-
// Check spacing within first subset
|
|
123
|
-
const subset0Y = subsets[0].map(n => positions[n][1]).sort((a, b) => a - b);
|
|
124
|
-
const spacings0 = [];
|
|
125
|
-
for (let i = 1; i < subset0Y.length; i++) {
|
|
126
|
-
spacings0.push(subset0Y[i] - subset0Y[i-1]);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// All spacings should be equal
|
|
130
|
-
if (spacings0.length > 0) {
|
|
131
|
-
const avgSpacing = spacings0.reduce((a, b) => a + b) / spacings0.length;
|
|
132
|
-
spacings0.forEach(s => assert.approximately(s, avgSpacing, 1e-10));
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should handle subsets of different sizes', () => {
|
|
137
|
-
const graph = {
|
|
138
|
-
nodes: () => ['A', 'B0', 'B1', 'C0', 'C1', 'C2', 'C3'],
|
|
139
|
-
edges: () => [['A', 'B0'], ['A', 'B1'], ['B0', 'C0'], ['B1', 'C2']]
|
|
140
|
-
};
|
|
141
|
-
const subsets = [['A'], ['B0', 'B1'], ['C0', 'C1', 'C2', 'C3']];
|
|
142
|
-
|
|
143
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
144
|
-
|
|
145
|
-
// Single node should be centered
|
|
146
|
-
assert.equal(positions['A'][1], 0);
|
|
147
|
-
|
|
148
|
-
// Larger subsets should still be evenly spaced
|
|
149
|
-
const cPositions = ['C0', 'C1', 'C2', 'C3'].map(n => positions[n][1]).sort((a, b) => a - b);
|
|
150
|
-
const span = cPositions[cPositions.length - 1] - cPositions[0];
|
|
151
|
-
assert.isAbove(span, 0);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('Parameter variations', () => {
|
|
156
|
-
it('should respect scale parameter', () => {
|
|
157
|
-
const graph = completeGraph(12);
|
|
158
|
-
const subsets = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]];
|
|
159
|
-
|
|
160
|
-
const positions1 = multipartiteLayout(graph, subsets, 'vertical', 1);
|
|
161
|
-
const positions2 = multipartiteLayout(graph, subsets, 'vertical', 2);
|
|
162
|
-
|
|
163
|
-
// Calculate layout spans
|
|
164
|
-
const span1 = getLayoutSpan(positions1);
|
|
165
|
-
const span2 = getLayoutSpan(positions2);
|
|
166
|
-
|
|
167
|
-
assert.approximately(span2.width / span1.width, 2, 0.1);
|
|
168
|
-
assert.approximately(span2.height / span1.height, 2, 0.1);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should respect center parameter', () => {
|
|
172
|
-
const graph = completeGraph(6);
|
|
173
|
-
const subsets = [[0, 1], [2, 3], [4, 5]];
|
|
174
|
-
const center = [10, -5];
|
|
175
|
-
|
|
176
|
-
const positions = multipartiteLayout(graph, subsets, 'vertical', 1, center);
|
|
177
|
-
|
|
178
|
-
// Calculate center of mass
|
|
179
|
-
const com = getCenterOfMass(positions);
|
|
180
|
-
|
|
181
|
-
assert.approximately(com[0], center[0], 0.5);
|
|
182
|
-
assert.approximately(com[1], center[1], 0.5);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should respect alignment parameter', () => {
|
|
186
|
-
const graph = completeGraph(9);
|
|
187
|
-
const subsets = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
|
|
188
|
-
|
|
189
|
-
const verticalPos = multipartiteLayout(graph, subsets, 'vertical');
|
|
190
|
-
const horizontalPos = multipartiteLayout(graph, subsets, 'horizontal');
|
|
191
|
-
|
|
192
|
-
// In vertical, subsets differ in x; in horizontal, subsets differ in y
|
|
193
|
-
const vDiffX = Math.abs(verticalPos[0][0] - verticalPos[3][0]);
|
|
194
|
-
const vDiffY = Math.abs(verticalPos[0][1] - verticalPos[3][1]);
|
|
195
|
-
|
|
196
|
-
const hDiffX = Math.abs(horizontalPos[0][0] - horizontalPos[3][0]);
|
|
197
|
-
const hDiffY = Math.abs(horizontalPos[0][1] - horizontalPos[3][1]);
|
|
198
|
-
|
|
199
|
-
// Vertical: large X difference, potentially small Y difference
|
|
200
|
-
assert.isAbove(vDiffX, 0.5);
|
|
201
|
-
|
|
202
|
-
// Horizontal: potentially small X difference, large Y difference
|
|
203
|
-
assert.isAbove(hDiffY, 0.5);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('Special cases and edge cases', () => {
|
|
208
|
-
it('should handle string node IDs', () => {
|
|
209
|
-
const graph = {
|
|
210
|
-
nodes: () => ['red1', 'red2', 'green1', 'green2', 'blue1', 'blue2'],
|
|
211
|
-
edges: () => [['red1', 'green1'], ['red2', 'green2'], ['green1', 'blue1'], ['green2', 'blue2']]
|
|
212
|
-
};
|
|
213
|
-
const subsets = [['red1', 'red2'], ['green1', 'green2'], ['blue1', 'blue2']];
|
|
214
|
-
|
|
215
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
216
|
-
|
|
217
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
218
|
-
graph.nodes().forEach(node => {
|
|
219
|
-
assert.isDefined(positions[node]);
|
|
220
|
-
assert.equal(positions[node].length, 2);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should handle nodes not in any subset', () => {
|
|
225
|
-
const graph = {
|
|
226
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
227
|
-
edges: () => [[0, 2], [1, 3], [2, 4]]
|
|
228
|
-
};
|
|
229
|
-
const subsets = [[0, 1], [2, 3]]; // Nodes 4, 5 not in subsets
|
|
230
|
-
|
|
231
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
232
|
-
|
|
233
|
-
// Only nodes in subsets should be positioned
|
|
234
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
235
|
-
assert.isUndefined(positions[4]);
|
|
236
|
-
assert.isUndefined(positions[5]);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should handle empty subsets', () => {
|
|
240
|
-
const graph = completeGraph(6);
|
|
241
|
-
const subsets = [
|
|
242
|
-
[0, 1],
|
|
243
|
-
[], // Empty subset
|
|
244
|
-
[2, 3, 4, 5]
|
|
245
|
-
];
|
|
246
|
-
|
|
247
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
248
|
-
|
|
249
|
-
// Should position nodes from non-empty subsets
|
|
250
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should produce consistent results', () => {
|
|
254
|
-
const graph = randomGraph(15, 0.3, 42);
|
|
255
|
-
const subsets = [
|
|
256
|
-
graph.nodes().slice(0, 5),
|
|
257
|
-
graph.nodes().slice(5, 10),
|
|
258
|
-
graph.nodes().slice(10, 15)
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
const positions1 = multipartiteLayout(graph, subsets);
|
|
262
|
-
const positions2 = multipartiteLayout(graph, subsets);
|
|
263
|
-
|
|
264
|
-
// Multipartite layout is deterministic
|
|
265
|
-
graph.nodes().forEach(node => {
|
|
266
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('should handle complete k-partite graphs', () => {
|
|
271
|
-
// Complete 3-partite graph K(2,3,4)
|
|
272
|
-
const graph = {
|
|
273
|
-
nodes: () => ['A0', 'A1', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2', 'C3'],
|
|
274
|
-
edges: () => []
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// Add all edges between different subsets
|
|
278
|
-
const subsets = [['A0', 'A1'], ['B0', 'B1', 'B2'], ['C0', 'C1', 'C2', 'C3']];
|
|
279
|
-
for (let i = 0; i < subsets.length; i++) {
|
|
280
|
-
for (let j = i + 1; j < subsets.length; j++) {
|
|
281
|
-
for (const u of subsets[i]) {
|
|
282
|
-
for (const v of subsets[j]) {
|
|
283
|
-
graph.edges().push([u, v]);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
290
|
-
|
|
291
|
-
assert.equal(Object.keys(positions).length, 9);
|
|
292
|
-
|
|
293
|
-
// Check that subsets are well-separated
|
|
294
|
-
const avgX = subsets.map(subset =>
|
|
295
|
-
subset.reduce((sum, n) => sum + positions[n][0], 0) / subset.length
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
assert.notEqual(avgX[0], avgX[1]);
|
|
299
|
-
assert.notEqual(avgX[1], avgX[2]);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('should handle large multipartite graphs efficiently', () => {
|
|
303
|
-
const n = 60; // 20 nodes per subset
|
|
304
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
305
|
-
const graph = {
|
|
306
|
-
nodes: () => nodes,
|
|
307
|
-
edges: () => []
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
const subsets = [
|
|
311
|
-
nodes.slice(0, 20),
|
|
312
|
-
nodes.slice(20, 40),
|
|
313
|
-
nodes.slice(40, 60)
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
const startTime = performance.now();
|
|
317
|
-
const positions = multipartiteLayout(graph, subsets);
|
|
318
|
-
const endTime = performance.now();
|
|
319
|
-
|
|
320
|
-
assert.equal(Object.keys(positions).length, 60);
|
|
321
|
-
assert.isBelow(endTime - startTime, 100); // Should be very fast
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
describe('Layout quality', () => {
|
|
326
|
-
it('should minimize edge crossings between adjacent layers', () => {
|
|
327
|
-
// Create a graph with clear layer structure
|
|
328
|
-
const graph = {
|
|
329
|
-
nodes: () => ['A0', 'A1', 'A2', 'B0', 'B1', 'B2', 'C0', 'C1', 'C2'],
|
|
330
|
-
edges: () => [
|
|
331
|
-
['A0', 'B0'], ['A1', 'B1'], ['A2', 'B2'], // Parallel edges
|
|
332
|
-
['B0', 'C0'], ['B1', 'C1'], ['B2', 'C2'] // Parallel edges
|
|
333
|
-
]
|
|
334
|
-
};
|
|
335
|
-
const subsets = [['A0', 'A1', 'A2'], ['B0', 'B1', 'B2'], ['C0', 'C1', 'C2']];
|
|
336
|
-
|
|
337
|
-
const positions = multipartiteLayout(graph, subsets, 'vertical');
|
|
338
|
-
|
|
339
|
-
// Check that connected nodes have similar y-coordinates
|
|
340
|
-
graph.edges().forEach(([u, v]) => {
|
|
341
|
-
const yDiff = Math.abs(positions[u][1] - positions[v][1]);
|
|
342
|
-
assert.isBelow(yDiff, 2); // Connected nodes should be roughly aligned
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should create clear visual separation between layers', () => {
|
|
347
|
-
const graph = completeGraph(15);
|
|
348
|
-
const subsets = [
|
|
349
|
-
[0, 1, 2, 3, 4],
|
|
350
|
-
[5, 6, 7, 8, 9],
|
|
351
|
-
[10, 11, 12, 13, 14]
|
|
352
|
-
];
|
|
353
|
-
|
|
354
|
-
const positions = multipartiteLayout(graph, subsets, 'vertical');
|
|
355
|
-
|
|
356
|
-
// Calculate min distance between layers
|
|
357
|
-
let minInterLayerDist = Infinity;
|
|
358
|
-
for (let i = 0; i < subsets.length - 1; i++) {
|
|
359
|
-
for (const u of subsets[i]) {
|
|
360
|
-
for (const v of subsets[i + 1]) {
|
|
361
|
-
const dist = Math.abs(positions[u][0] - positions[v][0]);
|
|
362
|
-
minInterLayerDist = Math.min(minInterLayerDist, dist);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Layers should be clearly separated
|
|
368
|
-
assert.isAbove(minInterLayerDist, 0.4); // Allow slightly closer layers
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Helper functions
|
|
374
|
-
function getLayoutSpan(positions) {
|
|
375
|
-
const nodes = Object.keys(positions);
|
|
376
|
-
let minX = Infinity, maxX = -Infinity;
|
|
377
|
-
let minY = Infinity, maxY = -Infinity;
|
|
378
|
-
|
|
379
|
-
nodes.forEach(node => {
|
|
380
|
-
minX = Math.min(minX, positions[node][0]);
|
|
381
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
382
|
-
minY = Math.min(minY, positions[node][1]);
|
|
383
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
width: maxX - minX,
|
|
388
|
-
height: maxY - minY
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function getCenterOfMass(positions) {
|
|
393
|
-
const nodes = Object.keys(positions);
|
|
394
|
-
const com = [0, 0];
|
|
395
|
-
|
|
396
|
-
nodes.forEach(node => {
|
|
397
|
-
com[0] += positions[node][0];
|
|
398
|
-
com[1] += positions[node][1];
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
com[0] /= nodes.length;
|
|
402
|
-
com[1] /= nodes.length;
|
|
403
|
-
return com;
|
|
404
|
-
}
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
planarLayout,
|
|
4
|
-
completeGraph,
|
|
5
|
-
cycleGraph,
|
|
6
|
-
starGraph,
|
|
7
|
-
gridGraph,
|
|
8
|
-
wheelGraph
|
|
9
|
-
} from '../src';
|
|
10
|
-
|
|
11
|
-
describe('Planar Layout', () => {
|
|
12
|
-
describe('Basic functionality', () => {
|
|
13
|
-
it('should position all nodes for planar graphs', () => {
|
|
14
|
-
// Cycle graph is planar
|
|
15
|
-
const graph = cycleGraph(6);
|
|
16
|
-
const positions = planarLayout(graph);
|
|
17
|
-
|
|
18
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
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 = planarLayout(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 = planarLayout(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 throw error for non-planar graphs', () => {
|
|
44
|
-
// K5 (complete graph with 5 nodes) is non-planar
|
|
45
|
-
const graph = completeGraph(5);
|
|
46
|
-
|
|
47
|
-
assert.throws(() => {
|
|
48
|
-
planarLayout(graph);
|
|
49
|
-
}, 'G is not planar');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should only support 2D layouts', () => {
|
|
53
|
-
const graph = cycleGraph(4);
|
|
54
|
-
|
|
55
|
-
// 2D works
|
|
56
|
-
assert.doesNotThrow(() => {
|
|
57
|
-
planarLayout(graph, 1, [0, 0], 2);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// 3D should throw error
|
|
61
|
-
assert.throws(() => {
|
|
62
|
-
planarLayout(graph, 1, [0, 0], 3);
|
|
63
|
-
}, 'can only handle 2 dimensions');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('Planar graph types', () => {
|
|
68
|
-
it('should handle star graphs (which are planar)', () => {
|
|
69
|
-
const graph = starGraph(8);
|
|
70
|
-
const positions = planarLayout(graph);
|
|
71
|
-
|
|
72
|
-
assert.equal(Object.keys(positions).length, 8);
|
|
73
|
-
|
|
74
|
-
// Center node should be distinguishable
|
|
75
|
-
const centerPos = positions[0];
|
|
76
|
-
assert.isDefined(centerPos);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should handle grid graphs (which are planar)', () => {
|
|
80
|
-
const graph = gridGraph(3, 3);
|
|
81
|
-
const positions = planarLayout(graph);
|
|
82
|
-
|
|
83
|
-
assert.equal(Object.keys(positions).length, 9);
|
|
84
|
-
|
|
85
|
-
// All nodes should be positioned
|
|
86
|
-
graph.nodes().forEach(node => {
|
|
87
|
-
assert.isDefined(positions[node]);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should handle wheel graphs (which are planar)', () => {
|
|
92
|
-
const graph = wheelGraph(6);
|
|
93
|
-
const positions = planarLayout(graph);
|
|
94
|
-
|
|
95
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should handle tree structures', () => {
|
|
99
|
-
const tree = {
|
|
100
|
-
nodes: () => [0, 1, 2, 3, 4, 5, 6],
|
|
101
|
-
edges: () => [[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [2, 6]]
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const positions = planarLayout(tree);
|
|
105
|
-
assert.equal(Object.keys(positions).length, 7);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('Parameter variations', () => {
|
|
110
|
-
it('should respect scale parameter', () => {
|
|
111
|
-
const graph = cycleGraph(8);
|
|
112
|
-
|
|
113
|
-
const positions1 = planarLayout(graph, 1);
|
|
114
|
-
const positions2 = planarLayout(graph, 2);
|
|
115
|
-
|
|
116
|
-
// Calculate bounding boxes
|
|
117
|
-
const bounds1 = getLayoutBounds(positions1);
|
|
118
|
-
const bounds2 = getLayoutBounds(positions2);
|
|
119
|
-
|
|
120
|
-
assert.approximately(bounds2.width / bounds1.width, 2, 0.1);
|
|
121
|
-
assert.approximately(bounds2.height / bounds1.height, 2, 0.1);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should respect center parameter', () => {
|
|
125
|
-
const graph = starGraph(5);
|
|
126
|
-
const center = [10, -5];
|
|
127
|
-
|
|
128
|
-
const positions = planarLayout(graph, 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
|
-
|
|
138
|
-
describe('Special cases and edge cases', () => {
|
|
139
|
-
it('should handle string node IDs', () => {
|
|
140
|
-
const graph = {
|
|
141
|
-
nodes: () => ['A', 'B', 'C', 'D'],
|
|
142
|
-
edges: () => [['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'A']]
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const positions = planarLayout(graph);
|
|
146
|
-
|
|
147
|
-
assert.equal(Object.keys(positions).length, 4);
|
|
148
|
-
['A', 'B', 'C', 'D'].forEach(node => {
|
|
149
|
-
assert.isDefined(positions[node]);
|
|
150
|
-
assert.equal(positions[node].length, 2);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should produce consistent results', () => {
|
|
155
|
-
const graph = gridGraph(4, 4);
|
|
156
|
-
|
|
157
|
-
const positions1 = planarLayout(graph);
|
|
158
|
-
const positions2 = planarLayout(graph);
|
|
159
|
-
|
|
160
|
-
// Planar layout should produce valid results
|
|
161
|
-
assert.equal(Object.keys(positions1).length, 16);
|
|
162
|
-
assert.equal(Object.keys(positions2).length, 16);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should handle disconnected planar components', () => {
|
|
166
|
-
const graph = {
|
|
167
|
-
nodes: () => [0, 1, 2, 3, 4, 5],
|
|
168
|
-
edges: () => [[0, 1], [1, 2], [3, 4], [4, 5]] // Two triangles
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const positions = planarLayout(graph);
|
|
172
|
-
|
|
173
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
174
|
-
graph.nodes().forEach(node => {
|
|
175
|
-
assert.isDefined(positions[node]);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should reject K3,3 (complete bipartite non-planar)', () => {
|
|
180
|
-
const k33 = {
|
|
181
|
-
nodes: () => ['A1', 'A2', 'A3', 'B1', 'B2', 'B3'],
|
|
182
|
-
edges: () => [
|
|
183
|
-
['A1', 'B1'], ['A1', 'B2'], ['A1', 'B3'],
|
|
184
|
-
['A2', 'B1'], ['A2', 'B2'], ['A2', 'B3'],
|
|
185
|
-
['A3', 'B1'], ['A3', 'B2'], ['A3', 'B3']
|
|
186
|
-
]
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
assert.throws(() => {
|
|
190
|
-
planarLayout(k33);
|
|
191
|
-
}, 'G is not planar');
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe('Layout quality', () => {
|
|
196
|
-
it('should produce non-crossing edges for planar graphs', () => {
|
|
197
|
-
const graph = cycleGraph(10);
|
|
198
|
-
const positions = planarLayout(graph);
|
|
199
|
-
|
|
200
|
-
// While we can't easily verify no crossings algorithmically,
|
|
201
|
-
// we can check that nodes are well-separated
|
|
202
|
-
const minDist = getMinimumDistance(positions);
|
|
203
|
-
assert.isAbove(minDist, 0.1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should handle embedded graphs efficiently', () => {
|
|
207
|
-
const graph = gridGraph(5, 5); // 25 nodes
|
|
208
|
-
|
|
209
|
-
const startTime = performance.now();
|
|
210
|
-
const positions = planarLayout(graph);
|
|
211
|
-
const endTime = performance.now();
|
|
212
|
-
|
|
213
|
-
assert.equal(Object.keys(positions).length, 25);
|
|
214
|
-
assert.isBelow(endTime - startTime, 100); // Should be fast
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Helper functions
|
|
220
|
-
function getLayoutBounds(positions) {
|
|
221
|
-
const nodes = Object.keys(positions);
|
|
222
|
-
let minX = Infinity, maxX = -Infinity;
|
|
223
|
-
let minY = Infinity, maxY = -Infinity;
|
|
224
|
-
|
|
225
|
-
nodes.forEach(node => {
|
|
226
|
-
minX = Math.min(minX, positions[node][0]);
|
|
227
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
228
|
-
minY = Math.min(minY, positions[node][1]);
|
|
229
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
width: maxX - minX,
|
|
234
|
-
height: maxY - minY
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getCenterOfMass(positions) {
|
|
239
|
-
const nodes = Object.keys(positions);
|
|
240
|
-
const com = [0, 0];
|
|
241
|
-
|
|
242
|
-
nodes.forEach(node => {
|
|
243
|
-
com[0] += positions[node][0];
|
|
244
|
-
com[1] += positions[node][1];
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
com[0] /= nodes.length;
|
|
248
|
-
com[1] /= nodes.length;
|
|
249
|
-
return com;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getMinimumDistance(positions) {
|
|
253
|
-
const nodes = Object.keys(positions);
|
|
254
|
-
let minDist = Infinity;
|
|
255
|
-
|
|
256
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
257
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
258
|
-
const dx = positions[nodes[i]][0] - positions[nodes[j]][0];
|
|
259
|
-
const dy = positions[nodes[i]][1] - positions[nodes[j]][1];
|
|
260
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
261
|
-
minDist = Math.min(minDist, dist);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return minDist;
|
|
266
|
-
}
|