@graphty/layout 1.2.3 → 1.2.5
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 +340 -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 +194 -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 +19 -4
- package/src/layouts/force-directed/forceatlas2.ts +23 -4
- package/src/utils/rescale.ts +90 -15
- 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,344 +0,0 @@
|
|
|
1
|
-
import { describe, it, assert } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
bipartiteLayout,
|
|
4
|
-
bipartiteGraph
|
|
5
|
-
} from '../src';
|
|
6
|
-
|
|
7
|
-
describe('Bipartite Layout', () => {
|
|
8
|
-
describe('Basic functionality', () => {
|
|
9
|
-
it('should position all nodes', () => {
|
|
10
|
-
const graph = bipartiteGraph(3, 4, 0.5, 42);
|
|
11
|
-
const positions = bipartiteLayout(graph, graph.setA);
|
|
12
|
-
|
|
13
|
-
assert.equal(Object.keys(positions).length, 7);
|
|
14
|
-
graph.nodes().forEach(node => {
|
|
15
|
-
assert.isDefined(positions[node]);
|
|
16
|
-
assert.equal(positions[node].length, 2);
|
|
17
|
-
assert.isNumber(positions[node][0]);
|
|
18
|
-
assert.isNumber(positions[node][1]);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should handle empty graph', () => {
|
|
23
|
-
const emptyGraph = { nodes: () => [], edges: () => [] };
|
|
24
|
-
const positions = bipartiteLayout(emptyGraph, []);
|
|
25
|
-
|
|
26
|
-
assert.equal(Object.keys(positions).length, 0);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should handle single node', () => {
|
|
30
|
-
const singleNode = { nodes: () => ['A'], edges: () => [] };
|
|
31
|
-
const positions = bipartiteLayout(singleNode, ['A']);
|
|
32
|
-
|
|
33
|
-
assert.equal(Object.keys(positions).length, 1);
|
|
34
|
-
assert.deepEqual(positions['A'], [0, 0]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should handle disconnected bipartite graph', () => {
|
|
38
|
-
const graph = {
|
|
39
|
-
nodes: () => ['A0', 'A1', 'B0', 'B1', 'B2'],
|
|
40
|
-
edges: () => [['A0', 'B0'], ['A1', 'B2']] // B1 is disconnected
|
|
41
|
-
};
|
|
42
|
-
const setA = ['A0', 'A1'];
|
|
43
|
-
|
|
44
|
-
const positions = bipartiteLayout(graph, setA);
|
|
45
|
-
|
|
46
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
47
|
-
graph.nodes().forEach(node => {
|
|
48
|
-
assert.isDefined(positions[node]);
|
|
49
|
-
assert.equal(positions[node].length, 2);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('Bipartite arrangement', () => {
|
|
55
|
-
it('should separate sets on opposite sides for vertical alignment', () => {
|
|
56
|
-
const graph = bipartiteGraph(4, 5, 0.7, 123);
|
|
57
|
-
const positions = bipartiteLayout(graph, graph.setA, 'vertical');
|
|
58
|
-
|
|
59
|
-
// Get x-coordinates for each set
|
|
60
|
-
const setAX = graph.setA.map(node => positions[node][0]);
|
|
61
|
-
const setBX = graph.setB.map(node => positions[node][0]);
|
|
62
|
-
|
|
63
|
-
const avgAX = setAX.reduce((a, b) => a + b) / setAX.length;
|
|
64
|
-
const avgBX = setBX.reduce((a, b) => a + b) / setBX.length;
|
|
65
|
-
|
|
66
|
-
// Sets should be on opposite sides (significant x difference)
|
|
67
|
-
assert.isAbove(Math.abs(avgAX - avgBX), 1);
|
|
68
|
-
|
|
69
|
-
// All nodes in a set should have same x-coordinate
|
|
70
|
-
setAX.forEach(x => assert.approximately(x, setAX[0], 1e-10));
|
|
71
|
-
setBX.forEach(x => assert.approximately(x, setBX[0], 1e-10));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should separate sets on opposite sides for horizontal alignment', () => {
|
|
75
|
-
const graph = bipartiteGraph(3, 4, 0.8, 456);
|
|
76
|
-
const positions = bipartiteLayout(graph, graph.setA, 'horizontal');
|
|
77
|
-
|
|
78
|
-
// Get y-coordinates for each set
|
|
79
|
-
const setAY = graph.setA.map(node => positions[node][1]);
|
|
80
|
-
const setBY = graph.setB.map(node => positions[node][1]);
|
|
81
|
-
|
|
82
|
-
const avgAY = setAY.reduce((a, b) => a + b) / setAY.length;
|
|
83
|
-
const avgBY = setBY.reduce((a, b) => a + b) / setBY.length;
|
|
84
|
-
|
|
85
|
-
// Sets should be on opposite sides (significant y difference)
|
|
86
|
-
assert.isAbove(Math.abs(avgAY - avgBY), 1);
|
|
87
|
-
|
|
88
|
-
// All nodes in a set should have same y-coordinate
|
|
89
|
-
setAY.forEach(y => assert.approximately(y, setAY[0], 1e-10));
|
|
90
|
-
setBY.forEach(y => assert.approximately(y, setBY[0], 1e-10));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should evenly space nodes within each set', () => {
|
|
94
|
-
const graph = {
|
|
95
|
-
nodes: () => ['A0', 'A1', 'A2', 'A3', 'B0', 'B1', 'B2'],
|
|
96
|
-
edges: () => [['A0', 'B0'], ['A1', 'B1'], ['A2', 'B2'], ['A3', 'B0']]
|
|
97
|
-
};
|
|
98
|
-
const setA = ['A0', 'A1', 'A2', 'A3'];
|
|
99
|
-
|
|
100
|
-
const positions = bipartiteLayout(graph, setA, 'vertical');
|
|
101
|
-
|
|
102
|
-
// Check spacing in set A
|
|
103
|
-
const aPositions = setA.map(node => positions[node][1]).sort((a, b) => a - b);
|
|
104
|
-
const aSpacings = [];
|
|
105
|
-
for (let i = 1; i < aPositions.length; i++) {
|
|
106
|
-
aSpacings.push(aPositions[i] - aPositions[i-1]);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// All spacings should be equal
|
|
110
|
-
const avgSpacing = aSpacings.reduce((a, b) => a + b) / aSpacings.length;
|
|
111
|
-
aSpacings.forEach(s => assert.approximately(s, avgSpacing, 1e-10));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should handle sets of different sizes', () => {
|
|
115
|
-
const graph = {
|
|
116
|
-
nodes: () => ['A0', 'B0', 'B1', 'B2', 'B3', 'B4'],
|
|
117
|
-
edges: () => [['A0', 'B0'], ['A0', 'B1'], ['A0', 'B2']]
|
|
118
|
-
};
|
|
119
|
-
const setA = ['A0'];
|
|
120
|
-
|
|
121
|
-
const positions = bipartiteLayout(graph, setA);
|
|
122
|
-
|
|
123
|
-
// Single node in set A should be positioned
|
|
124
|
-
assert.isDefined(positions['A0']);
|
|
125
|
-
assert.equal(positions['A0'].length, 2);
|
|
126
|
-
|
|
127
|
-
// Set B nodes should be evenly distributed
|
|
128
|
-
const bNodes = ['B0', 'B1', 'B2', 'B3', 'B4'];
|
|
129
|
-
const bPositions = bNodes.map(node => positions[node][1]).sort((a, b) => a - b);
|
|
130
|
-
|
|
131
|
-
// Check that they span a reasonable range
|
|
132
|
-
const span = bPositions[bPositions.length - 1] - bPositions[0];
|
|
133
|
-
assert.isAbove(span, 0.5); // Allow smaller span
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('Parameter variations', () => {
|
|
138
|
-
it('should respect scale parameter', () => {
|
|
139
|
-
const graph = bipartiteGraph(3, 3, 1.0);
|
|
140
|
-
const positions1 = bipartiteLayout(graph, graph.setA, 'vertical', 1);
|
|
141
|
-
const positions2 = bipartiteLayout(graph, graph.setA, 'vertical', 2);
|
|
142
|
-
|
|
143
|
-
// Calculate spans for each layout
|
|
144
|
-
const span1 = getLayoutSpan(positions1);
|
|
145
|
-
const span2 = getLayoutSpan(positions2);
|
|
146
|
-
|
|
147
|
-
assert.approximately(span2.width / span1.width, 2, 0.1);
|
|
148
|
-
assert.approximately(span2.height / span1.height, 2, 0.1);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should respect center parameter', () => {
|
|
152
|
-
const graph = bipartiteGraph(2, 3, 0.6);
|
|
153
|
-
const center = [5, -3];
|
|
154
|
-
|
|
155
|
-
const positions = bipartiteLayout(graph, graph.setA, 'vertical', 1, center);
|
|
156
|
-
|
|
157
|
-
// Calculate center of mass
|
|
158
|
-
const com = getCenterOfMass(positions);
|
|
159
|
-
|
|
160
|
-
assert.approximately(com[0], center[0], 0.5);
|
|
161
|
-
assert.approximately(com[1], center[1], 0.5);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should respect aspect ratio parameter', () => {
|
|
165
|
-
const graph = bipartiteGraph(4, 4, 0.5);
|
|
166
|
-
|
|
167
|
-
const positions1 = bipartiteLayout(graph, graph.setA, 'vertical', 1, [0, 0], 0.5);
|
|
168
|
-
const positions2 = bipartiteLayout(graph, graph.setA, 'vertical', 1, [0, 0], 2.0);
|
|
169
|
-
|
|
170
|
-
const span1 = getLayoutSpan(positions1);
|
|
171
|
-
const span2 = getLayoutSpan(positions2);
|
|
172
|
-
|
|
173
|
-
const ratio1 = span1.height / span1.width;
|
|
174
|
-
const ratio2 = span2.height / span2.width;
|
|
175
|
-
|
|
176
|
-
// Different aspect ratios should produce different layouts
|
|
177
|
-
assert.notEqual(ratio1, ratio2);
|
|
178
|
-
// Just check they are different, don't assume order
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('should handle alignment parameter correctly', () => {
|
|
182
|
-
const graph = bipartiteGraph(3, 4, 0.7);
|
|
183
|
-
|
|
184
|
-
const verticalPos = bipartiteLayout(graph, graph.setA, 'vertical');
|
|
185
|
-
const horizontalPos = bipartiteLayout(graph, graph.setA, 'horizontal');
|
|
186
|
-
|
|
187
|
-
// In vertical, sets differ in x; in horizontal, sets differ in y
|
|
188
|
-
const vDiffX = Math.abs(verticalPos[graph.setA[0]][0] - verticalPos[graph.setB[0]][0]);
|
|
189
|
-
const vDiffY = Math.abs(verticalPos[graph.setA[0]][1] - verticalPos[graph.setB[0]][1]);
|
|
190
|
-
|
|
191
|
-
const hDiffX = Math.abs(horizontalPos[graph.setA[0]][0] - horizontalPos[graph.setB[0]][0]);
|
|
192
|
-
const hDiffY = Math.abs(horizontalPos[graph.setA[0]][1] - horizontalPos[graph.setB[0]][1]);
|
|
193
|
-
|
|
194
|
-
// Vertical: large X difference, small Y difference
|
|
195
|
-
assert.isAbove(vDiffX, 1);
|
|
196
|
-
assert.isBelow(vDiffY, 1);
|
|
197
|
-
|
|
198
|
-
// Horizontal: small X difference, large Y difference
|
|
199
|
-
assert.isBelow(hDiffX, 1);
|
|
200
|
-
assert.isAbove(hDiffY, 1);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
describe('Special cases and edge cases', () => {
|
|
205
|
-
it('should handle string node IDs', () => {
|
|
206
|
-
const graph = {
|
|
207
|
-
nodes: () => ['user1', 'user2', 'user3', 'item1', 'item2'],
|
|
208
|
-
edges: () => [['user1', 'item1'], ['user2', 'item1'], ['user2', 'item2'], ['user3', 'item2']]
|
|
209
|
-
};
|
|
210
|
-
const users = ['user1', 'user2', 'user3'];
|
|
211
|
-
|
|
212
|
-
const positions = bipartiteLayout(graph, users);
|
|
213
|
-
|
|
214
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
215
|
-
graph.nodes().forEach(node => {
|
|
216
|
-
assert.isDefined(positions[node]);
|
|
217
|
-
assert.equal(positions[node].length, 2);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should handle nodes not in specified set', () => {
|
|
222
|
-
const graph = {
|
|
223
|
-
nodes: () => [0, 1, 2, 3, 4],
|
|
224
|
-
edges: () => [[0, 2], [0, 3], [1, 3], [1, 4]]
|
|
225
|
-
};
|
|
226
|
-
const setA = [0, 1]; // 2, 3, 4 are implicitly in set B
|
|
227
|
-
|
|
228
|
-
const positions = bipartiteLayout(graph, setA);
|
|
229
|
-
|
|
230
|
-
// All nodes should be positioned
|
|
231
|
-
assert.equal(Object.keys(positions).length, 5);
|
|
232
|
-
|
|
233
|
-
// Check that sets are separated
|
|
234
|
-
const aX = positions[0][0];
|
|
235
|
-
const bX = positions[2][0];
|
|
236
|
-
assert.notEqual(aX, bX);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should produce consistent results', () => {
|
|
240
|
-
const graph = bipartiteGraph(5, 6, 0.4, 789);
|
|
241
|
-
|
|
242
|
-
const positions1 = bipartiteLayout(graph, graph.setA);
|
|
243
|
-
const positions2 = bipartiteLayout(graph, graph.setA);
|
|
244
|
-
|
|
245
|
-
// Bipartite layout is deterministic
|
|
246
|
-
graph.nodes().forEach(node => {
|
|
247
|
-
assert.deepEqual(positions1[node], positions2[node]);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('should handle complete bipartite graphs', () => {
|
|
252
|
-
// Complete bipartite graph K(3,3)
|
|
253
|
-
const graph = {
|
|
254
|
-
nodes: () => ['A0', 'A1', 'A2', 'B0', 'B1', 'B2'],
|
|
255
|
-
edges: () => [
|
|
256
|
-
['A0', 'B0'], ['A0', 'B1'], ['A0', 'B2'],
|
|
257
|
-
['A1', 'B0'], ['A1', 'B1'], ['A1', 'B2'],
|
|
258
|
-
['A2', 'B0'], ['A2', 'B1'], ['A2', 'B2']
|
|
259
|
-
]
|
|
260
|
-
};
|
|
261
|
-
const setA = ['A0', 'A1', 'A2'];
|
|
262
|
-
|
|
263
|
-
const positions = bipartiteLayout(graph, setA);
|
|
264
|
-
|
|
265
|
-
// Should produce clean bipartite visualization
|
|
266
|
-
assert.equal(Object.keys(positions).length, 6);
|
|
267
|
-
|
|
268
|
-
// All nodes in each set should align
|
|
269
|
-
const aXs = setA.map(n => positions[n][0]);
|
|
270
|
-
assert.isTrue(aXs.every(x => Math.abs(x - aXs[0]) < 1e-10));
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should handle large bipartite graphs efficiently', () => {
|
|
274
|
-
const graph = bipartiteGraph(50, 50, 0.1, 12345);
|
|
275
|
-
|
|
276
|
-
const startTime = performance.now();
|
|
277
|
-
const positions = bipartiteLayout(graph, graph.setA);
|
|
278
|
-
const endTime = performance.now();
|
|
279
|
-
|
|
280
|
-
assert.equal(Object.keys(positions).length, 100);
|
|
281
|
-
assert.isBelow(endTime - startTime, 100); // Should be very fast
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe('Layout quality', () => {
|
|
286
|
-
it('should minimize edge crossings', () => {
|
|
287
|
-
// Create a graph where ordering matters
|
|
288
|
-
const graph = {
|
|
289
|
-
nodes: () => ['A0', 'A1', 'A2', 'B0', 'B1', 'B2'],
|
|
290
|
-
edges: () => [
|
|
291
|
-
['A0', 'B0'], ['A1', 'B1'], ['A2', 'B2'] // No crossings if ordered properly
|
|
292
|
-
]
|
|
293
|
-
};
|
|
294
|
-
const setA = ['A0', 'A1', 'A2'];
|
|
295
|
-
|
|
296
|
-
const positions = bipartiteLayout(graph, setA);
|
|
297
|
-
|
|
298
|
-
// Check that connected nodes have similar y-coordinates
|
|
299
|
-
// This suggests minimal edge crossings
|
|
300
|
-
const edgeLengths = graph.edges().map(([u, v]) => {
|
|
301
|
-
return Math.abs(positions[u][1] - positions[v][1]);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// All edges should have similar vertical spans (suggesting no crossings)
|
|
305
|
-
const avgLength = edgeLengths.reduce((a, b) => a + b) / edgeLengths.length;
|
|
306
|
-
edgeLengths.forEach(len => {
|
|
307
|
-
assert.isBelow(Math.abs(len - avgLength), 0.5);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Helper functions
|
|
314
|
-
function getLayoutSpan(positions) {
|
|
315
|
-
const nodes = Object.keys(positions);
|
|
316
|
-
let minX = Infinity, maxX = -Infinity;
|
|
317
|
-
let minY = Infinity, maxY = -Infinity;
|
|
318
|
-
|
|
319
|
-
nodes.forEach(node => {
|
|
320
|
-
minX = Math.min(minX, positions[node][0]);
|
|
321
|
-
maxX = Math.max(maxX, positions[node][0]);
|
|
322
|
-
minY = Math.min(minY, positions[node][1]);
|
|
323
|
-
maxY = Math.max(maxY, positions[node][1]);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
width: maxX - minX,
|
|
328
|
-
height: maxY - minY
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function getCenterOfMass(positions) {
|
|
333
|
-
const nodes = Object.keys(positions);
|
|
334
|
-
const com = [0, 0];
|
|
335
|
-
|
|
336
|
-
nodes.forEach(node => {
|
|
337
|
-
com[0] += positions[node][0];
|
|
338
|
-
com[1] += positions[node][1];
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
com[0] /= nodes.length;
|
|
342
|
-
com[1] /= nodes.length;
|
|
343
|
-
return com;
|
|
344
|
-
}
|