@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
package/examples/layout.js
DELETED
|
@@ -1,2304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout
|
|
3
|
-
* ======
|
|
4
|
-
*
|
|
5
|
-
* Node positioning algorithms for graph drawing in TypeScript.
|
|
6
|
-
*
|
|
7
|
-
* For `randomLayout()` the possible resulting shape
|
|
8
|
-
* is a square of side [0, scale] (default: [0, 1])
|
|
9
|
-
* Changing `center` shifts the layout by that amount.
|
|
10
|
-
*
|
|
11
|
-
* For the other layout routines, the extent is
|
|
12
|
-
* [center - scale, center + scale] (default: [-1, 1]).
|
|
13
|
-
*
|
|
14
|
-
* Ported from NetworkX Python library.
|
|
15
|
-
*/
|
|
16
|
-
// Utility array manipulation functions (NumPy-like)
|
|
17
|
-
const np = {
|
|
18
|
-
zeros: function (shape) {
|
|
19
|
-
if (typeof shape === 'number') {
|
|
20
|
-
return Array(shape).fill(0);
|
|
21
|
-
}
|
|
22
|
-
if (shape.length === 1) {
|
|
23
|
-
return Array(shape[0]).fill(0);
|
|
24
|
-
}
|
|
25
|
-
return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
|
|
26
|
-
},
|
|
27
|
-
ones: function (shape) {
|
|
28
|
-
if (typeof shape === 'number') {
|
|
29
|
-
return Array(shape).fill(1);
|
|
30
|
-
}
|
|
31
|
-
if (shape.length === 1) {
|
|
32
|
-
return Array(shape[0]).fill(1);
|
|
33
|
-
}
|
|
34
|
-
return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
|
|
35
|
-
},
|
|
36
|
-
linspace: function (start, stop, num) {
|
|
37
|
-
const step = (stop - start) / (num - 1);
|
|
38
|
-
return Array.from({ length: num }, (_, i) => start + i * step);
|
|
39
|
-
},
|
|
40
|
-
array: function (arr) {
|
|
41
|
-
return Array.isArray(arr) ? [...arr] : [arr];
|
|
42
|
-
},
|
|
43
|
-
repeat: function (a, repeats) {
|
|
44
|
-
const result = [];
|
|
45
|
-
for (let i = 0; i < repeats; i++) {
|
|
46
|
-
result.push(...np.array(a));
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
},
|
|
50
|
-
mean: function (arr, axis = null) {
|
|
51
|
-
if (axis === null) {
|
|
52
|
-
const flatArr = Array.isArray(arr[0])
|
|
53
|
-
? arr.flat(Infinity)
|
|
54
|
-
: arr;
|
|
55
|
-
const sum = flatArr.reduce((a, b) => a + b, 0);
|
|
56
|
-
return sum / flatArr.length;
|
|
57
|
-
}
|
|
58
|
-
if (axis === 0) {
|
|
59
|
-
const result = [];
|
|
60
|
-
const matrix = arr;
|
|
61
|
-
for (let i = 0; i < matrix[0].length; i++) {
|
|
62
|
-
let sum = 0;
|
|
63
|
-
for (let j = 0; j < matrix.length; j++) {
|
|
64
|
-
sum += matrix[j][i];
|
|
65
|
-
}
|
|
66
|
-
result.push(sum / matrix.length);
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
return arr.map(row => np.mean(row));
|
|
71
|
-
},
|
|
72
|
-
add: function (a, b) {
|
|
73
|
-
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
74
|
-
return a + b;
|
|
75
|
-
}
|
|
76
|
-
if (!Array.isArray(a)) {
|
|
77
|
-
return b.map(val => a + val);
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(b)) {
|
|
80
|
-
return a.map(val => val + b);
|
|
81
|
-
}
|
|
82
|
-
return a.map((val, i) => val + b[i]);
|
|
83
|
-
},
|
|
84
|
-
subtract: function (a, b) {
|
|
85
|
-
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
86
|
-
return a - b;
|
|
87
|
-
}
|
|
88
|
-
if (!Array.isArray(a)) {
|
|
89
|
-
return b.map(val => a - val);
|
|
90
|
-
}
|
|
91
|
-
if (!Array.isArray(b)) {
|
|
92
|
-
return a.map(val => val - b);
|
|
93
|
-
}
|
|
94
|
-
return a.map((val, i) => val - b[i]);
|
|
95
|
-
},
|
|
96
|
-
max: function (arr) {
|
|
97
|
-
if (!Array.isArray(arr))
|
|
98
|
-
return arr;
|
|
99
|
-
return Math.max(...arr.flat(Infinity));
|
|
100
|
-
},
|
|
101
|
-
min: function (arr) {
|
|
102
|
-
if (!Array.isArray(arr))
|
|
103
|
-
return arr;
|
|
104
|
-
return Math.min(...arr.flat(Infinity));
|
|
105
|
-
},
|
|
106
|
-
norm: function (arr) {
|
|
107
|
-
return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
// Random number generator (for seed-based randomization)
|
|
111
|
-
class RandomNumberGenerator {
|
|
112
|
-
constructor(seed) {
|
|
113
|
-
this.seed = seed || Math.floor(Math.random() * 1000000);
|
|
114
|
-
this.m = 2 ** 35 - 31;
|
|
115
|
-
this.a = 185852;
|
|
116
|
-
this.c = 1;
|
|
117
|
-
this._state = this.seed % this.m;
|
|
118
|
-
}
|
|
119
|
-
_next() {
|
|
120
|
-
this._state = (this.a * this._state + this.c) % this.m;
|
|
121
|
-
return this._state / this.m;
|
|
122
|
-
}
|
|
123
|
-
rand(shape = null) {
|
|
124
|
-
if (shape === null) {
|
|
125
|
-
return this._next();
|
|
126
|
-
}
|
|
127
|
-
if (typeof shape === 'number') {
|
|
128
|
-
const result = [];
|
|
129
|
-
for (let i = 0; i < shape; i++) {
|
|
130
|
-
result.push(this._next());
|
|
131
|
-
}
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
if (shape.length === 1) {
|
|
135
|
-
const result = [];
|
|
136
|
-
for (let i = 0; i < shape[0]; i++) {
|
|
137
|
-
result.push(this._next());
|
|
138
|
-
}
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
141
|
-
const result = [];
|
|
142
|
-
for (let i = 0; i < shape[0]; i++) {
|
|
143
|
-
result.push(this.rand(shape.slice(1)));
|
|
144
|
-
}
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Extract nodes from a graph object
|
|
150
|
-
*
|
|
151
|
-
* @param G - Graph or list of nodes
|
|
152
|
-
* @returns Array of nodes
|
|
153
|
-
*/
|
|
154
|
-
function getNodesFromGraph(G) {
|
|
155
|
-
return G.nodes ? G.nodes() : G;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Extract edges from a graph object
|
|
159
|
-
*
|
|
160
|
-
* @param G - Graph or list of nodes
|
|
161
|
-
* @returns Array of edges
|
|
162
|
-
*/
|
|
163
|
-
function getEdgesFromGraph(G) {
|
|
164
|
-
return G.edges ? G.edges() : [];
|
|
165
|
-
}
|
|
166
|
-
// Helper function similar to _process_params in Python version
|
|
167
|
-
function _processParams(G, center, dim) {
|
|
168
|
-
if (!center) {
|
|
169
|
-
center = Array(dim).fill(0);
|
|
170
|
-
}
|
|
171
|
-
if (center.length !== dim) {
|
|
172
|
-
throw new Error("length of center coordinates must match dimension of layout");
|
|
173
|
-
}
|
|
174
|
-
return { G, center };
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Position nodes uniformly at random in the unit square.
|
|
178
|
-
*
|
|
179
|
-
* @param G - Graph or list of nodes
|
|
180
|
-
* @param center - Coordinate pair around which to center the layout
|
|
181
|
-
* @param dim - Dimension of layout
|
|
182
|
-
* @param seed - Random seed for reproducible layouts
|
|
183
|
-
* @returns Positions dictionary keyed by node
|
|
184
|
-
*/
|
|
185
|
-
function randomLayout(G, center = null, dim = 2, seed = null) {
|
|
186
|
-
const processed = _processParams(G, center, dim);
|
|
187
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
188
|
-
center = processed.center;
|
|
189
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
190
|
-
const pos = {};
|
|
191
|
-
nodes.forEach((node) => {
|
|
192
|
-
pos[node] = rng.rand(dim).map((val, i) => val + center[i]);
|
|
193
|
-
});
|
|
194
|
-
return pos;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Position nodes on a circle (2D) or sphere (3D).
|
|
198
|
-
*
|
|
199
|
-
* @param G - Graph or list of nodes
|
|
200
|
-
* @param scale - Scale factor for positions
|
|
201
|
-
* @param center - Coordinate pair around which to center the layout
|
|
202
|
-
* @param dim - Dimension of layout (supports 2D circle or 3D sphere)
|
|
203
|
-
* @returns Positions dictionary keyed by node
|
|
204
|
-
*/
|
|
205
|
-
function circularLayout(G, scale = 1, center = null, dim = 2) {
|
|
206
|
-
if (dim < 2) {
|
|
207
|
-
throw new Error("cannot handle dimensions < 2");
|
|
208
|
-
}
|
|
209
|
-
const processed = _processParams(G, center, dim);
|
|
210
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
211
|
-
center = processed.center;
|
|
212
|
-
const pos = {};
|
|
213
|
-
if (nodes.length === 0) {
|
|
214
|
-
return pos;
|
|
215
|
-
}
|
|
216
|
-
if (nodes.length === 1) {
|
|
217
|
-
pos[nodes[0]] = center;
|
|
218
|
-
return pos;
|
|
219
|
-
}
|
|
220
|
-
if (dim === 2) {
|
|
221
|
-
// 2D circle layout
|
|
222
|
-
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
223
|
-
nodes.forEach((node, i) => {
|
|
224
|
-
const x = Math.cos(theta[i]) * scale + center[0];
|
|
225
|
-
const y = Math.sin(theta[i]) * scale + center[1];
|
|
226
|
-
pos[node] = [x, y];
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
else if (dim === 3) {
|
|
230
|
-
// 3D sphere layout using Fibonacci spiral
|
|
231
|
-
const n = nodes.length;
|
|
232
|
-
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
233
|
-
nodes.forEach((node, i) => {
|
|
234
|
-
// Use Fibonacci spiral for even distribution on sphere
|
|
235
|
-
const theta = 2 * Math.PI * i / goldenRatio;
|
|
236
|
-
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
237
|
-
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
238
|
-
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
239
|
-
const z = Math.cos(phi) * scale + center[2];
|
|
240
|
-
pos[node] = [x, y, z];
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
// For higher dimensions, fall back to random on hypersphere
|
|
245
|
-
const rng = new RandomNumberGenerator();
|
|
246
|
-
nodes.forEach((node) => {
|
|
247
|
-
// Generate random point on unit hypersphere
|
|
248
|
-
const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
249
|
-
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
250
|
-
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
return pos;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Position nodes in concentric circles.
|
|
257
|
-
*
|
|
258
|
-
* @param G - Graph or list of nodes
|
|
259
|
-
* @param nlist - List of node lists for each shell
|
|
260
|
-
* @param scale - Scale factor for positions
|
|
261
|
-
* @param center - Coordinate pair around which to center the layout
|
|
262
|
-
* @param dim - Dimension of layout (currently only supports dim=2)
|
|
263
|
-
* @returns Positions dictionary keyed by node
|
|
264
|
-
*/
|
|
265
|
-
function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
|
|
266
|
-
if (dim !== 2) {
|
|
267
|
-
throw new Error("can only handle 2 dimensions");
|
|
268
|
-
}
|
|
269
|
-
const processed = _processParams(G, center, dim);
|
|
270
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
271
|
-
center = processed.center;
|
|
272
|
-
const pos = {};
|
|
273
|
-
if (nodes.length === 0) {
|
|
274
|
-
return pos;
|
|
275
|
-
}
|
|
276
|
-
if (nodes.length === 1) {
|
|
277
|
-
pos[nodes[0]] = center;
|
|
278
|
-
return pos;
|
|
279
|
-
}
|
|
280
|
-
// If no nlist is specified, put all nodes in a single shell
|
|
281
|
-
if (!nlist) {
|
|
282
|
-
nlist = [nodes];
|
|
283
|
-
}
|
|
284
|
-
const radiusBump = scale / nlist.length;
|
|
285
|
-
let radius;
|
|
286
|
-
if (nlist[0].length === 1) {
|
|
287
|
-
// Single node at center
|
|
288
|
-
radius = 0;
|
|
289
|
-
pos[nlist[0][0]] = [...center];
|
|
290
|
-
radius += radiusBump;
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
// Start at radius 1
|
|
294
|
-
radius = radiusBump;
|
|
295
|
-
}
|
|
296
|
-
for (let i = 0; i < nlist.length; i++) {
|
|
297
|
-
const shell = nlist[i];
|
|
298
|
-
if (shell.length === 0)
|
|
299
|
-
continue;
|
|
300
|
-
if (shell.length === 1 && i === 0) {
|
|
301
|
-
// Already handled the case of a single center node
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
// Calculate positions on a circle
|
|
305
|
-
const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
|
|
306
|
-
shell.forEach((node, j) => {
|
|
307
|
-
const x = Math.cos(theta[j]) * radius + center[0];
|
|
308
|
-
const y = Math.sin(theta[j]) * radius + center[1];
|
|
309
|
-
pos[node] = [x, y];
|
|
310
|
-
});
|
|
311
|
-
radius += radiusBump;
|
|
312
|
-
}
|
|
313
|
-
return pos;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Position nodes using Fruchterman-Reingold force-directed algorithm.
|
|
317
|
-
*
|
|
318
|
-
* @param {Object} G - Graph or list of nodes
|
|
319
|
-
* @param {number} k - Optimal distance between nodes
|
|
320
|
-
* @param {Object} pos - Initial positions for nodes
|
|
321
|
-
* @param {Array} fixed - Nodes to keep fixed at initial position
|
|
322
|
-
* @param {number} iterations - Maximum number of iterations
|
|
323
|
-
* @param {number} scale - Scale factor for positions
|
|
324
|
-
* @param {Array|null} center - Coordinate pair around which to center the layout
|
|
325
|
-
* @param {number} dim - Dimension of layout
|
|
326
|
-
* @param {number} seed - Random seed for initial positions
|
|
327
|
-
* @returns {Object} Positions dictionary keyed by node
|
|
328
|
-
*/
|
|
329
|
-
function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
330
|
-
// Legacy compatibility alias
|
|
331
|
-
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Position nodes using Fruchterman-Reingold force-directed algorithm.
|
|
335
|
-
*
|
|
336
|
-
* @param {Object} G - Graph or list of nodes
|
|
337
|
-
* @param {number} k - Optimal distance between nodes
|
|
338
|
-
* @param {Object} pos - Initial positions for nodes
|
|
339
|
-
* @param {Array} fixed - Nodes to keep fixed at initial position
|
|
340
|
-
* @param {number} iterations - Maximum number of iterations
|
|
341
|
-
* @param {number} scale - Scale factor for positions
|
|
342
|
-
* @param {Array|null} center - Coordinate pair around which to center the layout
|
|
343
|
-
* @param {number} dim - Dimension of layout
|
|
344
|
-
* @param {number} seed - Random seed for initial positions
|
|
345
|
-
* @returns {Object} Positions dictionary keyed by node
|
|
346
|
-
*/
|
|
347
|
-
function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
348
|
-
const processed = _processParams(G, center, dim);
|
|
349
|
-
let graph = processed.G;
|
|
350
|
-
center = processed.center;
|
|
351
|
-
const nodes = getNodesFromGraph(graph);
|
|
352
|
-
const edges = getEdgesFromGraph(graph);
|
|
353
|
-
if (nodes.length === 0) {
|
|
354
|
-
return {};
|
|
355
|
-
}
|
|
356
|
-
if (nodes.length === 1) {
|
|
357
|
-
const singlePos = {};
|
|
358
|
-
singlePos[nodes[0]] = center;
|
|
359
|
-
return singlePos;
|
|
360
|
-
}
|
|
361
|
-
// Set up initial positions
|
|
362
|
-
let positions = {};
|
|
363
|
-
if (pos) {
|
|
364
|
-
// Use provided positions
|
|
365
|
-
for (const node of nodes) {
|
|
366
|
-
if (pos[node]) {
|
|
367
|
-
positions[node] = [...pos[node]];
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
371
|
-
positions[node] = rng.rand(dim);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
// Random initial positions
|
|
377
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
378
|
-
for (const node of nodes) {
|
|
379
|
-
positions[node] = rng.rand(dim);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
// Set up fixed nodes
|
|
383
|
-
const fixedNodes = new Set(fixed || []);
|
|
384
|
-
// Optimal distance between nodes
|
|
385
|
-
if (!k) {
|
|
386
|
-
k = 1.0 / Math.sqrt(nodes.length);
|
|
387
|
-
}
|
|
388
|
-
// Initialize temperature
|
|
389
|
-
let t = 0.1;
|
|
390
|
-
// Calculate temperature reduction
|
|
391
|
-
const dt = t / (iterations + 1);
|
|
392
|
-
// Simple cooling schedule
|
|
393
|
-
for (let i = 0; i < iterations; i++) {
|
|
394
|
-
// Calculate repulsive forces
|
|
395
|
-
const displacement = {};
|
|
396
|
-
for (const node of nodes) {
|
|
397
|
-
displacement[node] = Array(dim).fill(0);
|
|
398
|
-
}
|
|
399
|
-
// Repulsive forces between nodes
|
|
400
|
-
for (let v1i = 0; v1i < nodes.length; v1i++) {
|
|
401
|
-
const v1 = nodes[v1i];
|
|
402
|
-
for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
|
|
403
|
-
const v2 = nodes[v2i];
|
|
404
|
-
// Difference vector
|
|
405
|
-
const delta = positions[v1].map((p, i) => p - positions[v2][i]);
|
|
406
|
-
// Distance
|
|
407
|
-
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
408
|
-
// Force
|
|
409
|
-
const force = (k * k) / distance;
|
|
410
|
-
// Add force to displacement
|
|
411
|
-
for (let j = 0; j < dim; j++) {
|
|
412
|
-
const direction = delta[j] / distance;
|
|
413
|
-
displacement[v1][j] += direction * force;
|
|
414
|
-
displacement[v2][j] -= direction * force;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// Attractive forces between connected nodes
|
|
419
|
-
for (const [source, target] of edges) {
|
|
420
|
-
// Difference vector
|
|
421
|
-
const delta = positions[source].map((p, i) => p - positions[target][i]);
|
|
422
|
-
// Distance
|
|
423
|
-
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
424
|
-
// Force
|
|
425
|
-
const force = (distance * distance) / k;
|
|
426
|
-
// Add force to displacement
|
|
427
|
-
for (let j = 0; j < dim; j++) {
|
|
428
|
-
const direction = delta[j] / distance;
|
|
429
|
-
displacement[source][j] -= direction * force;
|
|
430
|
-
displacement[target][j] += direction * force;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// Update positions
|
|
434
|
-
for (const node of nodes) {
|
|
435
|
-
if (fixedNodes.has(node))
|
|
436
|
-
continue;
|
|
437
|
-
// Calculate displacement magnitude
|
|
438
|
-
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
|
|
439
|
-
// Limit maximum displacement by temperature
|
|
440
|
-
const limitedMagnitude = Math.min(magnitude, t);
|
|
441
|
-
// Update position
|
|
442
|
-
for (let j = 0; j < dim; j++) {
|
|
443
|
-
const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
|
|
444
|
-
positions[node][j] += direction * limitedMagnitude;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// Cool temperature
|
|
448
|
-
t -= dt;
|
|
449
|
-
}
|
|
450
|
-
// Rescale positions
|
|
451
|
-
if (!fixed) {
|
|
452
|
-
positions = rescaleLayout(positions, scale, center);
|
|
453
|
-
}
|
|
454
|
-
return positions;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Position nodes in a spectral layout using eigenvectors of the graph Laplacian.
|
|
458
|
-
*
|
|
459
|
-
* @param G - Graph
|
|
460
|
-
* @param scale - Scale factor for positions
|
|
461
|
-
* @param center - Coordinate pair around which to center the layout
|
|
462
|
-
* @param dim - Dimension of layout
|
|
463
|
-
* @returns Positions dictionary keyed by node
|
|
464
|
-
*/
|
|
465
|
-
function spectralLayout(G, scale = 1, center = null, dim = 2) {
|
|
466
|
-
const processed = _processParams(G, center, dim);
|
|
467
|
-
const graph = processed.G;
|
|
468
|
-
center = processed.center;
|
|
469
|
-
const nodes = getNodesFromGraph(graph);
|
|
470
|
-
if (nodes.length <= 2) {
|
|
471
|
-
if (nodes.length === 0) {
|
|
472
|
-
return {};
|
|
473
|
-
}
|
|
474
|
-
else if (nodes.length === 1) {
|
|
475
|
-
return { [nodes[0]]: center };
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
return {
|
|
479
|
-
[nodes[0]]: center.map(v => v - scale),
|
|
480
|
-
[nodes[1]]: center.map(v => v + scale)
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
// Create adjacency matrix
|
|
485
|
-
const N = nodes.length;
|
|
486
|
-
const nodeIndices = {};
|
|
487
|
-
nodes.forEach((node, i) => { nodeIndices[node] = i; });
|
|
488
|
-
const A = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
489
|
-
const edges = getEdgesFromGraph(graph);
|
|
490
|
-
for (const [source, target] of edges) {
|
|
491
|
-
const i = nodeIndices[source];
|
|
492
|
-
const j = nodeIndices[target];
|
|
493
|
-
A[i][j] = 1;
|
|
494
|
-
A[j][i] = 1; // Make symmetric for undirected graphs
|
|
495
|
-
}
|
|
496
|
-
// Create Laplacian matrix: L = D - A where D is degree matrix
|
|
497
|
-
const L = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
498
|
-
for (let i = 0; i < N; i++) {
|
|
499
|
-
// Compute degree (sum of row)
|
|
500
|
-
L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
|
|
501
|
-
for (let j = 0; j < N; j++) {
|
|
502
|
-
L[i][j] -= A[i][j];
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
// Compute eigenvectors using power iteration method
|
|
506
|
-
// We need the smallest non-zero eigenvectors of L
|
|
507
|
-
const eigenvectors = [];
|
|
508
|
-
// For each dimension, find an eigenvector
|
|
509
|
-
for (let d = 0; d < dim; d++) {
|
|
510
|
-
let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
|
|
511
|
-
// Orthogonalize against previous eigenvectors
|
|
512
|
-
for (const ev of eigenvectors) {
|
|
513
|
-
const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
|
|
514
|
-
vector = vector.map((val, idx) => val - dot * ev[idx]);
|
|
515
|
-
}
|
|
516
|
-
// Normalize
|
|
517
|
-
const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
|
|
518
|
-
vector = vector.map(val => val / norm);
|
|
519
|
-
// Apply shifted inverse iteration to find smallest non-zero eigenvector
|
|
520
|
-
// This is a simplification of the actual algorithm
|
|
521
|
-
for (let iter = 0; iter < 100; iter++) {
|
|
522
|
-
// Apply Laplacian
|
|
523
|
-
const newVec = Array(N).fill(0);
|
|
524
|
-
for (let i = 0; i < N; i++) {
|
|
525
|
-
for (let j = 0; j < N; j++) {
|
|
526
|
-
newVec[i] += L[i][j] * vector[j];
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
// Orthogonalize against the constant vector (eigenvector with eigenvalue 0)
|
|
530
|
-
const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
|
|
531
|
-
newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; });
|
|
532
|
-
// Normalize
|
|
533
|
-
const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
|
|
534
|
-
if (newNorm < 1e-10)
|
|
535
|
-
continue; // Skip if vector is close to zero
|
|
536
|
-
vector = newVec.map(val => val / newNorm);
|
|
537
|
-
}
|
|
538
|
-
eigenvectors.push(vector);
|
|
539
|
-
}
|
|
540
|
-
// Create position array from eigenvectors
|
|
541
|
-
const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
|
|
542
|
-
for (let i = 0; i < N; i++) {
|
|
543
|
-
for (let d = 0; d < dim; d++) {
|
|
544
|
-
positions[i][d] = eigenvectors[d][i];
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
// Rescale and create position dictionary
|
|
548
|
-
const scaledPositions = rescaleLayout(positions, scale);
|
|
549
|
-
const pos = {};
|
|
550
|
-
nodes.forEach((node, i) => {
|
|
551
|
-
pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
|
|
552
|
-
});
|
|
553
|
-
return pos;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Position nodes in a spiral layout.
|
|
557
|
-
*
|
|
558
|
-
* @param G - Graph or list of nodes
|
|
559
|
-
* @param scale - Scale factor for positions
|
|
560
|
-
* @param center - Coordinate pair around which to center the layout
|
|
561
|
-
* @param dim - Dimension of layout
|
|
562
|
-
* @param resolution - Controls the spacing between spiral elements
|
|
563
|
-
* @param equidistant - Whether to place nodes equidistant from each other
|
|
564
|
-
* @returns Positions dictionary keyed by node
|
|
565
|
-
*/
|
|
566
|
-
function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
|
|
567
|
-
if (dim !== 2) {
|
|
568
|
-
throw new Error("can only handle 2 dimensions");
|
|
569
|
-
}
|
|
570
|
-
const processed = _processParams(G, center || [0, 0], dim);
|
|
571
|
-
const nodes = getNodesFromGraph(processed.G);
|
|
572
|
-
center = processed.center;
|
|
573
|
-
const pos = {};
|
|
574
|
-
if (nodes.length === 0) {
|
|
575
|
-
return pos;
|
|
576
|
-
}
|
|
577
|
-
if (nodes.length === 1) {
|
|
578
|
-
pos[nodes[0]] = [...center];
|
|
579
|
-
return pos;
|
|
580
|
-
}
|
|
581
|
-
let positions = [];
|
|
582
|
-
if (equidistant) {
|
|
583
|
-
// Create equidistant points along the spiral
|
|
584
|
-
// This matches the Python implementation logic
|
|
585
|
-
const chord = 1;
|
|
586
|
-
const step = 0.5;
|
|
587
|
-
let theta = resolution;
|
|
588
|
-
theta += chord / (step * theta);
|
|
589
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
590
|
-
const r = step * theta;
|
|
591
|
-
theta += chord / r;
|
|
592
|
-
positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
// Create points with equal angle but increasing distance
|
|
597
|
-
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
598
|
-
const angle = dist.map(d => resolution * d);
|
|
599
|
-
positions = dist.map((d, i) => [
|
|
600
|
-
Math.cos(angle[i]) * d,
|
|
601
|
-
Math.sin(angle[i]) * d
|
|
602
|
-
]);
|
|
603
|
-
}
|
|
604
|
-
// Convert position array to position matrix for rescaling
|
|
605
|
-
const posArray = [];
|
|
606
|
-
for (let i = 0; i < positions.length; i++) {
|
|
607
|
-
posArray.push(positions[i]);
|
|
608
|
-
}
|
|
609
|
-
// Rescale positions and add center offset
|
|
610
|
-
const scaledPositions = rescaleLayout(posArray, scale);
|
|
611
|
-
for (let i = 0; i < scaledPositions.length; i++) {
|
|
612
|
-
scaledPositions[i][0] += center[0];
|
|
613
|
-
scaledPositions[i][1] += center[1];
|
|
614
|
-
}
|
|
615
|
-
// Create position dictionary
|
|
616
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
617
|
-
pos[nodes[i]] = scaledPositions[i];
|
|
618
|
-
}
|
|
619
|
-
return pos;
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Rescale node positions to fit in the specified scale and center.
|
|
623
|
-
*
|
|
624
|
-
* @param pos - Dictionary or array of positions
|
|
625
|
-
* @param scale - Scale factor for positions
|
|
626
|
-
* @param center - Coordinate pair around which to center the layout
|
|
627
|
-
* @returns Rescaled positions dictionary
|
|
628
|
-
*/
|
|
629
|
-
function rescaleLayout(pos, scale = 1, center = [0, 0]) {
|
|
630
|
-
// Check if pos is empty
|
|
631
|
-
if (Array.isArray(pos)) {
|
|
632
|
-
if (pos.length === 0)
|
|
633
|
-
return [];
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
if (Object.keys(pos).length === 0)
|
|
637
|
-
return {};
|
|
638
|
-
}
|
|
639
|
-
// Extract position values
|
|
640
|
-
const posValues = Array.isArray(pos) ? pos : Object.values(pos);
|
|
641
|
-
const dim = posValues[0].length;
|
|
642
|
-
// Calculate center of positions
|
|
643
|
-
const posCenter = Array(dim).fill(0);
|
|
644
|
-
for (const p of posValues) {
|
|
645
|
-
for (let i = 0; i < dim; i++) {
|
|
646
|
-
posCenter[i] += p[i] / posValues.length;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
// Center positions
|
|
650
|
-
let centeredPos = {};
|
|
651
|
-
if (Array.isArray(pos)) {
|
|
652
|
-
centeredPos = pos.map(p => p.map((val, i) => val - posCenter[i]));
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
for (const [node, p] of Object.entries(pos)) {
|
|
656
|
-
centeredPos[node] = p.map((val, i) => val - posCenter[i]);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// Find maximum distance from center
|
|
660
|
-
let maxDistance = 0;
|
|
661
|
-
const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
|
|
662
|
-
for (const p of centeredValues) {
|
|
663
|
-
const distance = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
|
|
664
|
-
maxDistance = Math.max(maxDistance, distance);
|
|
665
|
-
}
|
|
666
|
-
// Rescale
|
|
667
|
-
let scaledPos = Array.isArray(pos) ? [] : {};
|
|
668
|
-
if (maxDistance > 0) {
|
|
669
|
-
const scaleFactor = scale / maxDistance;
|
|
670
|
-
if (Array.isArray(pos)) {
|
|
671
|
-
scaledPos = centeredPos.map(p => p.map((val, i) => val * scaleFactor + center[i]));
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
for (const [node, p] of Object.entries(centeredPos)) {
|
|
675
|
-
scaledPos[node] = p.map((val, i) => val * scaleFactor + center[i]);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
else {
|
|
680
|
-
// All nodes at the same position
|
|
681
|
-
if (Array.isArray(pos)) {
|
|
682
|
-
scaledPos = Array(pos.length).fill(0).map(() => [...center]);
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
for (const node of Object.keys(pos)) {
|
|
686
|
-
scaledPos[node] = [...center];
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
return scaledPos;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Position nodes in two straight lines (bipartite layout).
|
|
694
|
-
*
|
|
695
|
-
* @param G - Graph or list of nodes
|
|
696
|
-
* @param nodes - Nodes in one node set of the graph
|
|
697
|
-
* @param align - The alignment of nodes: 'vertical' or 'horizontal'
|
|
698
|
-
* @param scale - Scale factor for positions
|
|
699
|
-
* @param center - Coordinate pair around which to center the layout
|
|
700
|
-
* @param aspectRatio - The ratio of the width to the height of the layout
|
|
701
|
-
* @returns Positions dictionary keyed by node
|
|
702
|
-
*/
|
|
703
|
-
function bipartiteLayout(G, nodes = null, align = 'vertical', scale = 1, center = null, aspectRatio = 4 / 3) {
|
|
704
|
-
if (align !== 'vertical' && align !== 'horizontal') {
|
|
705
|
-
throw new Error("align must be either vertical or horizontal");
|
|
706
|
-
}
|
|
707
|
-
const processed = _processParams(G, center || [0, 0], 2);
|
|
708
|
-
const graph = processed.G;
|
|
709
|
-
center = processed.center;
|
|
710
|
-
const allNodes = getNodesFromGraph(graph);
|
|
711
|
-
if (allNodes.length === 0) {
|
|
712
|
-
return {};
|
|
713
|
-
}
|
|
714
|
-
// If nodes not provided, try to determine bipartite sets
|
|
715
|
-
if (!nodes) {
|
|
716
|
-
// A simple heuristic for bipartite detection: use nodes with even/odd indices
|
|
717
|
-
// This is a simplification, in Python NetworkX has bipartite.sets()
|
|
718
|
-
nodes = allNodes.filter((_, i) => i % 2 === 0);
|
|
719
|
-
}
|
|
720
|
-
const left = new Set(nodes);
|
|
721
|
-
const right = new Set(allNodes.filter((n) => !left.has(n)));
|
|
722
|
-
const height = 1;
|
|
723
|
-
const width = aspectRatio * height;
|
|
724
|
-
const offset = [width / 2, height / 2];
|
|
725
|
-
const pos = {};
|
|
726
|
-
// Position nodes in the left set
|
|
727
|
-
const leftNodes = [...left];
|
|
728
|
-
leftNodes.forEach((node, i) => {
|
|
729
|
-
const x = 0;
|
|
730
|
-
const y = i * height / (leftNodes.length || 1);
|
|
731
|
-
pos[node] = [x, y];
|
|
732
|
-
});
|
|
733
|
-
// Position nodes in the right set
|
|
734
|
-
const rightNodes = [...right];
|
|
735
|
-
rightNodes.forEach((node, i) => {
|
|
736
|
-
const x = width;
|
|
737
|
-
const y = i * height / (rightNodes.length || 1);
|
|
738
|
-
pos[node] = [x, y];
|
|
739
|
-
});
|
|
740
|
-
// Center positions around the origin and apply offset
|
|
741
|
-
for (const node in pos) {
|
|
742
|
-
pos[node][0] -= offset[0];
|
|
743
|
-
pos[node][1] -= offset[1];
|
|
744
|
-
}
|
|
745
|
-
// Rescale positions
|
|
746
|
-
const scaledPos = rescaleLayout(pos, scale, center);
|
|
747
|
-
// Handle horizontal alignment
|
|
748
|
-
if (align === 'horizontal') {
|
|
749
|
-
for (const node in scaledPos) {
|
|
750
|
-
const temp = scaledPos[node][0];
|
|
751
|
-
scaledPos[node][0] = scaledPos[node][1];
|
|
752
|
-
scaledPos[node][1] = temp;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
return scaledPos;
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Position nodes in layers of straight lines (multipartite layout).
|
|
759
|
-
*
|
|
760
|
-
* @param G - Graph or list of nodes
|
|
761
|
-
* @param subsetKey - Object mapping layers to node sets, or node attribute name
|
|
762
|
-
* @param align - The alignment of nodes: 'vertical' or 'horizontal'
|
|
763
|
-
* @param scale - Scale factor for positions
|
|
764
|
-
* @param center - Coordinate pair around which to center the layout
|
|
765
|
-
* @returns Positions dictionary keyed by node
|
|
766
|
-
*/
|
|
767
|
-
function multipartiteLayout(G, subsetKey = 'subset', align = 'vertical', scale = 1, center = null) {
|
|
768
|
-
if (align !== 'vertical' && align !== 'horizontal') {
|
|
769
|
-
throw new Error("align must be either vertical or horizontal");
|
|
770
|
-
}
|
|
771
|
-
const processed = _processParams(G, center || [0, 0], 2);
|
|
772
|
-
const graph = processed.G;
|
|
773
|
-
center = processed.center;
|
|
774
|
-
const allNodes = getNodesFromGraph(graph);
|
|
775
|
-
if (allNodes.length === 0) {
|
|
776
|
-
return {};
|
|
777
|
-
}
|
|
778
|
-
// Convert subsetKey to a layer mapping if it's a string
|
|
779
|
-
let layers = {};
|
|
780
|
-
if (typeof subsetKey === 'string') {
|
|
781
|
-
// In JS we don't have access to node attributes directly
|
|
782
|
-
// This is a simplification - in a real implementation we would need
|
|
783
|
-
// to access node attributes from the graph
|
|
784
|
-
console.warn("Using string subsetKey requires node attributes, using default partitioning");
|
|
785
|
-
// Create a simple partitioning as fallback
|
|
786
|
-
layers = { 0: allNodes };
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
// subsetKey is already a mapping of layers to nodes
|
|
790
|
-
// Convert single nodes to arrays
|
|
791
|
-
for (const [key, value] of Object.entries(subsetKey)) {
|
|
792
|
-
if (Array.isArray(value)) {
|
|
793
|
-
layers[key] = value;
|
|
794
|
-
}
|
|
795
|
-
else {
|
|
796
|
-
layers[key] = [value];
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
const layerCount = Object.keys(layers).length;
|
|
801
|
-
let pos = {};
|
|
802
|
-
// Process each layer
|
|
803
|
-
Object.entries(layers).forEach(([layer, nodes], layerIdx) => {
|
|
804
|
-
const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
805
|
-
const layerSize = layerNodes.length;
|
|
806
|
-
layerNodes.forEach((node, nodeIdx) => {
|
|
807
|
-
// Place nodes in a grid: layerIdx determines x-coordinate (column)
|
|
808
|
-
// nodeIdx determines y-coordinate (row position within column)
|
|
809
|
-
const x = layerIdx - (layerCount - 1) / 2;
|
|
810
|
-
const y = nodeIdx - (layerSize - 1) / 2;
|
|
811
|
-
pos[node] = [x, y];
|
|
812
|
-
});
|
|
813
|
-
});
|
|
814
|
-
// Rescale positions
|
|
815
|
-
pos = rescaleLayout(pos, scale, center);
|
|
816
|
-
// Handle horizontal alignment
|
|
817
|
-
if (align === 'horizontal') {
|
|
818
|
-
for (const node in pos) {
|
|
819
|
-
const temp = pos[node][0];
|
|
820
|
-
pos[node][0] = pos[node][1];
|
|
821
|
-
pos[node][1] = temp;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return pos;
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Position nodes according to breadth-first search algorithm.
|
|
828
|
-
*
|
|
829
|
-
* @param G - Graph
|
|
830
|
-
* @param start - Starting node for bfs
|
|
831
|
-
* @param align - The alignment of layers: 'vertical' or 'horizontal'
|
|
832
|
-
* @param scale - Scale factor for positions
|
|
833
|
-
* @param center - Coordinate pair around which to center the layout
|
|
834
|
-
* @returns Positions dictionary keyed by node
|
|
835
|
-
*/
|
|
836
|
-
function bfsLayout(G, start, align = 'vertical', scale = 1, center = null) {
|
|
837
|
-
const processed = _processParams(G, center || [0, 0], 2);
|
|
838
|
-
const graph = processed.G;
|
|
839
|
-
center = processed.center;
|
|
840
|
-
const allNodes = getNodesFromGraph(graph);
|
|
841
|
-
if (allNodes.length === 0) {
|
|
842
|
-
return {};
|
|
843
|
-
}
|
|
844
|
-
// Compute BFS layers
|
|
845
|
-
const layers = {};
|
|
846
|
-
const visited = new Set();
|
|
847
|
-
let currentLayer = 0;
|
|
848
|
-
// Starting layer
|
|
849
|
-
layers[currentLayer] = [start];
|
|
850
|
-
visited.add(start);
|
|
851
|
-
// BFS traversal
|
|
852
|
-
while (Object.values(layers).flat().length < allNodes.length) {
|
|
853
|
-
const nextLayer = [];
|
|
854
|
-
const currentNodes = layers[currentLayer];
|
|
855
|
-
for (const node of currentNodes) {
|
|
856
|
-
// Get neighbors - this is a simplified approach
|
|
857
|
-
// In a real implementation, we would get neighbors from the graph
|
|
858
|
-
const neighbors = getNeighbors(graph, node);
|
|
859
|
-
for (const neighbor of neighbors) {
|
|
860
|
-
if (!visited.has(neighbor)) {
|
|
861
|
-
nextLayer.push(neighbor);
|
|
862
|
-
visited.add(neighbor);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (nextLayer.length === 0) {
|
|
867
|
-
// No more connected nodes
|
|
868
|
-
const unvisited = allNodes.filter((node) => !visited.has(node));
|
|
869
|
-
if (unvisited.length > 0) {
|
|
870
|
-
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
871
|
-
}
|
|
872
|
-
break;
|
|
873
|
-
}
|
|
874
|
-
currentLayer++;
|
|
875
|
-
layers[currentLayer] = nextLayer;
|
|
876
|
-
}
|
|
877
|
-
// Use multipartite_layout to position the layers
|
|
878
|
-
return multipartiteLayout(graph, layers, align, scale, center);
|
|
879
|
-
// Helper function to get neighbors
|
|
880
|
-
function getNeighbors(graph, node) {
|
|
881
|
-
if (!graph.edges)
|
|
882
|
-
return [];
|
|
883
|
-
return graph.edges()
|
|
884
|
-
.filter((edge) => edge[0] === node || edge[1] === node)
|
|
885
|
-
.map((edge) => edge[0] === node ? edge[1] : edge[0]);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Position nodes without edge intersections (planar layout).
|
|
890
|
-
*
|
|
891
|
-
* @param G - Graph
|
|
892
|
-
* @param scale - Scale factor for positions
|
|
893
|
-
* @param center - Coordinate pair around which to center the layout
|
|
894
|
-
* @param dim - Dimension of layout (must be 2)
|
|
895
|
-
* @returns Positions dictionary keyed by node
|
|
896
|
-
*/
|
|
897
|
-
function planarLayout(G, scale = 1, center = null, dim = 2) {
|
|
898
|
-
if (dim !== 2) {
|
|
899
|
-
throw new Error("can only handle 2 dimensions");
|
|
900
|
-
}
|
|
901
|
-
const processed = _processParams(G, center || [0, 0], dim);
|
|
902
|
-
const graph = processed.G;
|
|
903
|
-
center = processed.center;
|
|
904
|
-
const nodes = getNodesFromGraph(graph);
|
|
905
|
-
const edges = getEdgesFromGraph(graph);
|
|
906
|
-
if (nodes.length === 0) {
|
|
907
|
-
return {};
|
|
908
|
-
}
|
|
909
|
-
// Check if graph is planar and get embedding
|
|
910
|
-
const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
|
|
911
|
-
if (!isPlanar) {
|
|
912
|
-
throw new Error("G is not planar.");
|
|
913
|
-
}
|
|
914
|
-
if (!embedding) {
|
|
915
|
-
throw new Error("Failed to generate planar embedding.");
|
|
916
|
-
}
|
|
917
|
-
// Convert embedding to positions
|
|
918
|
-
let pos = combinatorialEmbeddingToPos(embedding, nodes);
|
|
919
|
-
// Rescale the positions
|
|
920
|
-
pos = rescaleLayout(pos, scale, center);
|
|
921
|
-
return pos;
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Check if graph is planar using a simplified version of Boyer-Myrvold algorithm.
|
|
925
|
-
* Returns planarity and embedding information.
|
|
926
|
-
*
|
|
927
|
-
* @param G - Graph
|
|
928
|
-
* @param nodes - List of nodes
|
|
929
|
-
* @param edges - List of edges
|
|
930
|
-
* @returns Object containing isPlanar flag and embedding
|
|
931
|
-
*/
|
|
932
|
-
function checkPlanarity(G, nodes, edges) {
|
|
933
|
-
// For small graphs (n <= 4), all are planar
|
|
934
|
-
if (nodes.length <= 4) {
|
|
935
|
-
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
936
|
-
}
|
|
937
|
-
// For K5 (complete graph with 5 nodes) and K3,3 (complete bipartite with 3,3 nodes)
|
|
938
|
-
// these are not planar by Kuratowski's theorem
|
|
939
|
-
if (isK5(nodes, edges) || isK33(nodes, edges)) {
|
|
940
|
-
return { isPlanar: false, embedding: null };
|
|
941
|
-
}
|
|
942
|
-
// For other graphs, use LR algorithm (Left-Right Planarity Test)
|
|
943
|
-
const result = lrPlanarityTest(nodes, edges);
|
|
944
|
-
return result;
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Check if graph is K5 (complete graph with 5 nodes)
|
|
948
|
-
*
|
|
949
|
-
* @param nodes - List of nodes
|
|
950
|
-
* @param edges - List of edges
|
|
951
|
-
* @returns True if graph is K5
|
|
952
|
-
*/
|
|
953
|
-
function isK5(nodes, edges) {
|
|
954
|
-
if (nodes.length !== 5)
|
|
955
|
-
return false;
|
|
956
|
-
// K5 has exactly 10 edges
|
|
957
|
-
if (edges.length !== 10)
|
|
958
|
-
return false;
|
|
959
|
-
// Check if every pair of distinct nodes is connected
|
|
960
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
961
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
962
|
-
const hasEdge = edges.some(e => (e[0] === nodes[i] && e[1] === nodes[j]) ||
|
|
963
|
-
(e[0] === nodes[j] && e[1] === nodes[i]));
|
|
964
|
-
if (!hasEdge)
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return true;
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Check if graph is K3,3 (complete bipartite with 3,3 nodes)
|
|
972
|
-
*
|
|
973
|
-
* @param nodes - List of nodes
|
|
974
|
-
* @param edges - List of edges
|
|
975
|
-
* @returns True if graph is K3,3
|
|
976
|
-
*/
|
|
977
|
-
function isK33(nodes, edges) {
|
|
978
|
-
if (nodes.length !== 6)
|
|
979
|
-
return false;
|
|
980
|
-
// K3,3 has exactly 9 edges
|
|
981
|
-
if (edges.length !== 9)
|
|
982
|
-
return false;
|
|
983
|
-
// Try to find a bipartite partition
|
|
984
|
-
const nodePartitions = tryFindBipartitePartition(nodes, edges);
|
|
985
|
-
if (!nodePartitions)
|
|
986
|
-
return false;
|
|
987
|
-
const [part1, part2] = nodePartitions;
|
|
988
|
-
// Check if both partitions have size 3
|
|
989
|
-
if (part1.length !== 3 || part2.length !== 3)
|
|
990
|
-
return false;
|
|
991
|
-
// Check if every node in part1 is connected to every node in part2
|
|
992
|
-
for (const n1 of part1) {
|
|
993
|
-
for (const n2 of part2) {
|
|
994
|
-
const hasEdge = edges.some(e => (e[0] === n1 && e[1] === n2) ||
|
|
995
|
-
(e[0] === n2 && e[1] === n1));
|
|
996
|
-
if (!hasEdge)
|
|
997
|
-
return false;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
return true;
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Try to find a bipartite partition of the nodes
|
|
1004
|
-
*
|
|
1005
|
-
* @param nodes - List of nodes
|
|
1006
|
-
* @param edges - List of edges
|
|
1007
|
-
* @returns Array of two partitions, or null if not bipartite
|
|
1008
|
-
*/
|
|
1009
|
-
function tryFindBipartitePartition(nodes, edges) {
|
|
1010
|
-
const colorMap = {};
|
|
1011
|
-
const adjList = {};
|
|
1012
|
-
// Create adjacency list
|
|
1013
|
-
for (const node of nodes) {
|
|
1014
|
-
adjList[node] = [];
|
|
1015
|
-
}
|
|
1016
|
-
for (const [u, v] of edges) {
|
|
1017
|
-
adjList[u].push(v);
|
|
1018
|
-
adjList[v].push(u);
|
|
1019
|
-
}
|
|
1020
|
-
// BFS to color nodes
|
|
1021
|
-
const queue = [nodes[0]];
|
|
1022
|
-
colorMap[nodes[0]] = 0;
|
|
1023
|
-
while (queue.length > 0) {
|
|
1024
|
-
const node = queue.shift();
|
|
1025
|
-
const nodeColor = colorMap[node];
|
|
1026
|
-
for (const neighbor of adjList[node]) {
|
|
1027
|
-
if (colorMap[neighbor] === undefined) {
|
|
1028
|
-
colorMap[neighbor] = 1 - nodeColor; // Toggle color (0/1)
|
|
1029
|
-
queue.push(neighbor);
|
|
1030
|
-
}
|
|
1031
|
-
else if (colorMap[neighbor] === nodeColor) {
|
|
1032
|
-
// Conflict: not bipartite
|
|
1033
|
-
return null;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
// Create partitions
|
|
1038
|
-
const part0 = [];
|
|
1039
|
-
const part1 = [];
|
|
1040
|
-
for (const node of nodes) {
|
|
1041
|
-
if (colorMap[node] === 0) {
|
|
1042
|
-
part0.push(node);
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
part1.push(node);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
return [part0, part1];
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Left-Right Planarity Test for general graphs
|
|
1052
|
-
*
|
|
1053
|
-
* @param nodes - List of nodes
|
|
1054
|
-
* @param edges - List of edges
|
|
1055
|
-
* @returns Object containing isPlanar flag and embedding
|
|
1056
|
-
*/
|
|
1057
|
-
function lrPlanarityTest(nodes, edges) {
|
|
1058
|
-
// Create adjacency list for the graph
|
|
1059
|
-
const adjList = {};
|
|
1060
|
-
for (const node of nodes) {
|
|
1061
|
-
adjList[node] = [];
|
|
1062
|
-
}
|
|
1063
|
-
for (const [u, v] of edges) {
|
|
1064
|
-
adjList[u].push(v);
|
|
1065
|
-
adjList[v].push(u);
|
|
1066
|
-
}
|
|
1067
|
-
// Step 1: Perform DFS to get an st-numbering (ordering of nodes)
|
|
1068
|
-
const visited = new Set();
|
|
1069
|
-
const ordering = [];
|
|
1070
|
-
function dfs(node) {
|
|
1071
|
-
visited.add(node);
|
|
1072
|
-
ordering.push(node);
|
|
1073
|
-
for (const neighbor of adjList[node]) {
|
|
1074
|
-
if (!visited.has(neighbor)) {
|
|
1075
|
-
dfs(neighbor);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
// Start DFS from first node
|
|
1080
|
-
dfs(nodes[0]);
|
|
1081
|
-
// If the graph is disconnected, it's still planar but we need to handle each component
|
|
1082
|
-
if (ordering.length < nodes.length) {
|
|
1083
|
-
// Create a simple triangulation embedding for disconnected graphs
|
|
1084
|
-
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
1085
|
-
}
|
|
1086
|
-
// Step 2: For a general implementation, we'll use a simplified approach
|
|
1087
|
-
// since this would normally require implementing the entire LR algorithm
|
|
1088
|
-
// For this implementation, since we can't fully implement Boyer-Myrvold,
|
|
1089
|
-
// we'll create a reasonable planar embedding for most planar graphs
|
|
1090
|
-
// We assume the graph is planar if it's sparse enough (|E| <= 3|V| - 6)
|
|
1091
|
-
// This is a necessary but not sufficient condition for planar graphs
|
|
1092
|
-
if (edges.length > 3 * nodes.length - 6) {
|
|
1093
|
-
return { isPlanar: false, embedding: null };
|
|
1094
|
-
}
|
|
1095
|
-
// Create a planar embedding using a triangulation approach
|
|
1096
|
-
const embedding = createTriangulationEmbedding(nodes, edges);
|
|
1097
|
-
return { isPlanar: true, embedding };
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Create a triangulation-based embedding for a planar graph
|
|
1101
|
-
*
|
|
1102
|
-
* @param nodes - List of nodes
|
|
1103
|
-
* @param edges - List of edges
|
|
1104
|
-
* @returns Embedding object
|
|
1105
|
-
*/
|
|
1106
|
-
function createTriangulationEmbedding(nodes, edges) {
|
|
1107
|
-
// Create a simple embedding using the incremental approach
|
|
1108
|
-
const embedding = {
|
|
1109
|
-
nodeOrder: [...nodes],
|
|
1110
|
-
faceList: [],
|
|
1111
|
-
nodePositions: {}
|
|
1112
|
-
};
|
|
1113
|
-
// Create a map of adjacent nodes
|
|
1114
|
-
const adjMap = {};
|
|
1115
|
-
for (const node of nodes) {
|
|
1116
|
-
adjMap[node] = new Set();
|
|
1117
|
-
}
|
|
1118
|
-
for (const [u, v] of edges) {
|
|
1119
|
-
adjMap[u].add(v);
|
|
1120
|
-
adjMap[v].add(u);
|
|
1121
|
-
}
|
|
1122
|
-
// Create outer face as a cycle (if possible)
|
|
1123
|
-
const outerFace = findCycle(nodes, edges, adjMap) || nodes;
|
|
1124
|
-
embedding.faceList.push(outerFace);
|
|
1125
|
-
// Position nodes on a convex polygon (outer face)
|
|
1126
|
-
const n = outerFace.length;
|
|
1127
|
-
for (let i = 0; i < n; i++) {
|
|
1128
|
-
const angle = 2 * Math.PI * i / n;
|
|
1129
|
-
embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
|
|
1130
|
-
}
|
|
1131
|
-
// Position interior nodes using barycentric coordinates
|
|
1132
|
-
const interiorNodes = nodes.filter(node => !embedding.nodePositions[node]);
|
|
1133
|
-
for (const node of interiorNodes) {
|
|
1134
|
-
const neighbors = Array.from(adjMap[node]);
|
|
1135
|
-
if (neighbors.length === 0) {
|
|
1136
|
-
// Isolated node, place at center
|
|
1137
|
-
embedding.nodePositions[node] = [0, 0];
|
|
1138
|
-
}
|
|
1139
|
-
else {
|
|
1140
|
-
// Average position of neighbors that have positions
|
|
1141
|
-
let xSum = 0, ySum = 0, count = 0;
|
|
1142
|
-
for (const neighbor of neighbors) {
|
|
1143
|
-
if (embedding.nodePositions[neighbor]) {
|
|
1144
|
-
xSum += embedding.nodePositions[neighbor][0];
|
|
1145
|
-
ySum += embedding.nodePositions[neighbor][1];
|
|
1146
|
-
count++;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
if (count > 0) {
|
|
1150
|
-
// Place slightly away from center to avoid overlaps
|
|
1151
|
-
const jitter = 0.1 * Math.random();
|
|
1152
|
-
embedding.nodePositions[node] = [
|
|
1153
|
-
xSum / count + jitter * (Math.random() - 0.5),
|
|
1154
|
-
ySum / count + jitter * (Math.random() - 0.5)
|
|
1155
|
-
];
|
|
1156
|
-
}
|
|
1157
|
-
else {
|
|
1158
|
-
// No neighbors have positions yet, place randomly inside unit circle
|
|
1159
|
-
const r = 0.5 * Math.random();
|
|
1160
|
-
const angle = 2 * Math.PI * Math.random();
|
|
1161
|
-
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
return embedding;
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Find a simple cycle in the graph (for outer face)
|
|
1169
|
-
*
|
|
1170
|
-
* @param nodes - List of nodes
|
|
1171
|
-
* @param edges - List of edges
|
|
1172
|
-
* @param adjMap - Adjacency map
|
|
1173
|
-
* @returns Cycle as array of nodes, or null if none found
|
|
1174
|
-
*/
|
|
1175
|
-
function findCycle(nodes, edges, adjMap) {
|
|
1176
|
-
if (nodes.length === 0)
|
|
1177
|
-
return null;
|
|
1178
|
-
if (nodes.length <= 2)
|
|
1179
|
-
return nodes; // Not a real cycle but handle it
|
|
1180
|
-
// Try to find a Hamiltonian cycle for simplicity (for small graphs)
|
|
1181
|
-
if (nodes.length <= 8) {
|
|
1182
|
-
const visited = new Set();
|
|
1183
|
-
const path = [];
|
|
1184
|
-
function hamiltonianCycleDFS(node) {
|
|
1185
|
-
path.push(node);
|
|
1186
|
-
visited.add(node);
|
|
1187
|
-
if (path.length === nodes.length) {
|
|
1188
|
-
// Check if it's a cycle (last node connects to first)
|
|
1189
|
-
if (adjMap[node].has(path[0])) {
|
|
1190
|
-
return true;
|
|
1191
|
-
}
|
|
1192
|
-
// Not a cycle
|
|
1193
|
-
visited.delete(node);
|
|
1194
|
-
path.pop();
|
|
1195
|
-
return false;
|
|
1196
|
-
}
|
|
1197
|
-
for (const neighbor of adjMap[node]) {
|
|
1198
|
-
if (!visited.has(neighbor)) {
|
|
1199
|
-
if (hamiltonianCycleDFS(neighbor)) {
|
|
1200
|
-
return true;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
visited.delete(node);
|
|
1205
|
-
path.pop();
|
|
1206
|
-
return false;
|
|
1207
|
-
}
|
|
1208
|
-
if (hamiltonianCycleDFS(nodes[0])) {
|
|
1209
|
-
return path;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
// Fallback: try to find any cycle using DFS
|
|
1213
|
-
const visited = new Set();
|
|
1214
|
-
const parent = {};
|
|
1215
|
-
let cycleFound = null;
|
|
1216
|
-
function findCycleDFS(node, parentNode) {
|
|
1217
|
-
visited.add(node);
|
|
1218
|
-
for (const neighbor of adjMap[node]) {
|
|
1219
|
-
if (neighbor === parentNode)
|
|
1220
|
-
continue;
|
|
1221
|
-
if (visited.has(neighbor)) {
|
|
1222
|
-
// Found a cycle
|
|
1223
|
-
cycleFound = constructCycle(node, neighbor, parent);
|
|
1224
|
-
return true;
|
|
1225
|
-
}
|
|
1226
|
-
parent[neighbor] = node;
|
|
1227
|
-
if (findCycleDFS(neighbor, node)) {
|
|
1228
|
-
return true;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
function constructCycle(u, v, parent) {
|
|
1234
|
-
const cycle = [v, u];
|
|
1235
|
-
let current = u;
|
|
1236
|
-
while (parent[current] !== undefined && parent[current] !== v) {
|
|
1237
|
-
current = parent[current];
|
|
1238
|
-
cycle.push(current);
|
|
1239
|
-
}
|
|
1240
|
-
return cycle;
|
|
1241
|
-
}
|
|
1242
|
-
// Try to find a cycle
|
|
1243
|
-
for (const node of nodes) {
|
|
1244
|
-
if (!visited.has(node)) {
|
|
1245
|
-
parent[node] = null;
|
|
1246
|
-
if (findCycleDFS(node, null)) {
|
|
1247
|
-
break;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return cycleFound || nodes; // Fallback to all nodes if no cycle found
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Convert a combinatorial embedding to node positions
|
|
1255
|
-
*
|
|
1256
|
-
* @param embedding - The embedding object
|
|
1257
|
-
* @param nodes - List of nodes
|
|
1258
|
-
* @returns Dictionary mapping nodes to positions
|
|
1259
|
-
*/
|
|
1260
|
-
function combinatorialEmbeddingToPos(embedding, nodes) {
|
|
1261
|
-
const pos = {};
|
|
1262
|
-
// Use the positions from the embedding
|
|
1263
|
-
for (const node of nodes) {
|
|
1264
|
-
if (embedding.nodePositions[node]) {
|
|
1265
|
-
pos[node] = embedding.nodePositions[node];
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
// Fallback for any nodes without positions
|
|
1269
|
-
pos[node] = [0, 0];
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return pos;
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Position nodes using Kamada-Kawai path-length cost-function.
|
|
1276
|
-
*
|
|
1277
|
-
* @param G - NetworkX graph or list of nodes
|
|
1278
|
-
* @param dist - A two-level dictionary of optimal distances between nodes
|
|
1279
|
-
* @param pos - Initial positions for nodes
|
|
1280
|
-
* @param weight - The edge attribute used for edge weights
|
|
1281
|
-
* @param scale - Scale factor for positions
|
|
1282
|
-
* @param center - Coordinate pair around which to center the layout
|
|
1283
|
-
* @param dim - Dimension of layout
|
|
1284
|
-
* @returns Positions dictionary keyed by node
|
|
1285
|
-
*/
|
|
1286
|
-
function kamadaKawaiLayout(G, dist = null, pos = null, weight = 'weight', scale = 1, center = null, dim = 2) {
|
|
1287
|
-
const processed = _processParams(G, center, dim);
|
|
1288
|
-
const graph = processed.G;
|
|
1289
|
-
center = processed.center;
|
|
1290
|
-
const nodes = getNodesFromGraph(graph);
|
|
1291
|
-
if (nodes.length === 0) {
|
|
1292
|
-
return {};
|
|
1293
|
-
}
|
|
1294
|
-
if (nodes.length === 1) {
|
|
1295
|
-
return { [nodes[0]]: center };
|
|
1296
|
-
}
|
|
1297
|
-
// Initialize distance matrix
|
|
1298
|
-
if (!dist) {
|
|
1299
|
-
dist = _computeShortestPathDistances(graph, weight);
|
|
1300
|
-
}
|
|
1301
|
-
// Convert distances to a matrix
|
|
1302
|
-
const nodesArray = Array.from(nodes);
|
|
1303
|
-
const nNodes = nodesArray.length;
|
|
1304
|
-
const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
|
|
1305
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1306
|
-
const nodeI = nodesArray[i];
|
|
1307
|
-
distMatrix[i][i] = 0;
|
|
1308
|
-
if (!dist[nodeI])
|
|
1309
|
-
continue;
|
|
1310
|
-
for (let j = 0; j < nNodes; j++) {
|
|
1311
|
-
const nodeJ = nodesArray[j];
|
|
1312
|
-
if (dist[nodeI][nodeJ] !== undefined) {
|
|
1313
|
-
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
// Initialize positions if not provided
|
|
1318
|
-
if (!pos) {
|
|
1319
|
-
if (dim >= 2) {
|
|
1320
|
-
// Use circular/spherical layout for 2D and 3D
|
|
1321
|
-
pos = circularLayout(G, 1, center, dim);
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
// For 1D, use a linear layout
|
|
1325
|
-
const posArray = {};
|
|
1326
|
-
nodesArray.forEach((node, i) => {
|
|
1327
|
-
posArray[node] = [i / (nNodes - 1 || 1)];
|
|
1328
|
-
});
|
|
1329
|
-
pos = posArray;
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
// Convert positions to array for computation
|
|
1333
|
-
const posArray = new Array(nNodes);
|
|
1334
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1335
|
-
const node = nodesArray[i];
|
|
1336
|
-
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
1337
|
-
// Ensure correct dimensionality
|
|
1338
|
-
while (posArray[i].length < dim) {
|
|
1339
|
-
posArray[i].push(0);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
// Run the Kamada-Kawai algorithm
|
|
1343
|
-
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
1344
|
-
// Convert positions array back to dictionary and rescale
|
|
1345
|
-
const finalPos = {};
|
|
1346
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1347
|
-
finalPos[nodesArray[i]] = newPositions[i];
|
|
1348
|
-
}
|
|
1349
|
-
return rescaleLayout(finalPos, scale, center);
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Compute all-pairs shortest path distances for the graph
|
|
1353
|
-
*
|
|
1354
|
-
* @param G - NetworkX graph
|
|
1355
|
-
* @param weight - Edge attribute for weight
|
|
1356
|
-
* @returns Dictionary of dictionaries of shortest path distances
|
|
1357
|
-
*/
|
|
1358
|
-
function _computeShortestPathDistances(G, weight) {
|
|
1359
|
-
const distances = {};
|
|
1360
|
-
const nodes = G.nodes ? G.nodes() : G;
|
|
1361
|
-
const edges = G.edges ? G.edges() : [];
|
|
1362
|
-
// Initialize distances with direct edges
|
|
1363
|
-
for (const node of nodes) {
|
|
1364
|
-
distances[node] = {};
|
|
1365
|
-
distances[node][node] = 0;
|
|
1366
|
-
for (const other of nodes) {
|
|
1367
|
-
if (node !== other) {
|
|
1368
|
-
distances[node][other] = Infinity;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
// Add direct edges
|
|
1373
|
-
for (const [source, target] of edges) {
|
|
1374
|
-
// In a real implementation, we would get the weight from the graph
|
|
1375
|
-
// For now, assume weight = 1 or use weight attribute if available
|
|
1376
|
-
let edgeWeight = 1;
|
|
1377
|
-
if (G.getEdgeData) {
|
|
1378
|
-
edgeWeight = G.getEdgeData(source, target, weight) || 1;
|
|
1379
|
-
}
|
|
1380
|
-
distances[source][target] = edgeWeight;
|
|
1381
|
-
distances[target][source] = edgeWeight; // Assuming undirected graph
|
|
1382
|
-
}
|
|
1383
|
-
// Floyd-Warshall algorithm for all-pairs shortest paths
|
|
1384
|
-
for (const k of nodes) {
|
|
1385
|
-
for (const i of nodes) {
|
|
1386
|
-
for (const j of nodes) {
|
|
1387
|
-
if (distances[i][k] + distances[k][j] < distances[i][j]) {
|
|
1388
|
-
distances[i][j] = distances[i][k] + distances[k][j];
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
return distances;
|
|
1394
|
-
}
|
|
1395
|
-
/**
|
|
1396
|
-
* Solve the Kamada-Kawai layout optimization problem
|
|
1397
|
-
*
|
|
1398
|
-
* @param distMatrix - Matrix of desired distances between nodes
|
|
1399
|
-
* @param positions - Initial node positions
|
|
1400
|
-
* @param dim - Dimension of layout
|
|
1401
|
-
* @returns Optimized node positions
|
|
1402
|
-
*/
|
|
1403
|
-
function _kamadaKawaiSolve(distMatrix, positions, dim) {
|
|
1404
|
-
// Implementation of L-BFGS optimization for Kamada-Kawai
|
|
1405
|
-
const nNodes = positions.length;
|
|
1406
|
-
const meanWeight = 1e-3;
|
|
1407
|
-
// Convert distances to inverse distances (with protection against division by zero)
|
|
1408
|
-
const invDistMatrix = distMatrix.map(row => row.map(d => d === 0 ? 0 : 1 / (d + 1e-3)));
|
|
1409
|
-
// Flatten positions for optimization
|
|
1410
|
-
let posVec = positions.flat();
|
|
1411
|
-
// Optimization parameters
|
|
1412
|
-
const maxIter = 500;
|
|
1413
|
-
const gtol = 1e-5;
|
|
1414
|
-
const m = 10; // L-BFGS memory size
|
|
1415
|
-
// Implement a simplified L-BFGS-B algorithm
|
|
1416
|
-
let alpha = 1.0;
|
|
1417
|
-
const oldValues = [];
|
|
1418
|
-
const oldGrads = [];
|
|
1419
|
-
for (let iter = 0; iter < maxIter; iter++) {
|
|
1420
|
-
// Calculate cost and gradient
|
|
1421
|
-
const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
|
|
1422
|
-
// Compute search direction using L-BFGS approximation
|
|
1423
|
-
const direction = _lbfgsDirection(grad, oldValues, oldGrads, m);
|
|
1424
|
-
// Simple line search for step size
|
|
1425
|
-
alpha = _backtrackingLineSearch(posVec, direction, cost, grad, (x) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0], alpha);
|
|
1426
|
-
// Save current position and gradient for next iteration
|
|
1427
|
-
const oldPos = [...posVec];
|
|
1428
|
-
// Update position
|
|
1429
|
-
for (let i = 0; i < posVec.length; i++) {
|
|
1430
|
-
posVec[i] += alpha * direction[i];
|
|
1431
|
-
}
|
|
1432
|
-
// Calculate new gradient
|
|
1433
|
-
const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
|
|
1434
|
-
// Update L-BFGS memory
|
|
1435
|
-
oldValues.push(posVec.map((val, i) => val - oldPos[i]));
|
|
1436
|
-
oldGrads.push(newGrad.map((val, i) => val - grad[i]));
|
|
1437
|
-
// Keep only m most recent updates
|
|
1438
|
-
if (oldValues.length > m) {
|
|
1439
|
-
oldValues.shift();
|
|
1440
|
-
oldGrads.shift();
|
|
1441
|
-
}
|
|
1442
|
-
// Check convergence
|
|
1443
|
-
const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0));
|
|
1444
|
-
if (gradNorm < gtol) {
|
|
1445
|
-
break;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
// Reshape result back into positions array
|
|
1449
|
-
const result = [];
|
|
1450
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1451
|
-
result.push(posVec.slice(i * dim, (i + 1) * dim));
|
|
1452
|
-
}
|
|
1453
|
-
return result;
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* Cost function and gradient for Kamada-Kawai layout algorithm
|
|
1457
|
-
*
|
|
1458
|
-
* @param posVec - Flattened position array
|
|
1459
|
-
* @param invDist - Inverse distance matrix
|
|
1460
|
-
* @param meanWeight - Weight for centering positions
|
|
1461
|
-
* @param dim - Dimension of layout
|
|
1462
|
-
* @returns Array with [cost, gradient]
|
|
1463
|
-
*/
|
|
1464
|
-
function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
|
|
1465
|
-
const nNodes = invDist.length;
|
|
1466
|
-
const positions = [];
|
|
1467
|
-
// Reshape flat vector into positions array
|
|
1468
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1469
|
-
positions.push(posVec.slice(i * dim, (i + 1) * dim));
|
|
1470
|
-
}
|
|
1471
|
-
// Calculate cost
|
|
1472
|
-
let cost = 0;
|
|
1473
|
-
// Add mean position penalty term
|
|
1474
|
-
const sumPos = Array(dim).fill(0);
|
|
1475
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1476
|
-
for (let d = 0; d < dim; d++) {
|
|
1477
|
-
sumPos[d] += positions[i][d];
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0);
|
|
1481
|
-
// Add distance penalty terms
|
|
1482
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1483
|
-
for (let j = i + 1; j < nNodes; j++) {
|
|
1484
|
-
// Calculate actual distance
|
|
1485
|
-
const diff = positions[i].map((val, d) => val - positions[j][d]);
|
|
1486
|
-
const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0));
|
|
1487
|
-
// Add penalty for difference between actual and ideal distance
|
|
1488
|
-
const idealInvDist = invDist[i][j];
|
|
1489
|
-
const offset = distance * idealInvDist - 1.0;
|
|
1490
|
-
cost += 0.5 * offset * offset;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
// Calculate gradient
|
|
1494
|
-
const grad = new Array(posVec.length).fill(0);
|
|
1495
|
-
// Add gradient of mean position penalty
|
|
1496
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1497
|
-
for (let d = 0; d < dim; d++) {
|
|
1498
|
-
grad[i * dim + d] += meanWeight * sumPos[d];
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
// Add gradient of distance penalties
|
|
1502
|
-
for (let i = 0; i < nNodes; i++) {
|
|
1503
|
-
for (let j = i + 1; j < nNodes; j++) {
|
|
1504
|
-
// Calculate actual distance and direction
|
|
1505
|
-
const diff = positions[i].map((val, d) => val - positions[j][d]);
|
|
1506
|
-
const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10;
|
|
1507
|
-
const direction = diff.map(d => d / distance);
|
|
1508
|
-
// Calculate contribution to gradient
|
|
1509
|
-
const idealInvDist = invDist[i][j];
|
|
1510
|
-
const offset = distance * idealInvDist - 1.0;
|
|
1511
|
-
for (let d = 0; d < dim; d++) {
|
|
1512
|
-
const force = idealInvDist * offset * direction[d];
|
|
1513
|
-
grad[i * dim + d] += force;
|
|
1514
|
-
grad[j * dim + d] -= force;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
return [cost, grad];
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Compute the search direction using L-BFGS approximation
|
|
1522
|
-
*
|
|
1523
|
-
* @param grad - Current gradient
|
|
1524
|
-
* @param sList - List of position differences (s_k)
|
|
1525
|
-
* @param yList - List of gradient differences (y_k)
|
|
1526
|
-
* @param m - Memory size
|
|
1527
|
-
* @returns Direction vector
|
|
1528
|
-
*/
|
|
1529
|
-
function _lbfgsDirection(grad, sList, yList, m) {
|
|
1530
|
-
if (sList.length === 0) {
|
|
1531
|
-
// First iteration - use negative gradient
|
|
1532
|
-
return grad.map(g => -g);
|
|
1533
|
-
}
|
|
1534
|
-
const q = grad.slice();
|
|
1535
|
-
const alpha = Array(sList.length).fill(0);
|
|
1536
|
-
const rho = [];
|
|
1537
|
-
// Compute rho values
|
|
1538
|
-
for (let i = 0; i < sList.length; i++) {
|
|
1539
|
-
const s = sList[i];
|
|
1540
|
-
const y = yList[i];
|
|
1541
|
-
rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
|
|
1542
|
-
}
|
|
1543
|
-
// Forward pass
|
|
1544
|
-
for (let i = sList.length - 1; i >= 0; i--) {
|
|
1545
|
-
const s = sList[i];
|
|
1546
|
-
alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0);
|
|
1547
|
-
for (let j = 0; j < q.length; j++) {
|
|
1548
|
-
q[j] -= alpha[i] * yList[i][j];
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
// Scale initial Hessian approximation
|
|
1552
|
-
let gamma = 1;
|
|
1553
|
-
if (sList.length > 0 && yList.length > 0) {
|
|
1554
|
-
const y = yList[yList.length - 1];
|
|
1555
|
-
const s = sList[sList.length - 1];
|
|
1556
|
-
gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
|
|
1557
|
-
y.reduce((sum, val) => sum + val * val, 0);
|
|
1558
|
-
}
|
|
1559
|
-
// Initialize direction with scaled negative gradient
|
|
1560
|
-
const direction = q.map(val => -gamma * val);
|
|
1561
|
-
// Backward pass
|
|
1562
|
-
for (let i = 0; i < sList.length; i++) {
|
|
1563
|
-
const s = sList[i];
|
|
1564
|
-
const y = yList[i];
|
|
1565
|
-
const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0);
|
|
1566
|
-
for (let j = 0; j < direction.length; j++) {
|
|
1567
|
-
direction[j] += s[j] * (alpha[i] - beta);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return direction;
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Backtracking line search to find step size
|
|
1574
|
-
*
|
|
1575
|
-
* @param x - Current position
|
|
1576
|
-
* @param direction - Search direction
|
|
1577
|
-
* @param f - Function value at current position
|
|
1578
|
-
* @param grad - Gradient at current position
|
|
1579
|
-
* @param func - Function to evaluate cost
|
|
1580
|
-
* @param alpha0 - Initial step size
|
|
1581
|
-
* @returns Optimal step size
|
|
1582
|
-
*/
|
|
1583
|
-
function _backtrackingLineSearch(x, direction, f, grad, func, alpha0) {
|
|
1584
|
-
const c1 = 1e-4;
|
|
1585
|
-
const c2 = 0.9;
|
|
1586
|
-
const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
|
|
1587
|
-
if (initialSlope >= 0) {
|
|
1588
|
-
return 1e-8; // Direction is not a descent direction
|
|
1589
|
-
}
|
|
1590
|
-
let alpha = alpha0;
|
|
1591
|
-
const maxIter = 20;
|
|
1592
|
-
for (let i = 0; i < maxIter; i++) {
|
|
1593
|
-
// Try step
|
|
1594
|
-
const newX = x.map((val, i) => val + alpha * direction[i]);
|
|
1595
|
-
const newF = func(newX);
|
|
1596
|
-
// Check sufficient decrease condition (Armijo condition)
|
|
1597
|
-
if (newF <= f + c1 * alpha * initialSlope) {
|
|
1598
|
-
return alpha;
|
|
1599
|
-
}
|
|
1600
|
-
// Reduce step size
|
|
1601
|
-
alpha *= c2;
|
|
1602
|
-
}
|
|
1603
|
-
return alpha; // Return last alpha even if not optimal
|
|
1604
|
-
}
|
|
1605
|
-
/**
|
|
1606
|
-
* Position nodes using the ForceAtlas2 force-directed algorithm.
|
|
1607
|
-
*
|
|
1608
|
-
* @param G - Graph
|
|
1609
|
-
* @param pos - Initial positions for nodes
|
|
1610
|
-
* @param maxIter - Maximum number of iterations
|
|
1611
|
-
* @param jitterTolerance - Controls tolerance for node speed adjustments
|
|
1612
|
-
* @param scalingRatio - Scaling of attraction and repulsion forces
|
|
1613
|
-
* @param gravity - Attraction to center to prevent disconnected components from drifting
|
|
1614
|
-
* @param distributedAction - Distributes attraction force among nodes
|
|
1615
|
-
* @param strongGravity - Uses a stronger gravity model
|
|
1616
|
-
* @param nodeMass - Dictionary mapping nodes to their masses
|
|
1617
|
-
* @param nodeSize - Dictionary mapping nodes to their sizes
|
|
1618
|
-
* @param weight - Edge attribute for weight
|
|
1619
|
-
* @param dissuadeHubs - Whether to prevent hub nodes from clustering
|
|
1620
|
-
* @param linlog - Whether to use logarithmic attraction
|
|
1621
|
-
* @param seed - Random seed for initial positions
|
|
1622
|
-
* @param dim - Dimension of layout
|
|
1623
|
-
* @returns Positions dictionary keyed by node
|
|
1624
|
-
*/
|
|
1625
|
-
function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1.0, scalingRatio = 2.0, gravity = 1.0, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) {
|
|
1626
|
-
const processed = _processParams(G, null, dim);
|
|
1627
|
-
const graph = processed.G;
|
|
1628
|
-
const nodes = getNodesFromGraph(graph);
|
|
1629
|
-
if (nodes.length === 0) {
|
|
1630
|
-
return {};
|
|
1631
|
-
}
|
|
1632
|
-
// Initialize random number generator
|
|
1633
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
1634
|
-
// Initialize positions if not provided
|
|
1635
|
-
let posArray;
|
|
1636
|
-
if (pos === null) {
|
|
1637
|
-
pos = {};
|
|
1638
|
-
posArray = new Array(nodes.length);
|
|
1639
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1640
|
-
posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
1641
|
-
pos[nodes[i]] = posArray[i];
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
else if (Object.keys(pos).length === nodes.length) {
|
|
1645
|
-
// Use provided positions
|
|
1646
|
-
posArray = new Array(nodes.length);
|
|
1647
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1648
|
-
posArray[i] = [...pos[nodes[i]]];
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
else {
|
|
1652
|
-
// Some nodes don't have positions, initialize within the range of existing positions
|
|
1653
|
-
let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
1654
|
-
let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
1655
|
-
// Find min and max of existing positions
|
|
1656
|
-
for (const node in pos) {
|
|
1657
|
-
for (let d = 0; d < dim; d++) {
|
|
1658
|
-
minPos[d] = Math.min(minPos[d], pos[node][d]);
|
|
1659
|
-
maxPos[d] = Math.max(maxPos[d], pos[node][d]);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
posArray = new Array(nodes.length);
|
|
1663
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1664
|
-
const node = nodes[i];
|
|
1665
|
-
if (pos[node]) {
|
|
1666
|
-
posArray[i] = [...pos[node]];
|
|
1667
|
-
}
|
|
1668
|
-
else {
|
|
1669
|
-
posArray[i] = Array(dim).fill(0).map((_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d]));
|
|
1670
|
-
pos[node] = posArray[i];
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
// Initialize mass and size arrays
|
|
1675
|
-
const mass = new Array(nodes.length).fill(0);
|
|
1676
|
-
const size = new Array(nodes.length).fill(0);
|
|
1677
|
-
// Flag to track whether to adjust for node sizes
|
|
1678
|
-
const adjustSizes = nodeSize !== null;
|
|
1679
|
-
// Set node masses and sizes
|
|
1680
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
1681
|
-
const node = nodes[i];
|
|
1682
|
-
mass[i] = nodeMass && nodeMass[node] ?
|
|
1683
|
-
nodeMass[node] :
|
|
1684
|
-
(graph.edges ? getNodeDegree(graph, node) + 1 : 1);
|
|
1685
|
-
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
1686
|
-
}
|
|
1687
|
-
// Create adjacency matrix
|
|
1688
|
-
const n = nodes.length;
|
|
1689
|
-
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
1690
|
-
// Populate adjacency matrix with edge weights
|
|
1691
|
-
const edges = graph.edges ? graph.edges() : [];
|
|
1692
|
-
const nodeIndices = {};
|
|
1693
|
-
nodes.forEach((node, i) => { nodeIndices[node] = i; });
|
|
1694
|
-
for (const [source, target] of edges) {
|
|
1695
|
-
const i = nodeIndices[source];
|
|
1696
|
-
const j = nodeIndices[target];
|
|
1697
|
-
// Use edge weight if provided, otherwise default to 1
|
|
1698
|
-
let edgeWeight = 1;
|
|
1699
|
-
if (weight && graph.getEdgeData) {
|
|
1700
|
-
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
1701
|
-
}
|
|
1702
|
-
A[i][j] = edgeWeight;
|
|
1703
|
-
A[j][i] = edgeWeight; // For undirected graphs
|
|
1704
|
-
}
|
|
1705
|
-
// Initialize force arrays
|
|
1706
|
-
const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
1707
|
-
const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
1708
|
-
const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
1709
|
-
// Simulation parameters
|
|
1710
|
-
let speed = 1;
|
|
1711
|
-
let speedEfficiency = 1;
|
|
1712
|
-
let swing = 1;
|
|
1713
|
-
let traction = 1;
|
|
1714
|
-
// Helper function to estimate factor for force scaling
|
|
1715
|
-
function estimateFactor(n, swing, traction, speed, speedEfficiency, jitterTolerance) {
|
|
1716
|
-
// Optimal jitter parameters
|
|
1717
|
-
const optJitter = 0.05 * Math.sqrt(n);
|
|
1718
|
-
const minJitter = Math.sqrt(optJitter);
|
|
1719
|
-
const maxJitter = 10;
|
|
1720
|
-
const minSpeedEfficiency = 0.05;
|
|
1721
|
-
// Estimate jitter based on current state
|
|
1722
|
-
const other = Math.min(maxJitter, optJitter * traction / (n * n));
|
|
1723
|
-
let jitter = jitterTolerance * Math.max(minJitter, other);
|
|
1724
|
-
// Adjust speed efficiency based on swing/traction ratio
|
|
1725
|
-
if (swing / traction > 2.0) {
|
|
1726
|
-
if (speedEfficiency > minSpeedEfficiency) {
|
|
1727
|
-
speedEfficiency *= 0.5;
|
|
1728
|
-
}
|
|
1729
|
-
jitter = Math.max(jitter, jitterTolerance);
|
|
1730
|
-
}
|
|
1731
|
-
// Calculate target speed
|
|
1732
|
-
let targetSpeed = swing === 0 ?
|
|
1733
|
-
Number.POSITIVE_INFINITY :
|
|
1734
|
-
jitter * speedEfficiency * traction / swing;
|
|
1735
|
-
// Further adjust speed efficiency
|
|
1736
|
-
if (swing > jitter * traction) {
|
|
1737
|
-
if (speedEfficiency > minSpeedEfficiency) {
|
|
1738
|
-
speedEfficiency *= 0.7;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
else if (speed < 1000) {
|
|
1742
|
-
speedEfficiency *= 1.3;
|
|
1743
|
-
}
|
|
1744
|
-
// Limit the speed increase
|
|
1745
|
-
const maxRise = 0.5;
|
|
1746
|
-
speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
|
|
1747
|
-
return [speed, speedEfficiency];
|
|
1748
|
-
}
|
|
1749
|
-
// Main simulation loop
|
|
1750
|
-
for (let iter = 0; iter < maxIter; iter++) {
|
|
1751
|
-
// Reset forces for this iteration
|
|
1752
|
-
for (let i = 0; i < n; i++) {
|
|
1753
|
-
for (let d = 0; d < dim; d++) {
|
|
1754
|
-
attraction[i][d] = 0;
|
|
1755
|
-
repulsion[i][d] = 0;
|
|
1756
|
-
gravities[i][d] = 0;
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
// Compute pairwise differences and distances
|
|
1760
|
-
const diff = Array(n).fill(0).map(() => Array(n).fill(0).map(() => Array(dim).fill(0)));
|
|
1761
|
-
const distance = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
1762
|
-
for (let i = 0; i < n; i++) {
|
|
1763
|
-
for (let j = 0; j < n; j++) {
|
|
1764
|
-
if (i === j)
|
|
1765
|
-
continue;
|
|
1766
|
-
for (let d = 0; d < dim; d++) {
|
|
1767
|
-
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
1768
|
-
}
|
|
1769
|
-
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
1770
|
-
// Prevent division by zero
|
|
1771
|
-
if (distance[i][j] < 0.01)
|
|
1772
|
-
distance[i][j] = 0.01;
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
// Calculate attraction forces
|
|
1776
|
-
if (linlog) {
|
|
1777
|
-
// Logarithmic attraction model
|
|
1778
|
-
for (let i = 0; i < n; i++) {
|
|
1779
|
-
for (let j = 0; j < n; j++) {
|
|
1780
|
-
if (i === j || A[i][j] === 0)
|
|
1781
|
-
continue;
|
|
1782
|
-
const dist = distance[i][j];
|
|
1783
|
-
const factor = -Math.log(1 + dist) / dist * A[i][j];
|
|
1784
|
-
for (let d = 0; d < dim; d++) {
|
|
1785
|
-
const force = factor * diff[i][j][d];
|
|
1786
|
-
attraction[i][d] += force;
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
else {
|
|
1792
|
-
// Linear attraction model
|
|
1793
|
-
for (let i = 0; i < n; i++) {
|
|
1794
|
-
for (let j = 0; j < n; j++) {
|
|
1795
|
-
if (i === j || A[i][j] === 0)
|
|
1796
|
-
continue;
|
|
1797
|
-
for (let d = 0; d < dim; d++) {
|
|
1798
|
-
const force = -diff[i][j][d] * A[i][j];
|
|
1799
|
-
attraction[i][d] += force;
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
// Apply distributed attraction if enabled
|
|
1805
|
-
if (distributedAction) {
|
|
1806
|
-
for (let i = 0; i < n; i++) {
|
|
1807
|
-
for (let d = 0; d < dim; d++) {
|
|
1808
|
-
attraction[i][d] /= mass[i];
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
// Calculate repulsion forces
|
|
1813
|
-
for (let i = 0; i < n; i++) {
|
|
1814
|
-
for (let j = 0; j < n; j++) {
|
|
1815
|
-
if (i === j)
|
|
1816
|
-
continue;
|
|
1817
|
-
let dist = distance[i][j];
|
|
1818
|
-
// Adjust distance for node sizes if needed
|
|
1819
|
-
if (adjustSizes) {
|
|
1820
|
-
dist -= size[i] - size[j];
|
|
1821
|
-
dist = Math.max(dist, 0.01); // Prevent negative or zero distances
|
|
1822
|
-
}
|
|
1823
|
-
const distSquared = dist * dist;
|
|
1824
|
-
const massProduct = mass[i] * mass[j];
|
|
1825
|
-
const factor = (massProduct / distSquared) * scalingRatio;
|
|
1826
|
-
for (let d = 0; d < dim; d++) {
|
|
1827
|
-
const direction = diff[i][j][d] / dist;
|
|
1828
|
-
repulsion[i][d] += direction * factor;
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
// Calculate gravity forces
|
|
1833
|
-
// First find the center of mass
|
|
1834
|
-
const centerOfMass = Array(dim).fill(0);
|
|
1835
|
-
for (let i = 0; i < n; i++) {
|
|
1836
|
-
for (let d = 0; d < dim; d++) {
|
|
1837
|
-
centerOfMass[d] += posArray[i][d] / n;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
for (let i = 0; i < n; i++) {
|
|
1841
|
-
const posCentered = Array(dim);
|
|
1842
|
-
for (let d = 0; d < dim; d++) {
|
|
1843
|
-
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
1844
|
-
}
|
|
1845
|
-
if (strongGravity) {
|
|
1846
|
-
// Strong gravity model
|
|
1847
|
-
for (let d = 0; d < dim; d++) {
|
|
1848
|
-
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
else {
|
|
1852
|
-
// Regular gravity model
|
|
1853
|
-
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
1854
|
-
if (dist > 0.01) {
|
|
1855
|
-
for (let d = 0; d < dim; d++) {
|
|
1856
|
-
const direction = posCentered[d] / dist;
|
|
1857
|
-
gravities[i][d] = -gravity * mass[i] * direction;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
// Calculate total forces and update positions
|
|
1863
|
-
const update = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
1864
|
-
let totalSwing = 0;
|
|
1865
|
-
let totalTraction = 0;
|
|
1866
|
-
for (let i = 0; i < n; i++) {
|
|
1867
|
-
for (let d = 0; d < dim; d++) {
|
|
1868
|
-
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
1869
|
-
}
|
|
1870
|
-
// Calculate swing and traction for this node
|
|
1871
|
-
const oldPos = [...posArray[i]];
|
|
1872
|
-
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
1873
|
-
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
1874
|
-
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
1875
|
-
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
1876
|
-
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
1877
|
-
totalSwing += mass[i] * swingMagnitude;
|
|
1878
|
-
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
1879
|
-
}
|
|
1880
|
-
// Update speed and efficiency
|
|
1881
|
-
[speed, speedEfficiency] = estimateFactor(n, totalSwing, totalTraction, speed, speedEfficiency, jitterTolerance);
|
|
1882
|
-
// Apply forces to update positions
|
|
1883
|
-
let totalMovement = 0;
|
|
1884
|
-
for (let i = 0; i < n; i++) {
|
|
1885
|
-
let factor;
|
|
1886
|
-
if (adjustSizes) {
|
|
1887
|
-
// Calculate displacement magnitude
|
|
1888
|
-
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1889
|
-
const swinging = mass[i] * df;
|
|
1890
|
-
// Determine scaling factor with size adjustments
|
|
1891
|
-
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
1892
|
-
factor = Math.min(factor * df, 10) / df;
|
|
1893
|
-
}
|
|
1894
|
-
else {
|
|
1895
|
-
// Standard scaling factor
|
|
1896
|
-
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1897
|
-
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
1898
|
-
}
|
|
1899
|
-
// Apply factor to update position
|
|
1900
|
-
for (let d = 0; d < dim; d++) {
|
|
1901
|
-
const movement = update[i][d] * factor;
|
|
1902
|
-
posArray[i][d] += movement;
|
|
1903
|
-
totalMovement += Math.abs(movement);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
// Check for convergence
|
|
1907
|
-
if (totalMovement < 1e-10) {
|
|
1908
|
-
break;
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
// Create position dictionary
|
|
1912
|
-
const positions = {};
|
|
1913
|
-
for (let i = 0; i < n; i++) {
|
|
1914
|
-
positions[nodes[i]] = posArray[i];
|
|
1915
|
-
}
|
|
1916
|
-
return rescaleLayout(positions);
|
|
1917
|
-
// Helper function to get node degree
|
|
1918
|
-
function getNodeDegree(graph, node) {
|
|
1919
|
-
if (!graph.edges)
|
|
1920
|
-
return 0;
|
|
1921
|
-
return graph.edges().filter((edge) => edge[0] === node || edge[1] === node).length;
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
/**
|
|
1925
|
-
* Layout algorithm with attractive and repulsive forces (ARF).
|
|
1926
|
-
*
|
|
1927
|
-
* @param G - Graph
|
|
1928
|
-
* @param pos - Initial positions for nodes
|
|
1929
|
-
* @param scaling - Scale factor for positions
|
|
1930
|
-
* @param a - Strength of springs between connected nodes (should be > 1)
|
|
1931
|
-
* @param maxIter - Maximum number of iterations
|
|
1932
|
-
* @param seed - Random seed for initial positions
|
|
1933
|
-
* @returns Positions dictionary keyed by node
|
|
1934
|
-
*/
|
|
1935
|
-
function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1000, seed = null) {
|
|
1936
|
-
if (a <= 1) {
|
|
1937
|
-
throw new Error("The parameter a should be larger than 1");
|
|
1938
|
-
}
|
|
1939
|
-
const nodes = getNodesFromGraph(G);
|
|
1940
|
-
const edges = getEdgesFromGraph(G);
|
|
1941
|
-
if (nodes.length === 0) {
|
|
1942
|
-
return {};
|
|
1943
|
-
}
|
|
1944
|
-
// Initialize positions if not provided
|
|
1945
|
-
if (!pos) {
|
|
1946
|
-
pos = randomLayout(G, null, 2, seed);
|
|
1947
|
-
}
|
|
1948
|
-
else {
|
|
1949
|
-
// Make sure all nodes have positions
|
|
1950
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
1951
|
-
const defaultPos = {};
|
|
1952
|
-
nodes.forEach((node) => {
|
|
1953
|
-
if (!pos[node]) {
|
|
1954
|
-
defaultPos[node] = [rng.rand(), rng.rand()];
|
|
1955
|
-
}
|
|
1956
|
-
});
|
|
1957
|
-
pos = { ...pos, ...defaultPos };
|
|
1958
|
-
}
|
|
1959
|
-
// Create node index mapping
|
|
1960
|
-
const nodeIndex = {};
|
|
1961
|
-
nodes.forEach((node, i) => {
|
|
1962
|
-
nodeIndex[node] = i;
|
|
1963
|
-
});
|
|
1964
|
-
// Create positions array
|
|
1965
|
-
const positions = nodes.map((node) => [...pos[node]]);
|
|
1966
|
-
// Initialize spring constant matrix
|
|
1967
|
-
const N = nodes.length;
|
|
1968
|
-
const K = Array(N).fill(0).map(() => Array(N).fill(1));
|
|
1969
|
-
// Set diagonal to zero (no self-attraction)
|
|
1970
|
-
for (let i = 0; i < N; i++) {
|
|
1971
|
-
K[i][i] = 0;
|
|
1972
|
-
}
|
|
1973
|
-
// Set stronger attraction between connected nodes
|
|
1974
|
-
for (const [source, target] of edges) {
|
|
1975
|
-
if (source === target)
|
|
1976
|
-
continue;
|
|
1977
|
-
const i = nodeIndex[source];
|
|
1978
|
-
const j = nodeIndex[target];
|
|
1979
|
-
K[i][j] = a;
|
|
1980
|
-
K[j][i] = a;
|
|
1981
|
-
}
|
|
1982
|
-
// Calculate rho (scale factor)
|
|
1983
|
-
const rho = scaling * Math.sqrt(N);
|
|
1984
|
-
// Optimization loop
|
|
1985
|
-
const dt = 1e-3; // Time step
|
|
1986
|
-
const etol = 1e-6; // Error tolerance
|
|
1987
|
-
let error = etol + 1;
|
|
1988
|
-
let nIter = 0;
|
|
1989
|
-
while (error > etol && nIter < maxIter) {
|
|
1990
|
-
// Calculate changes for each node
|
|
1991
|
-
const change = Array(N).fill(0).map(() => [0, 0]);
|
|
1992
|
-
for (let i = 0; i < N; i++) {
|
|
1993
|
-
for (let j = 0; j < N; j++) {
|
|
1994
|
-
if (i === j)
|
|
1995
|
-
continue;
|
|
1996
|
-
// Calculate difference vector
|
|
1997
|
-
const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
|
|
1998
|
-
// Calculate distance (with minimum to avoid division by zero)
|
|
1999
|
-
const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
|
|
2000
|
-
// Calculate attractive and repulsive forces
|
|
2001
|
-
for (let d = 0; d < diff.length; d++) {
|
|
2002
|
-
change[i][d] += K[i][j] * diff[d] - (rho / dist) * diff[d];
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
// Update positions
|
|
2007
|
-
for (let i = 0; i < N; i++) {
|
|
2008
|
-
for (let d = 0; d < positions[i].length; d++) {
|
|
2009
|
-
positions[i][d] += change[i][d] * dt;
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
// Calculate error (sum of force magnitudes)
|
|
2013
|
-
error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
|
|
2014
|
-
nIter++;
|
|
2015
|
-
}
|
|
2016
|
-
// Convert positions array back to object
|
|
2017
|
-
const finalPos = {};
|
|
2018
|
-
nodes.forEach((node, i) => {
|
|
2019
|
-
finalPos[node] = positions[i];
|
|
2020
|
-
});
|
|
2021
|
-
return finalPos;
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Return a dictionary of scaled positions keyed by node.
|
|
2025
|
-
*
|
|
2026
|
-
* @param pos - Dictionary of positions keyed by node
|
|
2027
|
-
* @param scale - Scale factor for positions
|
|
2028
|
-
* @returns Dictionary of scaled positions
|
|
2029
|
-
*/
|
|
2030
|
-
function rescaleLayoutDict(pos, scale = 1) {
|
|
2031
|
-
if (Object.keys(pos).length === 0) {
|
|
2032
|
-
return {};
|
|
2033
|
-
}
|
|
2034
|
-
// Extract positions as array
|
|
2035
|
-
const posArray = Object.values(pos);
|
|
2036
|
-
// Find center of positions
|
|
2037
|
-
const center = [];
|
|
2038
|
-
for (let d = 0; d < posArray[0].length; d++) {
|
|
2039
|
-
center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
|
|
2040
|
-
}
|
|
2041
|
-
// Center positions
|
|
2042
|
-
const centeredPos = {};
|
|
2043
|
-
for (const [node, p] of Object.entries(pos)) {
|
|
2044
|
-
centeredPos[node] = p.map((val, d) => val - center[d]);
|
|
2045
|
-
}
|
|
2046
|
-
// Find maximum distance from center
|
|
2047
|
-
let maxDist = 0;
|
|
2048
|
-
for (const p of Object.values(centeredPos)) {
|
|
2049
|
-
const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
|
|
2050
|
-
maxDist = Math.max(maxDist, dist);
|
|
2051
|
-
}
|
|
2052
|
-
// Scale positions
|
|
2053
|
-
const scaledPos = {};
|
|
2054
|
-
if (maxDist > 0) {
|
|
2055
|
-
for (const [node, p] of Object.entries(centeredPos)) {
|
|
2056
|
-
scaledPos[node] = p.map(val => val * scale / maxDist);
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
else {
|
|
2060
|
-
// All points at the center
|
|
2061
|
-
for (const node of Object.keys(centeredPos)) {
|
|
2062
|
-
scaledPos[node] = Array(centeredPos[node].length).fill(0);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
return scaledPos;
|
|
2066
|
-
}
|
|
2067
|
-
// Graph generation utilities
|
|
2068
|
-
/**
|
|
2069
|
-
* Create a complete graph with n nodes
|
|
2070
|
-
* @param n - Number of nodes
|
|
2071
|
-
* @returns Graph object with all nodes connected to all other nodes
|
|
2072
|
-
*/
|
|
2073
|
-
function completeGraph(n) {
|
|
2074
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2075
|
-
const edges = [];
|
|
2076
|
-
for (let i = 0; i < n; i++) {
|
|
2077
|
-
for (let j = i + 1; j < n; j++) {
|
|
2078
|
-
edges.push([i, j]);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
return {
|
|
2082
|
-
nodes: () => nodes,
|
|
2083
|
-
edges: () => edges
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
|
-
/**
|
|
2087
|
-
* Create a cycle graph with n nodes
|
|
2088
|
-
* @param n - Number of nodes
|
|
2089
|
-
* @returns Graph object with nodes connected in a cycle
|
|
2090
|
-
*/
|
|
2091
|
-
function cycleGraph(n) {
|
|
2092
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2093
|
-
const edges = [];
|
|
2094
|
-
for (let i = 0; i < n; i++) {
|
|
2095
|
-
edges.push([i, (i + 1) % n]);
|
|
2096
|
-
}
|
|
2097
|
-
return {
|
|
2098
|
-
nodes: () => nodes,
|
|
2099
|
-
edges: () => edges
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
/**
|
|
2103
|
-
* Create a star graph with n nodes (1 center + n-1 leaves)
|
|
2104
|
-
* @param n - Total number of nodes
|
|
2105
|
-
* @returns Graph object with star topology
|
|
2106
|
-
*/
|
|
2107
|
-
function starGraph(n) {
|
|
2108
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2109
|
-
const edges = [];
|
|
2110
|
-
// Connect all nodes to node 0 (center)
|
|
2111
|
-
for (let i = 1; i < n; i++) {
|
|
2112
|
-
edges.push([0, i]);
|
|
2113
|
-
}
|
|
2114
|
-
return {
|
|
2115
|
-
nodes: () => nodes,
|
|
2116
|
-
edges: () => edges
|
|
2117
|
-
};
|
|
2118
|
-
}
|
|
2119
|
-
/**
|
|
2120
|
-
* Create a wheel graph with n nodes (1 center + n-1 rim nodes)
|
|
2121
|
-
* @param n - Total number of nodes
|
|
2122
|
-
* @returns Graph object with wheel topology
|
|
2123
|
-
*/
|
|
2124
|
-
function wheelGraph(n) {
|
|
2125
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2126
|
-
const edges = [];
|
|
2127
|
-
// Connect all rim nodes to center (node 0)
|
|
2128
|
-
for (let i = 1; i < n; i++) {
|
|
2129
|
-
edges.push([0, i]);
|
|
2130
|
-
}
|
|
2131
|
-
// Connect rim nodes in a cycle
|
|
2132
|
-
for (let i = 1; i < n - 1; i++) {
|
|
2133
|
-
edges.push([i, i + 1]);
|
|
2134
|
-
}
|
|
2135
|
-
if (n > 2) {
|
|
2136
|
-
edges.push([n - 1, 1]);
|
|
2137
|
-
}
|
|
2138
|
-
return {
|
|
2139
|
-
nodes: () => nodes,
|
|
2140
|
-
edges: () => edges
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Create a grid graph with rows x cols nodes
|
|
2145
|
-
* @param rows - Number of rows
|
|
2146
|
-
* @param cols - Number of columns
|
|
2147
|
-
* @returns Graph object with grid topology
|
|
2148
|
-
*/
|
|
2149
|
-
function gridGraph(rows, cols) {
|
|
2150
|
-
const nodes = [];
|
|
2151
|
-
const edges = [];
|
|
2152
|
-
// Create nodes
|
|
2153
|
-
for (let i = 0; i < rows; i++) {
|
|
2154
|
-
for (let j = 0; j < cols; j++) {
|
|
2155
|
-
nodes.push(`${i},${j}`);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
// Create edges
|
|
2159
|
-
for (let i = 0; i < rows; i++) {
|
|
2160
|
-
for (let j = 0; j < cols; j++) {
|
|
2161
|
-
// Connect to right neighbor
|
|
2162
|
-
if (j < cols - 1) {
|
|
2163
|
-
edges.push([`${i},${j}`, `${i},${j + 1}`]);
|
|
2164
|
-
}
|
|
2165
|
-
// Connect to bottom neighbor
|
|
2166
|
-
if (i < rows - 1) {
|
|
2167
|
-
edges.push([`${i},${j}`, `${i + 1},${j}`]);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
return {
|
|
2172
|
-
nodes: () => nodes,
|
|
2173
|
-
edges: () => edges
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
/**
|
|
2177
|
-
* Create a random graph with n nodes and given edge probability
|
|
2178
|
-
* @param n - Number of nodes
|
|
2179
|
-
* @param p - Probability of edge between any two nodes (0-1)
|
|
2180
|
-
* @param seed - Random seed for reproducibility
|
|
2181
|
-
* @returns Graph object with random edges
|
|
2182
|
-
*/
|
|
2183
|
-
function randomGraph(n, p, seed) {
|
|
2184
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2185
|
-
const edges = [];
|
|
2186
|
-
// Simple deterministic pseudo-random if seed provided
|
|
2187
|
-
let currentSeed = seed;
|
|
2188
|
-
let random = seed !== undefined
|
|
2189
|
-
? () => {
|
|
2190
|
-
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
2191
|
-
return currentSeed / 233280;
|
|
2192
|
-
}
|
|
2193
|
-
: Math.random;
|
|
2194
|
-
for (let i = 0; i < n; i++) {
|
|
2195
|
-
for (let j = i + 1; j < n; j++) {
|
|
2196
|
-
if (random() < p) {
|
|
2197
|
-
edges.push([i, j]);
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
return {
|
|
2202
|
-
nodes: () => nodes,
|
|
2203
|
-
edges: () => edges
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* Create a bipartite graph with two sets of nodes
|
|
2208
|
-
* @param n1 - Number of nodes in first set
|
|
2209
|
-
* @param n2 - Number of nodes in second set
|
|
2210
|
-
* @param p - Probability of edge between nodes in different sets
|
|
2211
|
-
* @param seed - Random seed for reproducibility
|
|
2212
|
-
* @returns Graph object with bipartite structure and setA/setB properties
|
|
2213
|
-
*/
|
|
2214
|
-
function bipartiteGraph(n1, n2, p, seed) {
|
|
2215
|
-
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
2216
|
-
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
2217
|
-
const nodes = [...setA, ...setB];
|
|
2218
|
-
const edges = [];
|
|
2219
|
-
// Simple deterministic pseudo-random if seed provided
|
|
2220
|
-
let currentSeed = seed;
|
|
2221
|
-
let random = seed !== undefined
|
|
2222
|
-
? () => {
|
|
2223
|
-
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
2224
|
-
return currentSeed / 233280;
|
|
2225
|
-
}
|
|
2226
|
-
: Math.random;
|
|
2227
|
-
// Only connect nodes between sets
|
|
2228
|
-
for (const a of setA) {
|
|
2229
|
-
for (const b of setB) {
|
|
2230
|
-
if (random() < p) {
|
|
2231
|
-
edges.push([a, b]);
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
return {
|
|
2236
|
-
nodes: () => nodes,
|
|
2237
|
-
edges: () => edges,
|
|
2238
|
-
setA,
|
|
2239
|
-
setB
|
|
2240
|
-
};
|
|
2241
|
-
}
|
|
2242
|
-
/**
|
|
2243
|
-
* Create a scale-free graph using Barabási-Albert model
|
|
2244
|
-
* @param n - Total number of nodes
|
|
2245
|
-
* @param m - Number of edges to attach from new node
|
|
2246
|
-
* @param seed - Random seed for reproducibility
|
|
2247
|
-
* @returns Graph object with scale-free properties
|
|
2248
|
-
*/
|
|
2249
|
-
function scaleFreeGraph(n, m, seed) {
|
|
2250
|
-
if (m >= n) {
|
|
2251
|
-
throw new Error('m must be less than n');
|
|
2252
|
-
}
|
|
2253
|
-
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
2254
|
-
const edges = [];
|
|
2255
|
-
const degrees = new Array(n).fill(0);
|
|
2256
|
-
// Simple deterministic pseudo-random if seed provided
|
|
2257
|
-
let currentSeed = seed;
|
|
2258
|
-
let random = seed !== undefined
|
|
2259
|
-
? () => {
|
|
2260
|
-
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
2261
|
-
return currentSeed / 233280;
|
|
2262
|
-
}
|
|
2263
|
-
: Math.random;
|
|
2264
|
-
// Start with complete graph of m+1 nodes
|
|
2265
|
-
for (let i = 0; i <= m; i++) {
|
|
2266
|
-
for (let j = i + 1; j <= m; j++) {
|
|
2267
|
-
edges.push([i, j]);
|
|
2268
|
-
degrees[i]++;
|
|
2269
|
-
degrees[j]++;
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
// Add remaining nodes
|
|
2273
|
-
for (let i = m + 1; i < n; i++) {
|
|
2274
|
-
const targets = new Set();
|
|
2275
|
-
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
2276
|
-
// Choose m targets based on preferential attachment
|
|
2277
|
-
while (targets.size < m) {
|
|
2278
|
-
let r = random() * totalDegree;
|
|
2279
|
-
let cumSum = 0;
|
|
2280
|
-
for (let j = 0; j < i; j++) {
|
|
2281
|
-
cumSum += degrees[j];
|
|
2282
|
-
if (r <= cumSum && !targets.has(j)) {
|
|
2283
|
-
targets.add(j);
|
|
2284
|
-
break;
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
// Add edges to targets
|
|
2289
|
-
for (const target of targets) {
|
|
2290
|
-
edges.push([i, target]);
|
|
2291
|
-
degrees[i]++;
|
|
2292
|
-
degrees[target]++;
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
return {
|
|
2296
|
-
nodes: () => nodes,
|
|
2297
|
-
edges: () => edges
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
// Export the layout functions
|
|
2301
|
-
export { randomLayout, circularLayout, shellLayout, springLayout, fruchtermanReingoldLayout, spectralLayout, spiralLayout, bipartiteLayout, multipartiteLayout, bfsLayout, planarLayout, kamadaKawaiLayout, forceatlas2Layout, arfLayout, rescaleLayout, rescaleLayoutDict,
|
|
2302
|
-
// Graph generation utilities
|
|
2303
|
-
completeGraph, cycleGraph, starGraph, wheelGraph, gridGraph, randomGraph, bipartiteGraph, scaleFreeGraph };
|
|
2304
|
-
//# sourceMappingURL=layout.js.map
|