@graphty/layout 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -3
- package/dist/layout.js +1762 -0
- package/dist/layout.js.map +1 -0
- package/package.json +4 -3
- package/dist/src/algorithms/index.d.ts +0 -5
- package/dist/src/algorithms/index.js +0 -6
- package/dist/src/algorithms/index.js.map +0 -1
- package/dist/src/algorithms/optimization/index.d.ts +0 -7
- package/dist/src/algorithms/optimization/index.js +0 -7
- package/dist/src/algorithms/optimization/index.js.map +0 -1
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +0 -32
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +0 -176
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +0 -1
- package/dist/src/algorithms/optimization/lbfgs.d.ts +0 -13
- package/dist/src/algorithms/optimization/lbfgs.js +0 -56
- package/dist/src/algorithms/optimization/lbfgs.js.map +0 -1
- package/dist/src/algorithms/optimization/line-search.d.ts +0 -15
- package/dist/src/algorithms/optimization/line-search.js +0 -37
- package/dist/src/algorithms/optimization/line-search.js.map +0 -1
- package/dist/src/algorithms/optimization/types.d.ts +0 -5
- package/dist/src/algorithms/optimization/types.js +0 -5
- package/dist/src/algorithms/optimization/types.js.map +0 -1
- package/dist/src/algorithms/planarity/check.d.ts +0 -17
- package/dist/src/algorithms/planarity/check.js +0 -30
- package/dist/src/algorithms/planarity/check.js.map +0 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +0 -29
- package/dist/src/algorithms/planarity/embedding.js +0 -179
- package/dist/src/algorithms/planarity/embedding.js.map +0 -1
- package/dist/src/algorithms/planarity/index.d.ts +0 -7
- package/dist/src/algorithms/planarity/index.js +0 -8
- package/dist/src/algorithms/planarity/index.js.map +0 -1
- package/dist/src/algorithms/planarity/lr-test.d.ts +0 -15
- package/dist/src/algorithms/planarity/lr-test.js +0 -54
- package/dist/src/algorithms/planarity/lr-test.js.map +0 -1
- package/dist/src/algorithms/planarity/special-graphs.d.ts +0 -28
- package/dist/src/algorithms/planarity/special-graphs.js +0 -108
- package/dist/src/algorithms/planarity/special-graphs.js.map +0 -1
- package/dist/src/generators/basic.d.ts +0 -28
- package/dist/src/generators/basic.js +0 -79
- package/dist/src/generators/basic.js.map +0 -1
- package/dist/src/generators/bipartite.d.ts +0 -16
- package/dist/src/generators/bipartite.js +0 -40
- package/dist/src/generators/bipartite.js.map +0 -1
- package/dist/src/generators/grid.d.ts +0 -11
- package/dist/src/generators/grid.js +0 -37
- package/dist/src/generators/grid.js.map +0 -1
- package/dist/src/generators/index.d.ts +0 -8
- package/dist/src/generators/index.js +0 -9
- package/dist/src/generators/index.js.map +0 -1
- package/dist/src/generators/random.d.ts +0 -12
- package/dist/src/generators/random.js +0 -34
- package/dist/src/generators/random.js.map +0 -1
- package/dist/src/generators/scale-free.d.ts +0 -12
- package/dist/src/generators/scale-free.js +0 -62
- package/dist/src/generators/scale-free.js.map +0 -1
- package/dist/src/index.d.ts +0 -10
- package/dist/src/index.js +0 -15
- package/dist/src/index.js.map +0 -1
- package/dist/src/layouts/basic/index.d.ts +0 -4
- package/dist/src/layouts/basic/index.js +0 -5
- package/dist/src/layouts/basic/index.js.map +0 -1
- package/dist/src/layouts/basic/random.d.ts +0 -14
- package/dist/src/layouts/basic/random.js +0 -27
- package/dist/src/layouts/basic/random.js.map +0 -1
- package/dist/src/layouts/force-directed/arf.d.ts +0 -13
- package/dist/src/layouts/force-directed/arf.js +0 -103
- package/dist/src/layouts/force-directed/arf.js.map +0 -1
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +0 -22
- package/dist/src/layouts/force-directed/forceatlas2.js +0 -340
- package/dist/src/layouts/force-directed/forceatlas2.js.map +0 -1
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +0 -19
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +0 -131
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +0 -1
- package/dist/src/layouts/force-directed/index.d.ts +0 -8
- package/dist/src/layouts/force-directed/index.js +0 -9
- package/dist/src/layouts/force-directed/index.js.map +0 -1
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +0 -15
- package/dist/src/layouts/force-directed/kamada-kawai.js +0 -87
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +0 -1
- package/dist/src/layouts/force-directed/spring.d.ts +0 -19
- package/dist/src/layouts/force-directed/spring.js +0 -23
- package/dist/src/layouts/force-directed/spring.js.map +0 -1
- package/dist/src/layouts/geometric/circular.d.ts +0 -14
- package/dist/src/layouts/geometric/circular.js +0 -67
- package/dist/src/layouts/geometric/circular.js.map +0 -1
- package/dist/src/layouts/geometric/index.d.ts +0 -6
- package/dist/src/layouts/geometric/index.js +0 -7
- package/dist/src/layouts/geometric/index.js.map +0 -1
- package/dist/src/layouts/geometric/shell.d.ts +0 -15
- package/dist/src/layouts/geometric/shell.js +0 -67
- package/dist/src/layouts/geometric/shell.js.map +0 -1
- package/dist/src/layouts/geometric/spiral.d.ts +0 -16
- package/dist/src/layouts/geometric/spiral.js +0 -73
- package/dist/src/layouts/geometric/spiral.js.map +0 -1
- package/dist/src/layouts/hierarchical/bfs.d.ts +0 -12
- package/dist/src/layouts/hierarchical/bfs.js +0 -62
- package/dist/src/layouts/hierarchical/bfs.js.map +0 -1
- package/dist/src/layouts/hierarchical/bipartite.d.ts +0 -16
- package/dist/src/layouts/hierarchical/bipartite.js +0 -72
- package/dist/src/layouts/hierarchical/bipartite.js.map +0 -1
- package/dist/src/layouts/hierarchical/index.d.ts +0 -6
- package/dist/src/layouts/hierarchical/index.js +0 -7
- package/dist/src/layouts/hierarchical/index.js.map +0 -1
- package/dist/src/layouts/hierarchical/multipartite.d.ts +0 -12
- package/dist/src/layouts/hierarchical/multipartite.js +0 -73
- package/dist/src/layouts/hierarchical/multipartite.js.map +0 -1
- package/dist/src/layouts/index.d.ts +0 -8
- package/dist/src/layouts/index.js +0 -9
- package/dist/src/layouts/index.js.map +0 -1
- package/dist/src/layouts/specialized/index.d.ts +0 -5
- package/dist/src/layouts/specialized/index.js +0 -6
- package/dist/src/layouts/specialized/index.js.map +0 -1
- package/dist/src/layouts/specialized/planar.d.ts +0 -14
- package/dist/src/layouts/specialized/planar.js +0 -47
- package/dist/src/layouts/specialized/planar.js.map +0 -1
- package/dist/src/layouts/specialized/spectral.d.ts +0 -14
- package/dist/src/layouts/specialized/spectral.js +0 -106
- package/dist/src/layouts/specialized/spectral.js.map +0 -1
- package/dist/src/types/embedding.d.ts +0 -9
- package/dist/src/types/embedding.js +0 -5
- package/dist/src/types/embedding.js.map +0 -1
- package/dist/src/types/graph.d.ts +0 -10
- package/dist/src/types/graph.js +0 -5
- package/dist/src/types/graph.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -6
- package/dist/src/types/index.js +0 -7
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/types/layout.d.ts +0 -6
- package/dist/src/types/layout.js +0 -5
- package/dist/src/types/layout.js.map +0 -1
- package/dist/src/utils/graph.d.ts +0 -34
- package/dist/src/utils/graph.js +0 -69
- package/dist/src/utils/graph.js.map +0 -1
- package/dist/src/utils/index.d.ts +0 -8
- package/dist/src/utils/index.js +0 -9
- package/dist/src/utils/index.js.map +0 -1
- package/dist/src/utils/numpy.d.ts +0 -17
- package/dist/src/utils/numpy.js +0 -101
- package/dist/src/utils/numpy.js.map +0 -1
- package/dist/src/utils/params.d.ts +0 -17
- package/dist/src/utils/params.js +0 -22
- package/dist/src/utils/params.js.map +0 -1
- package/dist/src/utils/random.d.ts +0 -14
- package/dist/src/utils/random.js +0 -42
- package/dist/src/utils/random.js.map +0 -1
- package/dist/src/utils/rescale.d.ts +0 -21
- package/dist/src/utils/rescale.js +0 -194
- package/dist/src/utils/rescale.js.map +0 -1
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -30
- package/dist/vitest.config.js.map +0 -1
package/dist/layout.js
ADDED
|
@@ -0,0 +1,1762 @@
|
|
|
1
|
+
function rescaleLayout(pos, scale = 1, center = null) {
|
|
2
|
+
if (Array.isArray(pos)) {
|
|
3
|
+
if (pos.length === 0) return [];
|
|
4
|
+
} else {
|
|
5
|
+
if (Object.keys(pos).length === 0) return {};
|
|
6
|
+
}
|
|
7
|
+
const posValues = Array.isArray(pos) ? pos : Object.values(pos);
|
|
8
|
+
const dim = posValues[0].length;
|
|
9
|
+
if (!center) {
|
|
10
|
+
center = Array(dim).fill(0);
|
|
11
|
+
}
|
|
12
|
+
const targetDim = Math.max(dim, center.length);
|
|
13
|
+
const posCenter = Array(dim).fill(0);
|
|
14
|
+
const counts = Array(dim).fill(0);
|
|
15
|
+
for (const p of posValues) {
|
|
16
|
+
for (let i = 0; i < dim; i++) {
|
|
17
|
+
if (!isNaN(p[i])) {
|
|
18
|
+
posCenter[i] += p[i];
|
|
19
|
+
counts[i]++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (let i = 0; i < dim; i++) {
|
|
24
|
+
posCenter[i] = counts[i] > 0 ? posCenter[i] / counts[i] : 0;
|
|
25
|
+
}
|
|
26
|
+
let centeredPos = {};
|
|
27
|
+
if (Array.isArray(pos)) {
|
|
28
|
+
centeredPos = pos.map((p) => {
|
|
29
|
+
const centered = Array(targetDim).fill(0);
|
|
30
|
+
for (let i = 0; i < targetDim; i++) {
|
|
31
|
+
centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
|
|
32
|
+
}
|
|
33
|
+
return centered;
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
for (const [node, p] of Object.entries(pos)) {
|
|
37
|
+
const centered = Array(targetDim).fill(0);
|
|
38
|
+
for (let i = 0; i < targetDim; i++) {
|
|
39
|
+
centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0);
|
|
40
|
+
}
|
|
41
|
+
centeredPos[node] = centered;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let maxDistance = 0;
|
|
45
|
+
const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos);
|
|
46
|
+
for (const p of centeredValues) {
|
|
47
|
+
let sumSquares = 0;
|
|
48
|
+
for (const val of p) {
|
|
49
|
+
if (!isNaN(val)) {
|
|
50
|
+
sumSquares += val * val;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const distance = Math.sqrt(sumSquares);
|
|
54
|
+
if (!isNaN(distance)) {
|
|
55
|
+
maxDistance = Math.max(maxDistance, distance);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let scaledPos = Array.isArray(pos) ? [] : {};
|
|
59
|
+
if (maxDistance > 0) {
|
|
60
|
+
const scaleFactor = scale / maxDistance;
|
|
61
|
+
if (Array.isArray(pos)) {
|
|
62
|
+
scaledPos = centeredPos.map(
|
|
63
|
+
(p, idx) => p.map((val, i) => {
|
|
64
|
+
const origPos = pos[idx];
|
|
65
|
+
if (i >= origPos.length) {
|
|
66
|
+
return center[i];
|
|
67
|
+
}
|
|
68
|
+
if (isNaN(val)) return NaN;
|
|
69
|
+
return val * scaleFactor + center[i];
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
for (const [node, p] of Object.entries(centeredPos)) {
|
|
74
|
+
const origPos = pos[node];
|
|
75
|
+
scaledPos[node] = p.map((val, i) => {
|
|
76
|
+
if (i >= origPos.length) {
|
|
77
|
+
return center[i];
|
|
78
|
+
}
|
|
79
|
+
if (isNaN(val)) return NaN;
|
|
80
|
+
return val * scaleFactor + center[i];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
if (Array.isArray(pos)) {
|
|
86
|
+
scaledPos = pos.map((p) => {
|
|
87
|
+
const result = Array(targetDim);
|
|
88
|
+
for (let i = 0; i < targetDim; i++) {
|
|
89
|
+
if (i < p.length) {
|
|
90
|
+
result[i] = isNaN(p[i]) ? NaN : center[i];
|
|
91
|
+
} else {
|
|
92
|
+
result[i] = center[i];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
for (const [node, p] of Object.entries(pos)) {
|
|
99
|
+
const result = Array(targetDim);
|
|
100
|
+
for (let i = 0; i < targetDim; i++) {
|
|
101
|
+
if (i < p.length) {
|
|
102
|
+
result[i] = isNaN(p[i]) ? NaN : center[i];
|
|
103
|
+
} else {
|
|
104
|
+
result[i] = center[i];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
scaledPos[node] = result;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return scaledPos;
|
|
112
|
+
}
|
|
113
|
+
function rescaleLayoutDict(pos, scale = 1) {
|
|
114
|
+
if (Object.keys(pos).length === 0) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
const posArray = Object.values(pos);
|
|
118
|
+
const center = [];
|
|
119
|
+
for (let d = 0; d < posArray[0].length; d++) {
|
|
120
|
+
center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length;
|
|
121
|
+
}
|
|
122
|
+
const centeredPos = {};
|
|
123
|
+
for (const [node, p] of Object.entries(pos)) {
|
|
124
|
+
centeredPos[node] = p.map((val, d) => val - center[d]);
|
|
125
|
+
}
|
|
126
|
+
let maxDist = 0;
|
|
127
|
+
for (const p of Object.values(centeredPos)) {
|
|
128
|
+
const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0));
|
|
129
|
+
maxDist = Math.max(maxDist, dist);
|
|
130
|
+
}
|
|
131
|
+
const scaledPos = {};
|
|
132
|
+
if (maxDist > 0) {
|
|
133
|
+
for (const [node, p] of Object.entries(centeredPos)) {
|
|
134
|
+
scaledPos[node] = p.map((val) => val * scale / maxDist);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
for (const node of Object.keys(centeredPos)) {
|
|
138
|
+
scaledPos[node] = Array(centeredPos[node].length).fill(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return scaledPos;
|
|
142
|
+
}
|
|
143
|
+
function _processParams(G, center, dim) {
|
|
144
|
+
if (!center) {
|
|
145
|
+
center = Array(dim).fill(0);
|
|
146
|
+
}
|
|
147
|
+
if (center.length !== dim) {
|
|
148
|
+
throw new Error("length of center coordinates must match dimension of layout");
|
|
149
|
+
}
|
|
150
|
+
return { G, center };
|
|
151
|
+
}
|
|
152
|
+
function getNodesFromGraph(G) {
|
|
153
|
+
if (Array.isArray(G)) {
|
|
154
|
+
return G;
|
|
155
|
+
}
|
|
156
|
+
return G.nodes();
|
|
157
|
+
}
|
|
158
|
+
function getEdgesFromGraph(G) {
|
|
159
|
+
if (Array.isArray(G)) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
return G.edges();
|
|
163
|
+
}
|
|
164
|
+
function getNeighbors(graph, node) {
|
|
165
|
+
if (!graph.edges) return [];
|
|
166
|
+
const neighbors = /* @__PURE__ */ new Set();
|
|
167
|
+
const edges = graph.edges();
|
|
168
|
+
for (const [source, target] of edges) {
|
|
169
|
+
if (source === node) {
|
|
170
|
+
neighbors.add(target);
|
|
171
|
+
} else if (target === node) {
|
|
172
|
+
neighbors.add(source);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Array.from(neighbors);
|
|
176
|
+
}
|
|
177
|
+
class RandomNumberGenerator {
|
|
178
|
+
constructor(seed) {
|
|
179
|
+
this.seed = seed || Math.floor(Math.random() * 1e6);
|
|
180
|
+
this.m = 2 ** 35 - 31;
|
|
181
|
+
this.a = 185852;
|
|
182
|
+
this.c = 1;
|
|
183
|
+
this._state = this.seed % this.m;
|
|
184
|
+
}
|
|
185
|
+
_next() {
|
|
186
|
+
this._state = (this.a * this._state + this.c) % this.m;
|
|
187
|
+
return this._state / this.m;
|
|
188
|
+
}
|
|
189
|
+
rand(shape = null) {
|
|
190
|
+
if (shape === null) {
|
|
191
|
+
return this._next();
|
|
192
|
+
}
|
|
193
|
+
if (typeof shape === "number") {
|
|
194
|
+
const result2 = [];
|
|
195
|
+
for (let i = 0; i < shape; i++) {
|
|
196
|
+
result2.push(this._next());
|
|
197
|
+
}
|
|
198
|
+
return result2;
|
|
199
|
+
}
|
|
200
|
+
if (shape.length === 1) {
|
|
201
|
+
const result2 = [];
|
|
202
|
+
for (let i = 0; i < shape[0]; i++) {
|
|
203
|
+
result2.push(this._next());
|
|
204
|
+
}
|
|
205
|
+
return result2;
|
|
206
|
+
}
|
|
207
|
+
const result = [];
|
|
208
|
+
for (let i = 0; i < shape[0]; i++) {
|
|
209
|
+
result.push(this.rand(shape.slice(1)));
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function randomLayout(G, center = null, dim = 2, seed = null) {
|
|
215
|
+
const processed = _processParams(G, center, dim);
|
|
216
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
217
|
+
center = processed.center;
|
|
218
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
219
|
+
const pos = {};
|
|
220
|
+
nodes.forEach((node) => {
|
|
221
|
+
pos[node] = rng.rand(dim).map((val, i) => val + center[i]);
|
|
222
|
+
});
|
|
223
|
+
return pos;
|
|
224
|
+
}
|
|
225
|
+
const np = {
|
|
226
|
+
zeros: function(shape) {
|
|
227
|
+
if (typeof shape === "number") {
|
|
228
|
+
return Array(shape).fill(0);
|
|
229
|
+
}
|
|
230
|
+
if (shape.length === 1) {
|
|
231
|
+
return Array(shape[0]).fill(0);
|
|
232
|
+
}
|
|
233
|
+
return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1)));
|
|
234
|
+
},
|
|
235
|
+
ones: function(shape) {
|
|
236
|
+
if (typeof shape === "number") {
|
|
237
|
+
return Array(shape).fill(1);
|
|
238
|
+
}
|
|
239
|
+
if (shape.length === 1) {
|
|
240
|
+
return Array(shape[0]).fill(1);
|
|
241
|
+
}
|
|
242
|
+
return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1)));
|
|
243
|
+
},
|
|
244
|
+
linspace: function(start, stop, num) {
|
|
245
|
+
if (num === 1) {
|
|
246
|
+
return [start];
|
|
247
|
+
}
|
|
248
|
+
const step = (stop - start) / (num - 1);
|
|
249
|
+
return Array.from({ length: num }, (_, i) => start + i * step);
|
|
250
|
+
},
|
|
251
|
+
array: function(arr) {
|
|
252
|
+
return Array.isArray(arr) ? [...arr] : [arr];
|
|
253
|
+
},
|
|
254
|
+
repeat: function(a, repeats) {
|
|
255
|
+
const result = [];
|
|
256
|
+
for (let i = 0; i < repeats; i++) {
|
|
257
|
+
result.push(...np.array(a));
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
},
|
|
261
|
+
mean: function(arr, axis = null) {
|
|
262
|
+
if (axis === null) {
|
|
263
|
+
const flatArr = Array.isArray(arr[0]) ? arr.flat(Infinity) : arr;
|
|
264
|
+
const sum = flatArr.reduce((a, b) => a + b, 0);
|
|
265
|
+
return sum / flatArr.length;
|
|
266
|
+
}
|
|
267
|
+
if (axis === 0) {
|
|
268
|
+
const result = [];
|
|
269
|
+
const matrix = arr;
|
|
270
|
+
for (let i = 0; i < matrix[0].length; i++) {
|
|
271
|
+
let sum = 0;
|
|
272
|
+
for (let j = 0; j < matrix.length; j++) {
|
|
273
|
+
sum += matrix[j][i];
|
|
274
|
+
}
|
|
275
|
+
result.push(sum / matrix.length);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
return arr.map((row) => np.mean(row));
|
|
280
|
+
},
|
|
281
|
+
add: function(a, b) {
|
|
282
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
283
|
+
return a + b;
|
|
284
|
+
}
|
|
285
|
+
if (!Array.isArray(a)) {
|
|
286
|
+
return b.map((val) => a + val);
|
|
287
|
+
}
|
|
288
|
+
if (!Array.isArray(b)) {
|
|
289
|
+
return a.map((val) => val + b);
|
|
290
|
+
}
|
|
291
|
+
return a.map((val, i) => val + b[i]);
|
|
292
|
+
},
|
|
293
|
+
subtract: function(a, b) {
|
|
294
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
295
|
+
return a - b;
|
|
296
|
+
}
|
|
297
|
+
if (!Array.isArray(a)) {
|
|
298
|
+
return b.map((val) => a - val);
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(b)) {
|
|
301
|
+
return a.map((val) => val - b);
|
|
302
|
+
}
|
|
303
|
+
return a.map((val, i) => val - b[i]);
|
|
304
|
+
},
|
|
305
|
+
max: function(arr) {
|
|
306
|
+
if (!Array.isArray(arr)) return arr;
|
|
307
|
+
return Math.max(...arr.flat(Infinity));
|
|
308
|
+
},
|
|
309
|
+
min: function(arr) {
|
|
310
|
+
if (!Array.isArray(arr)) return arr;
|
|
311
|
+
return Math.min(...arr.flat(Infinity));
|
|
312
|
+
},
|
|
313
|
+
norm: function(arr) {
|
|
314
|
+
return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function circularLayout(G, scale = 1, center = null, dim = 2) {
|
|
318
|
+
if (dim < 2) {
|
|
319
|
+
throw new Error("cannot handle dimensions < 2");
|
|
320
|
+
}
|
|
321
|
+
const processed = _processParams(G, center, dim);
|
|
322
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
323
|
+
center = processed.center;
|
|
324
|
+
const pos = {};
|
|
325
|
+
if (nodes.length === 0) {
|
|
326
|
+
return pos;
|
|
327
|
+
}
|
|
328
|
+
if (nodes.length === 1) {
|
|
329
|
+
pos[nodes[0]] = center;
|
|
330
|
+
return pos;
|
|
331
|
+
}
|
|
332
|
+
if (dim === 2) {
|
|
333
|
+
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
334
|
+
nodes.forEach((node, i) => {
|
|
335
|
+
const x = Math.cos(theta[i]) * scale + center[0];
|
|
336
|
+
const y = Math.sin(theta[i]) * scale + center[1];
|
|
337
|
+
pos[node] = [x, y];
|
|
338
|
+
});
|
|
339
|
+
} else if (dim === 3) {
|
|
340
|
+
const n = nodes.length;
|
|
341
|
+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
342
|
+
nodes.forEach((node, i) => {
|
|
343
|
+
const theta = 2 * Math.PI * i / goldenRatio;
|
|
344
|
+
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
345
|
+
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
346
|
+
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
347
|
+
const z = Math.cos(phi) * scale + center[2];
|
|
348
|
+
pos[node] = [x, y, z];
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
const rng = new RandomNumberGenerator();
|
|
352
|
+
nodes.forEach((node) => {
|
|
353
|
+
const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
354
|
+
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
355
|
+
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return pos;
|
|
359
|
+
}
|
|
360
|
+
function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) {
|
|
361
|
+
if (dim !== 2) {
|
|
362
|
+
throw new Error("can only handle 2 dimensions");
|
|
363
|
+
}
|
|
364
|
+
const processed = _processParams(G, center, dim);
|
|
365
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
366
|
+
center = processed.center;
|
|
367
|
+
const pos = {};
|
|
368
|
+
if (nodes.length === 0) {
|
|
369
|
+
return pos;
|
|
370
|
+
}
|
|
371
|
+
if (nodes.length === 1) {
|
|
372
|
+
pos[nodes[0]] = center;
|
|
373
|
+
return pos;
|
|
374
|
+
}
|
|
375
|
+
if (!nlist) {
|
|
376
|
+
nlist = [nodes];
|
|
377
|
+
}
|
|
378
|
+
const radiusBump = scale / nlist.length;
|
|
379
|
+
let radius;
|
|
380
|
+
if (nlist[0].length === 1) {
|
|
381
|
+
radius = 0;
|
|
382
|
+
pos[nlist[0][0]] = [...center];
|
|
383
|
+
radius += radiusBump;
|
|
384
|
+
} else {
|
|
385
|
+
radius = radiusBump;
|
|
386
|
+
}
|
|
387
|
+
for (let i = 0; i < nlist.length; i++) {
|
|
388
|
+
const shell = nlist[i];
|
|
389
|
+
if (shell.length === 0) continue;
|
|
390
|
+
if (shell.length === 1 && i === 0) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
|
|
394
|
+
shell.forEach((node, j) => {
|
|
395
|
+
const x = Math.cos(theta[j]) * radius + center[0];
|
|
396
|
+
const y = Math.sin(theta[j]) * radius + center[1];
|
|
397
|
+
pos[node] = [x, y];
|
|
398
|
+
});
|
|
399
|
+
radius += radiusBump;
|
|
400
|
+
}
|
|
401
|
+
return pos;
|
|
402
|
+
}
|
|
403
|
+
function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) {
|
|
404
|
+
if (dim !== 2) {
|
|
405
|
+
throw new Error("can only handle 2 dimensions");
|
|
406
|
+
}
|
|
407
|
+
const processed = _processParams(G, center || [0, 0], dim);
|
|
408
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
409
|
+
center = processed.center;
|
|
410
|
+
const pos = {};
|
|
411
|
+
if (nodes.length === 0) {
|
|
412
|
+
return pos;
|
|
413
|
+
}
|
|
414
|
+
if (nodes.length === 1) {
|
|
415
|
+
pos[nodes[0]] = [...center];
|
|
416
|
+
return pos;
|
|
417
|
+
}
|
|
418
|
+
let positions = [];
|
|
419
|
+
if (equidistant) {
|
|
420
|
+
const chord = 1;
|
|
421
|
+
const step = 0.5;
|
|
422
|
+
let theta = resolution;
|
|
423
|
+
theta += chord / (step * theta);
|
|
424
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
425
|
+
const r = step * theta;
|
|
426
|
+
theta += chord / r;
|
|
427
|
+
positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
431
|
+
const angle = dist.map((d) => resolution * d);
|
|
432
|
+
positions = dist.map((d, i) => [
|
|
433
|
+
Math.cos(angle[i]) * d,
|
|
434
|
+
Math.sin(angle[i]) * d
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
const posArray = [];
|
|
438
|
+
for (let i = 0; i < positions.length; i++) {
|
|
439
|
+
posArray.push(positions[i]);
|
|
440
|
+
}
|
|
441
|
+
const scaledPositions = rescaleLayout(posArray, scale);
|
|
442
|
+
for (let i = 0; i < scaledPositions.length; i++) {
|
|
443
|
+
scaledPositions[i][0] += center[0];
|
|
444
|
+
scaledPositions[i][1] += center[1];
|
|
445
|
+
}
|
|
446
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
447
|
+
pos[nodes[i]] = scaledPositions[i];
|
|
448
|
+
}
|
|
449
|
+
return pos;
|
|
450
|
+
}
|
|
451
|
+
function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
452
|
+
const processed = _processParams(G, center, dim);
|
|
453
|
+
let graph = processed.G;
|
|
454
|
+
center = processed.center;
|
|
455
|
+
const nodes = getNodesFromGraph(graph);
|
|
456
|
+
const edges = getEdgesFromGraph(graph);
|
|
457
|
+
if (nodes.length === 0) {
|
|
458
|
+
return {};
|
|
459
|
+
}
|
|
460
|
+
if (nodes.length === 1) {
|
|
461
|
+
const singlePos = {};
|
|
462
|
+
singlePos[nodes[0]] = center;
|
|
463
|
+
return singlePos;
|
|
464
|
+
}
|
|
465
|
+
let positions = {};
|
|
466
|
+
if (pos) {
|
|
467
|
+
for (const node of nodes) {
|
|
468
|
+
if (pos[node]) {
|
|
469
|
+
positions[node] = [...pos[node]];
|
|
470
|
+
} else {
|
|
471
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
472
|
+
positions[node] = rng.rand(dim);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
477
|
+
for (const node of nodes) {
|
|
478
|
+
positions[node] = rng.rand(dim);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const fixedNodes = new Set(fixed || []);
|
|
482
|
+
if (!k) {
|
|
483
|
+
k = 1 / Math.sqrt(nodes.length);
|
|
484
|
+
}
|
|
485
|
+
let t = 0.1;
|
|
486
|
+
const dt = t / (iterations + 1);
|
|
487
|
+
for (let i = 0; i < iterations; i++) {
|
|
488
|
+
const displacement = {};
|
|
489
|
+
for (const node of nodes) {
|
|
490
|
+
displacement[node] = Array(dim).fill(0);
|
|
491
|
+
}
|
|
492
|
+
for (let v1i = 0; v1i < nodes.length; v1i++) {
|
|
493
|
+
const v1 = nodes[v1i];
|
|
494
|
+
for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
|
|
495
|
+
const v2 = nodes[v2i];
|
|
496
|
+
const delta = positions[v1].map((p, i2) => p - positions[v2][i2]);
|
|
497
|
+
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
498
|
+
const force = k * k / distance;
|
|
499
|
+
for (let j = 0; j < dim; j++) {
|
|
500
|
+
const direction = delta[j] / distance;
|
|
501
|
+
displacement[v1][j] += direction * force;
|
|
502
|
+
displacement[v2][j] -= direction * force;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const [source, target] of edges) {
|
|
507
|
+
const delta = positions[source].map((p, i2) => p - positions[target][i2]);
|
|
508
|
+
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
509
|
+
const force = distance * distance / k;
|
|
510
|
+
for (let j = 0; j < dim; j++) {
|
|
511
|
+
const direction = delta[j] / distance;
|
|
512
|
+
displacement[source][j] -= direction * force;
|
|
513
|
+
displacement[target][j] += direction * force;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const node of nodes) {
|
|
517
|
+
if (fixedNodes.has(node)) continue;
|
|
518
|
+
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
|
|
519
|
+
const limitedMagnitude = Math.min(magnitude, t);
|
|
520
|
+
for (let j = 0; j < dim; j++) {
|
|
521
|
+
const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
|
|
522
|
+
positions[node][j] += direction * limitedMagnitude;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
t -= dt;
|
|
526
|
+
}
|
|
527
|
+
if (!fixed) {
|
|
528
|
+
positions = rescaleLayout(positions, scale, center);
|
|
529
|
+
}
|
|
530
|
+
return positions;
|
|
531
|
+
}
|
|
532
|
+
function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
|
|
533
|
+
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
534
|
+
}
|
|
535
|
+
function _lbfgsDirection(grad, sList, yList, m) {
|
|
536
|
+
if (sList.length === 0) {
|
|
537
|
+
return grad.map((g) => -g);
|
|
538
|
+
}
|
|
539
|
+
const q = grad.slice();
|
|
540
|
+
const alpha = Array(sList.length).fill(0);
|
|
541
|
+
const rho = [];
|
|
542
|
+
for (let i = 0; i < sList.length; i++) {
|
|
543
|
+
const s = sList[i];
|
|
544
|
+
const y = yList[i];
|
|
545
|
+
rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
|
|
546
|
+
}
|
|
547
|
+
for (let i = sList.length - 1; i >= 0; i--) {
|
|
548
|
+
const s = sList[i];
|
|
549
|
+
alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0);
|
|
550
|
+
for (let j = 0; j < q.length; j++) {
|
|
551
|
+
q[j] -= alpha[i] * yList[i][j];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
let gamma = 1;
|
|
555
|
+
if (sList.length > 0 && yList.length > 0) {
|
|
556
|
+
const y = yList[yList.length - 1];
|
|
557
|
+
const s = sList[sList.length - 1];
|
|
558
|
+
gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) / y.reduce((sum, val) => sum + val * val, 0);
|
|
559
|
+
}
|
|
560
|
+
const direction = q.map((val) => -gamma * val);
|
|
561
|
+
for (let i = 0; i < sList.length; i++) {
|
|
562
|
+
const s = sList[i];
|
|
563
|
+
const y = yList[i];
|
|
564
|
+
const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0);
|
|
565
|
+
for (let j = 0; j < direction.length; j++) {
|
|
566
|
+
direction[j] += s[j] * (alpha[i] - beta);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return direction;
|
|
570
|
+
}
|
|
571
|
+
function _backtrackingLineSearch(x, direction, f, grad, func, alpha0) {
|
|
572
|
+
const c1 = 1e-4;
|
|
573
|
+
const c2 = 0.9;
|
|
574
|
+
const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
|
|
575
|
+
if (initialSlope >= 0) {
|
|
576
|
+
return 1e-8;
|
|
577
|
+
}
|
|
578
|
+
let alpha = alpha0;
|
|
579
|
+
const maxIter = 20;
|
|
580
|
+
for (let i = 0; i < maxIter; i++) {
|
|
581
|
+
const newX = x.map((val, i2) => val + alpha * direction[i2]);
|
|
582
|
+
const newF = func(newX);
|
|
583
|
+
if (newF <= f + c1 * alpha * initialSlope) {
|
|
584
|
+
return alpha;
|
|
585
|
+
}
|
|
586
|
+
alpha *= c2;
|
|
587
|
+
}
|
|
588
|
+
return alpha;
|
|
589
|
+
}
|
|
590
|
+
function _computeShortestPathDistances(G, weight) {
|
|
591
|
+
const distances = {};
|
|
592
|
+
const nodes = getNodesFromGraph(G);
|
|
593
|
+
const edges = getEdgesFromGraph(G);
|
|
594
|
+
for (const node of nodes) {
|
|
595
|
+
distances[node] = {};
|
|
596
|
+
distances[node][node] = 0;
|
|
597
|
+
for (const other of nodes) {
|
|
598
|
+
if (node !== other) {
|
|
599
|
+
distances[node][other] = Infinity;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const [source, target] of edges) {
|
|
604
|
+
let edgeWeight = 1;
|
|
605
|
+
if (G.getEdgeData) {
|
|
606
|
+
edgeWeight = G.getEdgeData(source, target, weight) || 1;
|
|
607
|
+
}
|
|
608
|
+
distances[source][target] = edgeWeight;
|
|
609
|
+
distances[target][source] = edgeWeight;
|
|
610
|
+
}
|
|
611
|
+
for (const k of nodes) {
|
|
612
|
+
for (const i of nodes) {
|
|
613
|
+
for (const j of nodes) {
|
|
614
|
+
if (distances[i][k] + distances[k][j] < distances[i][j]) {
|
|
615
|
+
distances[i][j] = distances[i][k] + distances[k][j];
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return distances;
|
|
621
|
+
}
|
|
622
|
+
function _kamadaKawaiSolve(distMatrix, positions, dim) {
|
|
623
|
+
const nNodes = positions.length;
|
|
624
|
+
const meanWeight = 1e-3;
|
|
625
|
+
const invDistMatrix = distMatrix.map(
|
|
626
|
+
(row) => row.map((d) => d === 0 ? 0 : 1 / (d + 1e-3))
|
|
627
|
+
);
|
|
628
|
+
let posVec = positions.flat();
|
|
629
|
+
const maxIter = 500;
|
|
630
|
+
const gtol = 1e-5;
|
|
631
|
+
const m = 10;
|
|
632
|
+
let alpha = 1;
|
|
633
|
+
const oldValues = [];
|
|
634
|
+
const oldGrads = [];
|
|
635
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
636
|
+
const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
|
|
637
|
+
const direction = _lbfgsDirection(grad, oldValues, oldGrads);
|
|
638
|
+
alpha = _backtrackingLineSearch(
|
|
639
|
+
posVec,
|
|
640
|
+
direction,
|
|
641
|
+
cost,
|
|
642
|
+
grad,
|
|
643
|
+
(x) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0],
|
|
644
|
+
alpha
|
|
645
|
+
);
|
|
646
|
+
const oldPos = [...posVec];
|
|
647
|
+
for (let i = 0; i < posVec.length; i++) {
|
|
648
|
+
posVec[i] += alpha * direction[i];
|
|
649
|
+
}
|
|
650
|
+
const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim);
|
|
651
|
+
oldValues.push(posVec.map((val, i) => val - oldPos[i]));
|
|
652
|
+
oldGrads.push(newGrad.map((val, i) => val - grad[i]));
|
|
653
|
+
if (oldValues.length > m) {
|
|
654
|
+
oldValues.shift();
|
|
655
|
+
oldGrads.shift();
|
|
656
|
+
}
|
|
657
|
+
const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0));
|
|
658
|
+
if (gradNorm < gtol) {
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const result = [];
|
|
663
|
+
for (let i = 0; i < nNodes; i++) {
|
|
664
|
+
result.push(posVec.slice(i * dim, (i + 1) * dim));
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) {
|
|
669
|
+
const nNodes = invDist.length;
|
|
670
|
+
const positions = [];
|
|
671
|
+
for (let i = 0; i < nNodes; i++) {
|
|
672
|
+
positions.push(posVec.slice(i * dim, (i + 1) * dim));
|
|
673
|
+
}
|
|
674
|
+
let cost = 0;
|
|
675
|
+
const sumPos = Array(dim).fill(0);
|
|
676
|
+
for (let i = 0; i < nNodes; i++) {
|
|
677
|
+
for (let d = 0; d < dim; d++) {
|
|
678
|
+
sumPos[d] += positions[i][d];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0);
|
|
682
|
+
for (let i = 0; i < nNodes; i++) {
|
|
683
|
+
for (let j = i + 1; j < nNodes; j++) {
|
|
684
|
+
const diff = positions[i].map((val, d) => val - positions[j][d]);
|
|
685
|
+
const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0));
|
|
686
|
+
const idealInvDist = invDist[i][j];
|
|
687
|
+
const offset = distance * idealInvDist - 1;
|
|
688
|
+
cost += 0.5 * offset * offset;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const grad = new Array(posVec.length).fill(0);
|
|
692
|
+
for (let i = 0; i < nNodes; i++) {
|
|
693
|
+
for (let d = 0; d < dim; d++) {
|
|
694
|
+
grad[i * dim + d] += meanWeight * sumPos[d];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (let i = 0; i < nNodes; i++) {
|
|
698
|
+
for (let j = i + 1; j < nNodes; j++) {
|
|
699
|
+
const diff = positions[i].map((val, d) => val - positions[j][d]);
|
|
700
|
+
const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10;
|
|
701
|
+
const direction = diff.map((d) => d / distance);
|
|
702
|
+
const idealInvDist = invDist[i][j];
|
|
703
|
+
const offset = distance * idealInvDist - 1;
|
|
704
|
+
for (let d = 0; d < dim; d++) {
|
|
705
|
+
const force = idealInvDist * offset * direction[d];
|
|
706
|
+
grad[i * dim + d] += force;
|
|
707
|
+
grad[j * dim + d] -= force;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return [cost, grad];
|
|
712
|
+
}
|
|
713
|
+
function kamadaKawaiLayout(G, dist = null, pos = null, weight = "weight", scale = 1, center = null, dim = 2) {
|
|
714
|
+
const processed = _processParams(G, center, dim);
|
|
715
|
+
const graph = processed.G;
|
|
716
|
+
center = processed.center;
|
|
717
|
+
const nodes = getNodesFromGraph(graph);
|
|
718
|
+
if (nodes.length === 0) {
|
|
719
|
+
return {};
|
|
720
|
+
}
|
|
721
|
+
if (nodes.length === 1) {
|
|
722
|
+
return { [nodes[0]]: center };
|
|
723
|
+
}
|
|
724
|
+
if (!dist) {
|
|
725
|
+
if (Array.isArray(graph)) {
|
|
726
|
+
throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes");
|
|
727
|
+
}
|
|
728
|
+
dist = _computeShortestPathDistances(graph, weight);
|
|
729
|
+
}
|
|
730
|
+
const nodesArray = Array.from(nodes);
|
|
731
|
+
const nNodes = nodesArray.length;
|
|
732
|
+
const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6));
|
|
733
|
+
for (let i = 0; i < nNodes; i++) {
|
|
734
|
+
const nodeI = nodesArray[i];
|
|
735
|
+
distMatrix[i][i] = 0;
|
|
736
|
+
if (!dist[nodeI]) continue;
|
|
737
|
+
for (let j = 0; j < nNodes; j++) {
|
|
738
|
+
const nodeJ = nodesArray[j];
|
|
739
|
+
if (dist[nodeI][nodeJ] !== void 0) {
|
|
740
|
+
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (!pos) {
|
|
745
|
+
if (dim >= 2) {
|
|
746
|
+
pos = circularLayout(G, 1, center, dim);
|
|
747
|
+
} else {
|
|
748
|
+
const posArray2 = {};
|
|
749
|
+
nodesArray.forEach((node, i) => {
|
|
750
|
+
posArray2[node] = [i / (nNodes - 1 || 1)];
|
|
751
|
+
});
|
|
752
|
+
pos = posArray2;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const posArray = new Array(nNodes);
|
|
756
|
+
for (let i = 0; i < nNodes; i++) {
|
|
757
|
+
const node = nodesArray[i];
|
|
758
|
+
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
759
|
+
while (posArray[i].length < dim) {
|
|
760
|
+
posArray[i].push(0);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
764
|
+
const finalPos = {};
|
|
765
|
+
for (let i = 0; i < nNodes; i++) {
|
|
766
|
+
finalPos[nodesArray[i]] = newPositions[i];
|
|
767
|
+
}
|
|
768
|
+
return rescaleLayout(finalPos, scale, center);
|
|
769
|
+
}
|
|
770
|
+
function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1, scalingRatio = 2, gravity = 1, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) {
|
|
771
|
+
const processed = _processParams(G, null, dim);
|
|
772
|
+
const graph = processed.G;
|
|
773
|
+
const nodes = getNodesFromGraph(graph);
|
|
774
|
+
if (nodes.length === 0) {
|
|
775
|
+
return {};
|
|
776
|
+
}
|
|
777
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
778
|
+
let posArray;
|
|
779
|
+
if (pos === null) {
|
|
780
|
+
pos = {};
|
|
781
|
+
posArray = new Array(nodes.length);
|
|
782
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
783
|
+
posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1);
|
|
784
|
+
pos[nodes[i]] = posArray[i];
|
|
785
|
+
}
|
|
786
|
+
} else if (Object.keys(pos).length === nodes.length) {
|
|
787
|
+
posArray = new Array(nodes.length);
|
|
788
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
789
|
+
const nodePos = pos[nodes[i]];
|
|
790
|
+
posArray[i] = new Array(dim);
|
|
791
|
+
for (let d = 0; d < dim; d++) {
|
|
792
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
797
|
+
let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
798
|
+
for (const node in pos) {
|
|
799
|
+
const nodePos = pos[node];
|
|
800
|
+
for (let d = 0; d < dim; d++) {
|
|
801
|
+
if (d < nodePos.length) {
|
|
802
|
+
minPos[d] = Math.min(minPos[d], nodePos[d]);
|
|
803
|
+
maxPos[d] = Math.max(maxPos[d], nodePos[d]);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (let d = 0; d < dim; d++) {
|
|
808
|
+
if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
|
|
809
|
+
minPos[d] = -1;
|
|
810
|
+
maxPos[d] = 1;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
posArray = new Array(nodes.length);
|
|
814
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
815
|
+
const node = nodes[i];
|
|
816
|
+
if (pos[node]) {
|
|
817
|
+
const nodePos = pos[node];
|
|
818
|
+
posArray[i] = new Array(dim);
|
|
819
|
+
for (let d = 0; d < dim; d++) {
|
|
820
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1;
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
posArray[i] = Array(dim).fill(0).map(
|
|
824
|
+
(_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d])
|
|
825
|
+
);
|
|
826
|
+
pos[node] = posArray[i];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const mass = new Array(nodes.length).fill(0);
|
|
831
|
+
const size = new Array(nodes.length).fill(0);
|
|
832
|
+
const adjustSizes = nodeSize !== null;
|
|
833
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
834
|
+
const node = nodes[i];
|
|
835
|
+
mass[i] = nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1;
|
|
836
|
+
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
837
|
+
}
|
|
838
|
+
const n = nodes.length;
|
|
839
|
+
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
840
|
+
const edges = Array.isArray(graph) ? [] : graph.edges();
|
|
841
|
+
const nodeIndices = {};
|
|
842
|
+
nodes.forEach((node, i) => {
|
|
843
|
+
nodeIndices[node] = i;
|
|
844
|
+
});
|
|
845
|
+
for (const [source, target] of edges) {
|
|
846
|
+
const i = nodeIndices[source];
|
|
847
|
+
const j = nodeIndices[target];
|
|
848
|
+
let edgeWeight = 1;
|
|
849
|
+
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
850
|
+
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
851
|
+
}
|
|
852
|
+
A[i][j] = edgeWeight;
|
|
853
|
+
A[j][i] = edgeWeight;
|
|
854
|
+
}
|
|
855
|
+
const gravities = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
856
|
+
const attraction = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
857
|
+
const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
858
|
+
let speed = 1;
|
|
859
|
+
let speedEfficiency = 1;
|
|
860
|
+
function estimateFactor(n2, swing2, traction2, speed2, speedEfficiency2, jitterTolerance2) {
|
|
861
|
+
const optJitter = 0.05 * Math.sqrt(n2);
|
|
862
|
+
const minJitter = Math.sqrt(optJitter);
|
|
863
|
+
const maxJitter = 10;
|
|
864
|
+
const minSpeedEfficiency = 0.05;
|
|
865
|
+
const other = Math.min(maxJitter, optJitter * traction2 / (n2 * n2));
|
|
866
|
+
let jitter = jitterTolerance2 * Math.max(minJitter, other);
|
|
867
|
+
if (swing2 / traction2 > 2) {
|
|
868
|
+
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
869
|
+
speedEfficiency2 *= 0.5;
|
|
870
|
+
}
|
|
871
|
+
jitter = Math.max(jitter, jitterTolerance2);
|
|
872
|
+
}
|
|
873
|
+
let targetSpeed = swing2 === 0 ? Number.POSITIVE_INFINITY : jitter * speedEfficiency2 * traction2 / swing2;
|
|
874
|
+
if (swing2 > jitter * traction2) {
|
|
875
|
+
if (speedEfficiency2 > minSpeedEfficiency) {
|
|
876
|
+
speedEfficiency2 *= 0.7;
|
|
877
|
+
}
|
|
878
|
+
} else if (speed2 < 1e3) {
|
|
879
|
+
speedEfficiency2 *= 1.3;
|
|
880
|
+
}
|
|
881
|
+
const maxRise = 0.5;
|
|
882
|
+
speed2 = speed2 + Math.min(targetSpeed - speed2, maxRise * speed2);
|
|
883
|
+
return [speed2, speedEfficiency2];
|
|
884
|
+
}
|
|
885
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
886
|
+
for (let i = 0; i < n; i++) {
|
|
887
|
+
for (let d = 0; d < dim; d++) {
|
|
888
|
+
attraction[i][d] = 0;
|
|
889
|
+
repulsion[i][d] = 0;
|
|
890
|
+
gravities[i][d] = 0;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const diff = Array(n).fill(0).map(
|
|
894
|
+
() => Array(n).fill(0).map(() => Array(dim).fill(0))
|
|
895
|
+
);
|
|
896
|
+
const distance = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
897
|
+
for (let i = 0; i < n; i++) {
|
|
898
|
+
for (let j = 0; j < n; j++) {
|
|
899
|
+
if (i === j) continue;
|
|
900
|
+
for (let d = 0; d < dim; d++) {
|
|
901
|
+
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
902
|
+
}
|
|
903
|
+
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
904
|
+
if (distance[i][j] < 0.01) distance[i][j] = 0.01;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (linlog) {
|
|
908
|
+
for (let i = 0; i < n; i++) {
|
|
909
|
+
for (let j = 0; j < n; j++) {
|
|
910
|
+
if (i === j || A[i][j] === 0) continue;
|
|
911
|
+
const dist = distance[i][j];
|
|
912
|
+
const factor = -Math.log(1 + dist) / dist * A[i][j];
|
|
913
|
+
for (let d = 0; d < dim; d++) {
|
|
914
|
+
const force = factor * diff[i][j][d];
|
|
915
|
+
attraction[i][d] += force;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
for (let i = 0; i < n; i++) {
|
|
921
|
+
for (let j = 0; j < n; j++) {
|
|
922
|
+
if (i === j || A[i][j] === 0) continue;
|
|
923
|
+
for (let d = 0; d < dim; d++) {
|
|
924
|
+
const force = -diff[i][j][d] * A[i][j];
|
|
925
|
+
attraction[i][d] += force;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (distributedAction) {
|
|
931
|
+
for (let i = 0; i < n; i++) {
|
|
932
|
+
for (let d = 0; d < dim; d++) {
|
|
933
|
+
attraction[i][d] /= mass[i];
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (let i = 0; i < n; i++) {
|
|
938
|
+
for (let j = 0; j < n; j++) {
|
|
939
|
+
if (i === j) continue;
|
|
940
|
+
let dist = distance[i][j];
|
|
941
|
+
if (adjustSizes) {
|
|
942
|
+
dist -= size[i] - size[j];
|
|
943
|
+
dist = Math.max(dist, 0.01);
|
|
944
|
+
}
|
|
945
|
+
const distSquared = dist * dist;
|
|
946
|
+
const massProduct = mass[i] * mass[j];
|
|
947
|
+
const factor = massProduct / distSquared * scalingRatio;
|
|
948
|
+
for (let d = 0; d < dim; d++) {
|
|
949
|
+
const direction = diff[i][j][d] / dist;
|
|
950
|
+
repulsion[i][d] += direction * factor;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const centerOfMass = Array(dim).fill(0);
|
|
955
|
+
for (let i = 0; i < n; i++) {
|
|
956
|
+
for (let d = 0; d < dim; d++) {
|
|
957
|
+
centerOfMass[d] += posArray[i][d] / n;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
for (let i = 0; i < n; i++) {
|
|
961
|
+
const posCentered = Array(dim);
|
|
962
|
+
for (let d = 0; d < dim; d++) {
|
|
963
|
+
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
964
|
+
}
|
|
965
|
+
if (strongGravity) {
|
|
966
|
+
for (let d = 0; d < dim; d++) {
|
|
967
|
+
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
971
|
+
if (dist > 0.01) {
|
|
972
|
+
for (let d = 0; d < dim; d++) {
|
|
973
|
+
const direction = posCentered[d] / dist;
|
|
974
|
+
gravities[i][d] = -gravity * mass[i] * direction;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const update = Array(n).fill(0).map(() => Array(dim).fill(0));
|
|
980
|
+
let totalSwing = 0;
|
|
981
|
+
let totalTraction = 0;
|
|
982
|
+
for (let i = 0; i < n; i++) {
|
|
983
|
+
for (let d = 0; d < dim; d++) {
|
|
984
|
+
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
985
|
+
}
|
|
986
|
+
const oldPos = [...posArray[i]];
|
|
987
|
+
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
988
|
+
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
989
|
+
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
990
|
+
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
991
|
+
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
992
|
+
totalSwing += mass[i] * swingMagnitude;
|
|
993
|
+
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
994
|
+
}
|
|
995
|
+
[speed, speedEfficiency] = estimateFactor(
|
|
996
|
+
n,
|
|
997
|
+
totalSwing,
|
|
998
|
+
totalTraction,
|
|
999
|
+
speed,
|
|
1000
|
+
speedEfficiency,
|
|
1001
|
+
jitterTolerance
|
|
1002
|
+
);
|
|
1003
|
+
let totalMovement = 0;
|
|
1004
|
+
for (let i = 0; i < n; i++) {
|
|
1005
|
+
let factor;
|
|
1006
|
+
if (adjustSizes) {
|
|
1007
|
+
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1008
|
+
const swinging = mass[i] * df;
|
|
1009
|
+
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
1010
|
+
factor = Math.min(factor * df, 10) / df;
|
|
1011
|
+
} else {
|
|
1012
|
+
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
1013
|
+
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
1014
|
+
}
|
|
1015
|
+
for (let d = 0; d < dim; d++) {
|
|
1016
|
+
const movement = update[i][d] * factor;
|
|
1017
|
+
posArray[i][d] += movement;
|
|
1018
|
+
totalMovement += Math.abs(movement);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (totalMovement < 1e-10) {
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const positions = {};
|
|
1026
|
+
for (let i = 0; i < n; i++) {
|
|
1027
|
+
positions[nodes[i]] = posArray[i];
|
|
1028
|
+
}
|
|
1029
|
+
return rescaleLayout(positions);
|
|
1030
|
+
}
|
|
1031
|
+
function getNodeDegree(graph, node) {
|
|
1032
|
+
return graph.edges().filter(
|
|
1033
|
+
(edge) => edge[0] === node || edge[1] === node
|
|
1034
|
+
).length;
|
|
1035
|
+
}
|
|
1036
|
+
function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1e3, seed = null) {
|
|
1037
|
+
if (a <= 1) {
|
|
1038
|
+
throw new Error("The parameter a should be larger than 1");
|
|
1039
|
+
}
|
|
1040
|
+
const nodes = getNodesFromGraph(G);
|
|
1041
|
+
const edges = getEdgesFromGraph(G);
|
|
1042
|
+
if (nodes.length === 0) {
|
|
1043
|
+
return {};
|
|
1044
|
+
}
|
|
1045
|
+
if (!pos) {
|
|
1046
|
+
pos = randomLayout(G, null, 2, seed);
|
|
1047
|
+
} else {
|
|
1048
|
+
const rng = new RandomNumberGenerator(seed ?? void 0);
|
|
1049
|
+
const defaultPos = {};
|
|
1050
|
+
nodes.forEach((node) => {
|
|
1051
|
+
if (!pos[node]) {
|
|
1052
|
+
defaultPos[node] = [rng.rand(), rng.rand()];
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
pos = { ...pos, ...defaultPos };
|
|
1056
|
+
}
|
|
1057
|
+
const nodeIndex = {};
|
|
1058
|
+
nodes.forEach((node, i) => {
|
|
1059
|
+
nodeIndex[node] = i;
|
|
1060
|
+
});
|
|
1061
|
+
const positions = nodes.map((node) => [...pos[node]]);
|
|
1062
|
+
const N = nodes.length;
|
|
1063
|
+
const K = Array(N).fill(0).map(() => Array(N).fill(1));
|
|
1064
|
+
for (let i = 0; i < N; i++) {
|
|
1065
|
+
K[i][i] = 0;
|
|
1066
|
+
}
|
|
1067
|
+
for (const [source, target] of edges) {
|
|
1068
|
+
if (source === target) continue;
|
|
1069
|
+
const i = nodeIndex[source];
|
|
1070
|
+
const j = nodeIndex[target];
|
|
1071
|
+
K[i][j] = a;
|
|
1072
|
+
K[j][i] = a;
|
|
1073
|
+
}
|
|
1074
|
+
const rho = scaling * Math.sqrt(N);
|
|
1075
|
+
const dt = 1e-3;
|
|
1076
|
+
const etol = 1e-6;
|
|
1077
|
+
let error = etol + 1;
|
|
1078
|
+
let nIter = 0;
|
|
1079
|
+
while (error > etol && nIter < maxIter) {
|
|
1080
|
+
const change = Array(N).fill(0).map(() => [0, 0]);
|
|
1081
|
+
for (let i = 0; i < N; i++) {
|
|
1082
|
+
for (let j = 0; j < N; j++) {
|
|
1083
|
+
if (i === j) continue;
|
|
1084
|
+
const diff = positions[i].map((coord, dim) => coord - positions[j][dim]);
|
|
1085
|
+
const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01;
|
|
1086
|
+
for (let d = 0; d < diff.length; d++) {
|
|
1087
|
+
change[i][d] += K[i][j] * diff[d] - rho / dist * diff[d];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
for (let i = 0; i < N; i++) {
|
|
1092
|
+
for (let d = 0; d < positions[i].length; d++) {
|
|
1093
|
+
positions[i][d] += change[i][d] * dt;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0);
|
|
1097
|
+
nIter++;
|
|
1098
|
+
}
|
|
1099
|
+
const finalPos = {};
|
|
1100
|
+
nodes.forEach((node, i) => {
|
|
1101
|
+
finalPos[node] = positions[i];
|
|
1102
|
+
});
|
|
1103
|
+
return finalPos;
|
|
1104
|
+
}
|
|
1105
|
+
function bipartiteLayout(G, nodes = null, align = "vertical", scale = 1, center = null, aspectRatio = 4 / 3) {
|
|
1106
|
+
if (align !== "vertical" && align !== "horizontal") {
|
|
1107
|
+
throw new Error("align must be either vertical or horizontal");
|
|
1108
|
+
}
|
|
1109
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
1110
|
+
const graph = processed.G;
|
|
1111
|
+
center = processed.center;
|
|
1112
|
+
const allNodes = getNodesFromGraph(graph);
|
|
1113
|
+
if (allNodes.length === 0) {
|
|
1114
|
+
return {};
|
|
1115
|
+
}
|
|
1116
|
+
if (!nodes) {
|
|
1117
|
+
nodes = allNodes.filter((_, i) => i % 2 === 0);
|
|
1118
|
+
}
|
|
1119
|
+
const left = new Set(nodes);
|
|
1120
|
+
const right = new Set(allNodes.filter((n) => !left.has(n)));
|
|
1121
|
+
const height = 1;
|
|
1122
|
+
const width = aspectRatio * height;
|
|
1123
|
+
const offset = [width / 2, height / 2];
|
|
1124
|
+
const pos = {};
|
|
1125
|
+
const leftNodes = [...left];
|
|
1126
|
+
leftNodes.forEach((node, i) => {
|
|
1127
|
+
const x = 0;
|
|
1128
|
+
const y = i * height / (leftNodes.length || 1);
|
|
1129
|
+
pos[node] = [x, y];
|
|
1130
|
+
});
|
|
1131
|
+
const rightNodes = [...right];
|
|
1132
|
+
rightNodes.forEach((node, i) => {
|
|
1133
|
+
const x = width;
|
|
1134
|
+
const y = i * height / (rightNodes.length || 1);
|
|
1135
|
+
pos[node] = [x, y];
|
|
1136
|
+
});
|
|
1137
|
+
for (const node in pos) {
|
|
1138
|
+
pos[node][0] -= offset[0];
|
|
1139
|
+
pos[node][1] -= offset[1];
|
|
1140
|
+
}
|
|
1141
|
+
const scaledPos = rescaleLayout(pos, scale, center);
|
|
1142
|
+
if (align === "horizontal") {
|
|
1143
|
+
for (const node in scaledPos) {
|
|
1144
|
+
const temp = scaledPos[node][0];
|
|
1145
|
+
scaledPos[node][0] = scaledPos[node][1];
|
|
1146
|
+
scaledPos[node][1] = temp;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return scaledPos;
|
|
1150
|
+
}
|
|
1151
|
+
function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale = 1, center = null) {
|
|
1152
|
+
if (align !== "vertical" && align !== "horizontal") {
|
|
1153
|
+
throw new Error("align must be either vertical or horizontal");
|
|
1154
|
+
}
|
|
1155
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
1156
|
+
const graph = processed.G;
|
|
1157
|
+
center = processed.center;
|
|
1158
|
+
const allNodes = getNodesFromGraph(graph);
|
|
1159
|
+
if (allNodes.length === 0) {
|
|
1160
|
+
return {};
|
|
1161
|
+
}
|
|
1162
|
+
let layers = {};
|
|
1163
|
+
if (typeof subsetKey === "string") {
|
|
1164
|
+
console.warn("Using string subsetKey requires node attributes, using default partitioning");
|
|
1165
|
+
layers = { 0: allNodes };
|
|
1166
|
+
} else {
|
|
1167
|
+
for (const [key, value] of Object.entries(subsetKey)) {
|
|
1168
|
+
if (Array.isArray(value)) {
|
|
1169
|
+
layers[key] = value;
|
|
1170
|
+
} else {
|
|
1171
|
+
layers[key] = [value];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const layerCount = Object.keys(layers).length;
|
|
1176
|
+
let pos = {};
|
|
1177
|
+
Object.entries(layers).forEach(([layer, nodes], layerIdx) => {
|
|
1178
|
+
const layerNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
1179
|
+
const layerSize = layerNodes.length;
|
|
1180
|
+
layerNodes.forEach((node, nodeIdx) => {
|
|
1181
|
+
const x = layerIdx - (layerCount - 1) / 2;
|
|
1182
|
+
const y = nodeIdx - (layerSize - 1) / 2;
|
|
1183
|
+
pos[node] = [x, y];
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
pos = rescaleLayout(pos, scale, center);
|
|
1187
|
+
if (align === "horizontal") {
|
|
1188
|
+
for (const node in pos) {
|
|
1189
|
+
const temp = pos[node][0];
|
|
1190
|
+
pos[node][0] = pos[node][1];
|
|
1191
|
+
pos[node][1] = temp;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return pos;
|
|
1195
|
+
}
|
|
1196
|
+
function bfsLayout(G, start, align = "vertical", scale = 1, center = null) {
|
|
1197
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
1198
|
+
if (Array.isArray(processed.G)) {
|
|
1199
|
+
throw new Error("BFS layout requires a Graph with edges, not just a list of nodes");
|
|
1200
|
+
}
|
|
1201
|
+
const graph = processed.G;
|
|
1202
|
+
center = processed.center;
|
|
1203
|
+
const allNodes = getNodesFromGraph(graph);
|
|
1204
|
+
if (allNodes.length === 0) {
|
|
1205
|
+
return {};
|
|
1206
|
+
}
|
|
1207
|
+
const layers = {};
|
|
1208
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1209
|
+
let currentLayer = 0;
|
|
1210
|
+
layers[currentLayer] = [start];
|
|
1211
|
+
visited.add(start);
|
|
1212
|
+
while (Object.values(layers).flat().length < allNodes.length) {
|
|
1213
|
+
const nextLayer = [];
|
|
1214
|
+
const currentNodes = layers[currentLayer];
|
|
1215
|
+
for (const node of currentNodes) {
|
|
1216
|
+
const neighbors = getNeighbors(graph, node);
|
|
1217
|
+
for (const neighbor of neighbors) {
|
|
1218
|
+
if (!visited.has(neighbor)) {
|
|
1219
|
+
nextLayer.push(neighbor);
|
|
1220
|
+
visited.add(neighbor);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (nextLayer.length === 0) {
|
|
1225
|
+
const unvisited = allNodes.filter((node) => !visited.has(node));
|
|
1226
|
+
if (unvisited.length > 0) {
|
|
1227
|
+
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
1228
|
+
}
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
currentLayer++;
|
|
1232
|
+
layers[currentLayer] = nextLayer;
|
|
1233
|
+
}
|
|
1234
|
+
return multipartiteLayout(graph, layers, align, scale, center);
|
|
1235
|
+
}
|
|
1236
|
+
function spectralLayout(G, scale = 1, center = null, dim = 2) {
|
|
1237
|
+
const processed = _processParams(G, center, dim);
|
|
1238
|
+
const graph = processed.G;
|
|
1239
|
+
center = processed.center;
|
|
1240
|
+
const nodes = getNodesFromGraph(graph);
|
|
1241
|
+
if (nodes.length <= 2) {
|
|
1242
|
+
if (nodes.length === 0) {
|
|
1243
|
+
return {};
|
|
1244
|
+
} else if (nodes.length === 1) {
|
|
1245
|
+
return { [nodes[0]]: center };
|
|
1246
|
+
} else {
|
|
1247
|
+
return {
|
|
1248
|
+
[nodes[0]]: center.map((v) => v - scale),
|
|
1249
|
+
[nodes[1]]: center.map((v) => v + scale)
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const N = nodes.length;
|
|
1254
|
+
const nodeIndices = {};
|
|
1255
|
+
nodes.forEach((node, i) => {
|
|
1256
|
+
nodeIndices[node] = i;
|
|
1257
|
+
});
|
|
1258
|
+
const A = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1259
|
+
const edges = getEdgesFromGraph(graph);
|
|
1260
|
+
for (const [source, target] of edges) {
|
|
1261
|
+
const i = nodeIndices[source];
|
|
1262
|
+
const j = nodeIndices[target];
|
|
1263
|
+
A[i][j] = 1;
|
|
1264
|
+
A[j][i] = 1;
|
|
1265
|
+
}
|
|
1266
|
+
const L = Array(N).fill(0).map(() => Array(N).fill(0));
|
|
1267
|
+
for (let i = 0; i < N; i++) {
|
|
1268
|
+
L[i][i] = A[i].reduce((sum, val) => sum + val, 0);
|
|
1269
|
+
for (let j = 0; j < N; j++) {
|
|
1270
|
+
L[i][j] -= A[i][j];
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const eigenvectors = [];
|
|
1274
|
+
for (let d = 0; d < dim; d++) {
|
|
1275
|
+
let vector = Array(N).fill(0).map(() => Math.random() - 0.5);
|
|
1276
|
+
for (const ev of eigenvectors) {
|
|
1277
|
+
const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0);
|
|
1278
|
+
vector = vector.map((val, idx) => val - dot * ev[idx]);
|
|
1279
|
+
}
|
|
1280
|
+
const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0));
|
|
1281
|
+
vector = vector.map((val) => val / norm);
|
|
1282
|
+
for (let iter = 0; iter < 100; iter++) {
|
|
1283
|
+
const newVec = Array(N).fill(0);
|
|
1284
|
+
for (let i = 0; i < N; i++) {
|
|
1285
|
+
for (let j = 0; j < N; j++) {
|
|
1286
|
+
newVec[i] += L[i][j] * vector[j];
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const mean = newVec.reduce((acc, val) => acc + val, 0) / N;
|
|
1290
|
+
newVec.forEach((val, idx, arr) => {
|
|
1291
|
+
arr[idx] = val - mean;
|
|
1292
|
+
});
|
|
1293
|
+
const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0));
|
|
1294
|
+
if (newNorm < 1e-10) continue;
|
|
1295
|
+
vector = newVec.map((val) => val / newNorm);
|
|
1296
|
+
}
|
|
1297
|
+
eigenvectors.push(vector);
|
|
1298
|
+
}
|
|
1299
|
+
const positions = Array(N).fill(0).map(() => Array(dim).fill(0));
|
|
1300
|
+
for (let i = 0; i < N; i++) {
|
|
1301
|
+
for (let d = 0; d < dim; d++) {
|
|
1302
|
+
positions[i][d] = eigenvectors[d][i];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const scaledPositions = rescaleLayout(positions, scale);
|
|
1306
|
+
const pos = {};
|
|
1307
|
+
nodes.forEach((node, i) => {
|
|
1308
|
+
pos[node] = scaledPositions[i].map((val, j) => val + center[j]);
|
|
1309
|
+
});
|
|
1310
|
+
return pos;
|
|
1311
|
+
}
|
|
1312
|
+
function isK5(nodes, edges) {
|
|
1313
|
+
if (nodes.length !== 5) return false;
|
|
1314
|
+
if (edges.length !== 10) return false;
|
|
1315
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1316
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
1317
|
+
const hasEdge = edges.some(
|
|
1318
|
+
(e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i]
|
|
1319
|
+
);
|
|
1320
|
+
if (!hasEdge) return false;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
function isK33(nodes, edges) {
|
|
1326
|
+
if (nodes.length !== 6) return false;
|
|
1327
|
+
if (edges.length !== 9) return false;
|
|
1328
|
+
const nodePartitions = tryFindBipartitePartition(nodes, edges);
|
|
1329
|
+
if (!nodePartitions) return false;
|
|
1330
|
+
const [part1, part2] = nodePartitions;
|
|
1331
|
+
if (part1.length !== 3 || part2.length !== 3) return false;
|
|
1332
|
+
for (const n1 of part1) {
|
|
1333
|
+
for (const n2 of part2) {
|
|
1334
|
+
const hasEdge = edges.some(
|
|
1335
|
+
(e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1
|
|
1336
|
+
);
|
|
1337
|
+
if (!hasEdge) return false;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
function tryFindBipartitePartition(nodes, edges) {
|
|
1343
|
+
const colorMap = {};
|
|
1344
|
+
const adjList = {};
|
|
1345
|
+
for (const node of nodes) {
|
|
1346
|
+
adjList[node] = [];
|
|
1347
|
+
}
|
|
1348
|
+
for (const [u, v] of edges) {
|
|
1349
|
+
adjList[u].push(v);
|
|
1350
|
+
adjList[v].push(u);
|
|
1351
|
+
}
|
|
1352
|
+
const queue = [nodes[0]];
|
|
1353
|
+
colorMap[nodes[0]] = 0;
|
|
1354
|
+
while (queue.length > 0) {
|
|
1355
|
+
const node = queue.shift();
|
|
1356
|
+
const nodeColor = colorMap[node];
|
|
1357
|
+
for (const neighbor of adjList[node]) {
|
|
1358
|
+
if (colorMap[neighbor] === void 0) {
|
|
1359
|
+
colorMap[neighbor] = 1 - nodeColor;
|
|
1360
|
+
queue.push(neighbor);
|
|
1361
|
+
} else if (colorMap[neighbor] === nodeColor) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const part0 = [];
|
|
1367
|
+
const part1 = [];
|
|
1368
|
+
for (const node of nodes) {
|
|
1369
|
+
if (colorMap[node] === 0) {
|
|
1370
|
+
part0.push(node);
|
|
1371
|
+
} else {
|
|
1372
|
+
part1.push(node);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return [part0, part1];
|
|
1376
|
+
}
|
|
1377
|
+
function createTriangulationEmbedding(nodes, edges) {
|
|
1378
|
+
const embedding = {
|
|
1379
|
+
nodeOrder: [...nodes],
|
|
1380
|
+
faceList: [],
|
|
1381
|
+
nodePositions: {}
|
|
1382
|
+
};
|
|
1383
|
+
const adjMap = {};
|
|
1384
|
+
for (const node of nodes) {
|
|
1385
|
+
adjMap[node] = /* @__PURE__ */ new Set();
|
|
1386
|
+
}
|
|
1387
|
+
for (const [u, v] of edges) {
|
|
1388
|
+
adjMap[u].add(v);
|
|
1389
|
+
adjMap[v].add(u);
|
|
1390
|
+
}
|
|
1391
|
+
const outerFace = findCycle(nodes, edges, adjMap) || nodes;
|
|
1392
|
+
embedding.faceList.push(outerFace);
|
|
1393
|
+
const n = outerFace.length;
|
|
1394
|
+
for (let i = 0; i < n; i++) {
|
|
1395
|
+
const angle = 2 * Math.PI * i / n;
|
|
1396
|
+
embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
|
|
1397
|
+
}
|
|
1398
|
+
const interiorNodes = nodes.filter((node) => !embedding.nodePositions[node]);
|
|
1399
|
+
for (const node of interiorNodes) {
|
|
1400
|
+
const neighbors = Array.from(adjMap[node]);
|
|
1401
|
+
if (neighbors.length === 0) {
|
|
1402
|
+
embedding.nodePositions[node] = [0, 0];
|
|
1403
|
+
} else {
|
|
1404
|
+
let xSum = 0, ySum = 0, count = 0;
|
|
1405
|
+
for (const neighbor of neighbors) {
|
|
1406
|
+
if (embedding.nodePositions[neighbor]) {
|
|
1407
|
+
xSum += embedding.nodePositions[neighbor][0];
|
|
1408
|
+
ySum += embedding.nodePositions[neighbor][1];
|
|
1409
|
+
count++;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (count > 0) {
|
|
1413
|
+
const jitter = 0.1 * Math.random();
|
|
1414
|
+
embedding.nodePositions[node] = [
|
|
1415
|
+
xSum / count + jitter * (Math.random() - 0.5),
|
|
1416
|
+
ySum / count + jitter * (Math.random() - 0.5)
|
|
1417
|
+
];
|
|
1418
|
+
} else {
|
|
1419
|
+
const r = 0.5 * Math.random();
|
|
1420
|
+
const angle = 2 * Math.PI * Math.random();
|
|
1421
|
+
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return embedding;
|
|
1426
|
+
}
|
|
1427
|
+
function findCycle(nodes, edges, adjMap) {
|
|
1428
|
+
if (nodes.length === 0) return null;
|
|
1429
|
+
if (nodes.length <= 2) return nodes;
|
|
1430
|
+
if (nodes.length <= 8) {
|
|
1431
|
+
let hamiltonianCycleDFS = function(node) {
|
|
1432
|
+
path.push(node);
|
|
1433
|
+
visited2.add(node);
|
|
1434
|
+
if (path.length === nodes.length) {
|
|
1435
|
+
if (adjMap[node].has(path[0])) {
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
visited2.delete(node);
|
|
1439
|
+
path.pop();
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
for (const neighbor of adjMap[node]) {
|
|
1443
|
+
if (!visited2.has(neighbor)) {
|
|
1444
|
+
if (hamiltonianCycleDFS(neighbor)) {
|
|
1445
|
+
return true;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
visited2.delete(node);
|
|
1450
|
+
path.pop();
|
|
1451
|
+
return false;
|
|
1452
|
+
};
|
|
1453
|
+
const visited2 = /* @__PURE__ */ new Set();
|
|
1454
|
+
const path = [];
|
|
1455
|
+
if (hamiltonianCycleDFS(nodes[0])) {
|
|
1456
|
+
return path;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1460
|
+
const parent = {};
|
|
1461
|
+
let cycleFound = null;
|
|
1462
|
+
function findCycleDFS(node, parentNode) {
|
|
1463
|
+
visited.add(node);
|
|
1464
|
+
for (const neighbor of adjMap[node]) {
|
|
1465
|
+
if (neighbor === parentNode) continue;
|
|
1466
|
+
if (visited.has(neighbor)) {
|
|
1467
|
+
cycleFound = constructCycle(node, neighbor, parent);
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
parent[neighbor] = node;
|
|
1471
|
+
if (findCycleDFS(neighbor, node)) {
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
function constructCycle(u, v, parent2) {
|
|
1478
|
+
const cycle = [v, u];
|
|
1479
|
+
let current = u;
|
|
1480
|
+
while (parent2[current] !== void 0 && parent2[current] !== v) {
|
|
1481
|
+
current = parent2[current];
|
|
1482
|
+
cycle.push(current);
|
|
1483
|
+
}
|
|
1484
|
+
return cycle;
|
|
1485
|
+
}
|
|
1486
|
+
for (const node of nodes) {
|
|
1487
|
+
if (!visited.has(node)) {
|
|
1488
|
+
parent[node] = null;
|
|
1489
|
+
if (findCycleDFS(node, null)) {
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return cycleFound || nodes;
|
|
1495
|
+
}
|
|
1496
|
+
function combinatorialEmbeddingToPos(embedding, nodes) {
|
|
1497
|
+
const pos = {};
|
|
1498
|
+
for (const node of nodes) {
|
|
1499
|
+
if (embedding.nodePositions[node]) {
|
|
1500
|
+
pos[node] = embedding.nodePositions[node];
|
|
1501
|
+
} else {
|
|
1502
|
+
pos[node] = [0, 0];
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return pos;
|
|
1506
|
+
}
|
|
1507
|
+
function lrPlanarityTest(nodes, edges) {
|
|
1508
|
+
const adjList = {};
|
|
1509
|
+
for (const node of nodes) {
|
|
1510
|
+
adjList[node] = [];
|
|
1511
|
+
}
|
|
1512
|
+
for (const [u, v] of edges) {
|
|
1513
|
+
adjList[u].push(v);
|
|
1514
|
+
adjList[v].push(u);
|
|
1515
|
+
}
|
|
1516
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1517
|
+
const ordering = [];
|
|
1518
|
+
function dfs(node) {
|
|
1519
|
+
visited.add(node);
|
|
1520
|
+
ordering.push(node);
|
|
1521
|
+
for (const neighbor of adjList[node]) {
|
|
1522
|
+
if (!visited.has(neighbor)) {
|
|
1523
|
+
dfs(neighbor);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
dfs(nodes[0]);
|
|
1528
|
+
if (ordering.length < nodes.length) {
|
|
1529
|
+
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
1530
|
+
}
|
|
1531
|
+
if (edges.length > 3 * nodes.length - 6) {
|
|
1532
|
+
return { isPlanar: false, embedding: null };
|
|
1533
|
+
}
|
|
1534
|
+
const embedding = createTriangulationEmbedding(nodes, edges);
|
|
1535
|
+
return { isPlanar: true, embedding };
|
|
1536
|
+
}
|
|
1537
|
+
function checkPlanarity(G, nodes, edges) {
|
|
1538
|
+
if (nodes.length <= 4) {
|
|
1539
|
+
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
|
|
1540
|
+
}
|
|
1541
|
+
if (isK5(nodes, edges) || isK33(nodes, edges)) {
|
|
1542
|
+
return { isPlanar: false, embedding: null };
|
|
1543
|
+
}
|
|
1544
|
+
const result = lrPlanarityTest(nodes, edges);
|
|
1545
|
+
return result;
|
|
1546
|
+
}
|
|
1547
|
+
function planarLayout(G, scale = 1, center = null, dim = 2) {
|
|
1548
|
+
if (dim !== 2) {
|
|
1549
|
+
throw new Error("can only handle 2 dimensions");
|
|
1550
|
+
}
|
|
1551
|
+
const processed = _processParams(G, center || [0, 0], dim);
|
|
1552
|
+
if (Array.isArray(processed.G)) {
|
|
1553
|
+
throw new Error("Planar layout requires a Graph with edges, not just a list of nodes");
|
|
1554
|
+
}
|
|
1555
|
+
const graph = processed.G;
|
|
1556
|
+
center = processed.center;
|
|
1557
|
+
const nodes = getNodesFromGraph(graph);
|
|
1558
|
+
const edges = getEdgesFromGraph(graph);
|
|
1559
|
+
if (nodes.length === 0) {
|
|
1560
|
+
return {};
|
|
1561
|
+
}
|
|
1562
|
+
const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges);
|
|
1563
|
+
if (!isPlanar) {
|
|
1564
|
+
throw new Error("G is not planar.");
|
|
1565
|
+
}
|
|
1566
|
+
if (!embedding) {
|
|
1567
|
+
throw new Error("Failed to generate planar embedding.");
|
|
1568
|
+
}
|
|
1569
|
+
let pos = combinatorialEmbeddingToPos(embedding, nodes);
|
|
1570
|
+
pos = rescaleLayout(pos, scale, center);
|
|
1571
|
+
return pos;
|
|
1572
|
+
}
|
|
1573
|
+
function completeGraph(n) {
|
|
1574
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1575
|
+
const edges = [];
|
|
1576
|
+
for (let i = 0; i < n; i++) {
|
|
1577
|
+
for (let j = i + 1; j < n; j++) {
|
|
1578
|
+
edges.push([i, j]);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
nodes: () => nodes,
|
|
1583
|
+
edges: () => edges
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function cycleGraph(n) {
|
|
1587
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1588
|
+
const edges = [];
|
|
1589
|
+
for (let i = 0; i < n; i++) {
|
|
1590
|
+
edges.push([i, (i + 1) % n]);
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
nodes: () => nodes,
|
|
1594
|
+
edges: () => edges
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function starGraph(n) {
|
|
1598
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1599
|
+
const edges = [];
|
|
1600
|
+
for (let i = 1; i < n; i++) {
|
|
1601
|
+
edges.push([0, i]);
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
nodes: () => nodes,
|
|
1605
|
+
edges: () => edges
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function wheelGraph(n) {
|
|
1609
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1610
|
+
const edges = [];
|
|
1611
|
+
for (let i = 1; i < n; i++) {
|
|
1612
|
+
edges.push([0, i]);
|
|
1613
|
+
}
|
|
1614
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1615
|
+
edges.push([i, i + 1]);
|
|
1616
|
+
}
|
|
1617
|
+
if (n > 2) {
|
|
1618
|
+
edges.push([n - 1, 1]);
|
|
1619
|
+
}
|
|
1620
|
+
return {
|
|
1621
|
+
nodes: () => nodes,
|
|
1622
|
+
edges: () => edges
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function gridGraph(rows, cols) {
|
|
1626
|
+
const nodes = [];
|
|
1627
|
+
const edges = [];
|
|
1628
|
+
for (let i = 0; i < rows; i++) {
|
|
1629
|
+
for (let j = 0; j < cols; j++) {
|
|
1630
|
+
nodes.push(`${i},${j}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
for (let i = 0; i < rows; i++) {
|
|
1634
|
+
for (let j = 0; j < cols; j++) {
|
|
1635
|
+
if (j < cols - 1) {
|
|
1636
|
+
edges.push([`${i},${j}`, `${i},${j + 1}`]);
|
|
1637
|
+
}
|
|
1638
|
+
if (i < rows - 1) {
|
|
1639
|
+
edges.push([`${i},${j}`, `${i + 1},${j}`]);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
nodes: () => nodes,
|
|
1645
|
+
edges: () => edges
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function randomGraph(n, p, seed) {
|
|
1649
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1650
|
+
const edges = [];
|
|
1651
|
+
let currentSeed = seed;
|
|
1652
|
+
let random = seed !== void 0 ? () => {
|
|
1653
|
+
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1654
|
+
return currentSeed / 233280;
|
|
1655
|
+
} : Math.random;
|
|
1656
|
+
for (let i = 0; i < n; i++) {
|
|
1657
|
+
for (let j = i + 1; j < n; j++) {
|
|
1658
|
+
if (random() < p) {
|
|
1659
|
+
edges.push([i, j]);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
nodes: () => nodes,
|
|
1665
|
+
edges: () => edges
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function bipartiteGraph(n1, n2, p, seed) {
|
|
1669
|
+
const setA = Array.from({ length: n1 }, (_, i) => `A${i}`);
|
|
1670
|
+
const setB = Array.from({ length: n2 }, (_, i) => `B${i}`);
|
|
1671
|
+
const nodes = [...setA, ...setB];
|
|
1672
|
+
const edges = [];
|
|
1673
|
+
let currentSeed = seed;
|
|
1674
|
+
let random = seed !== void 0 ? () => {
|
|
1675
|
+
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1676
|
+
return currentSeed / 233280;
|
|
1677
|
+
} : Math.random;
|
|
1678
|
+
for (const a of setA) {
|
|
1679
|
+
for (const b of setB) {
|
|
1680
|
+
if (random() < p) {
|
|
1681
|
+
edges.push([a, b]);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
nodes: () => nodes,
|
|
1687
|
+
edges: () => edges,
|
|
1688
|
+
setA,
|
|
1689
|
+
setB
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function scaleFreeGraph(n, m, seed) {
|
|
1693
|
+
if (m >= n) {
|
|
1694
|
+
throw new Error("m must be less than n");
|
|
1695
|
+
}
|
|
1696
|
+
const nodes = Array.from({ length: n }, (_, i) => i);
|
|
1697
|
+
const edges = [];
|
|
1698
|
+
const degrees = new Array(n).fill(0);
|
|
1699
|
+
let currentSeed = seed;
|
|
1700
|
+
let random = seed !== void 0 ? () => {
|
|
1701
|
+
currentSeed = (currentSeed * 9301 + 49297) % 233280;
|
|
1702
|
+
return currentSeed / 233280;
|
|
1703
|
+
} : Math.random;
|
|
1704
|
+
for (let i = 0; i <= m; i++) {
|
|
1705
|
+
for (let j = i + 1; j <= m; j++) {
|
|
1706
|
+
edges.push([i, j]);
|
|
1707
|
+
degrees[i]++;
|
|
1708
|
+
degrees[j]++;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (let i = m + 1; i < n; i++) {
|
|
1712
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1713
|
+
const totalDegree = degrees.reduce((sum, d) => sum + d, 0);
|
|
1714
|
+
while (targets.size < m) {
|
|
1715
|
+
let r = random() * totalDegree;
|
|
1716
|
+
let cumSum = 0;
|
|
1717
|
+
for (let j = 0; j < i; j++) {
|
|
1718
|
+
cumSum += degrees[j];
|
|
1719
|
+
if (r <= cumSum && !targets.has(j)) {
|
|
1720
|
+
targets.add(j);
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
for (const target of targets) {
|
|
1726
|
+
edges.push([i, target]);
|
|
1727
|
+
degrees[i]++;
|
|
1728
|
+
degrees[target]++;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return {
|
|
1732
|
+
nodes: () => nodes,
|
|
1733
|
+
edges: () => edges
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
export {
|
|
1737
|
+
arfLayout,
|
|
1738
|
+
bfsLayout,
|
|
1739
|
+
bipartiteGraph,
|
|
1740
|
+
bipartiteLayout,
|
|
1741
|
+
circularLayout,
|
|
1742
|
+
completeGraph,
|
|
1743
|
+
cycleGraph,
|
|
1744
|
+
forceatlas2Layout,
|
|
1745
|
+
fruchtermanReingoldLayout,
|
|
1746
|
+
gridGraph,
|
|
1747
|
+
kamadaKawaiLayout,
|
|
1748
|
+
multipartiteLayout,
|
|
1749
|
+
planarLayout,
|
|
1750
|
+
randomGraph,
|
|
1751
|
+
randomLayout,
|
|
1752
|
+
rescaleLayout,
|
|
1753
|
+
rescaleLayoutDict,
|
|
1754
|
+
scaleFreeGraph,
|
|
1755
|
+
shellLayout,
|
|
1756
|
+
spectralLayout,
|
|
1757
|
+
spiralLayout,
|
|
1758
|
+
springLayout,
|
|
1759
|
+
starGraph,
|
|
1760
|
+
wheelGraph
|
|
1761
|
+
};
|
|
1762
|
+
//# sourceMappingURL=layout.js.map
|