@graphty/layout 1.2.8 → 1.3.2
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 +394 -404
- package/dist/layout.js +881 -821
- package/dist/layout.js.map +1 -1
- package/dist/src/algorithms/index.d.ts +2 -2
- package/dist/src/algorithms/index.js +2 -2
- package/dist/src/algorithms/index.js.map +1 -1
- package/dist/src/algorithms/optimization/index.d.ts +4 -4
- package/dist/src/algorithms/optimization/index.js +3 -3
- package/dist/src/algorithms/optimization/index.js.map +1 -1
- package/dist/src/algorithms/optimization/kamada-kawai-solver.d.ts +2 -5
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js +6 -9
- package/dist/src/algorithms/optimization/kamada-kawai-solver.js.map +1 -1
- package/dist/src/algorithms/optimization/lbfgs.d.ts +0 -1
- package/dist/src/algorithms/optimization/lbfgs.js +3 -5
- package/dist/src/algorithms/optimization/lbfgs.js.map +1 -1
- package/dist/src/algorithms/optimization/line-search.d.ts +0 -1
- package/dist/src/algorithms/optimization/line-search.js +0 -1
- package/dist/src/algorithms/optimization/line-search.js.map +1 -1
- package/dist/src/algorithms/optimization/types.d.ts +1 -1
- package/dist/src/algorithms/planarity/check.d.ts +3 -3
- package/dist/src/algorithms/planarity/check.js +7 -7
- package/dist/src/algorithms/planarity/check.js.map +1 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +3 -5
- package/dist/src/algorithms/planarity/embedding.js +20 -16
- package/dist/src/algorithms/planarity/embedding.js.map +1 -1
- package/dist/src/algorithms/planarity/index.d.ts +4 -4
- package/dist/src/algorithms/planarity/index.js +4 -4
- package/dist/src/algorithms/planarity/index.js.map +1 -1
- package/dist/src/algorithms/planarity/lr-test.d.ts +3 -3
- package/dist/src/algorithms/planarity/lr-test.js +5 -5
- package/dist/src/algorithms/planarity/lr-test.js.map +1 -1
- package/dist/src/algorithms/planarity/special-graphs.d.ts +1 -4
- package/dist/src/algorithms/planarity/special-graphs.js +18 -15
- package/dist/src/algorithms/planarity/special-graphs.js.map +1 -1
- package/dist/src/generators/basic.d.ts +1 -1
- package/dist/src/generators/basic.js +4 -4
- package/dist/src/generators/basic.js.map +1 -1
- package/dist/src/generators/bipartite.d.ts +1 -1
- package/dist/src/generators/bipartite.js +2 -2
- package/dist/src/generators/bipartite.js.map +1 -1
- package/dist/src/generators/grid.d.ts +1 -1
- package/dist/src/generators/grid.js +1 -1
- package/dist/src/generators/grid.js.map +1 -1
- package/dist/src/generators/index.d.ts +5 -5
- package/dist/src/generators/index.js +5 -5
- package/dist/src/generators/index.js.map +1 -1
- package/dist/src/generators/random.d.ts +1 -1
- package/dist/src/generators/random.js +2 -2
- package/dist/src/generators/random.js.map +1 -1
- package/dist/src/generators/scale-free.d.ts +1 -1
- package/dist/src/generators/scale-free.js +4 -4
- package/dist/src/generators/scale-free.js.map +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/layouts/basic/index.d.ts +1 -1
- package/dist/src/layouts/basic/index.js +1 -1
- package/dist/src/layouts/basic/random.d.ts +1 -2
- package/dist/src/layouts/basic/random.js +3 -4
- package/dist/src/layouts/basic/random.js.map +1 -1
- package/dist/src/layouts/force-directed/arf.d.ts +1 -2
- package/dist/src/layouts/force-directed/arf.js +13 -8
- package/dist/src/layouts/force-directed/arf.js.map +1 -1
- package/dist/src/layouts/force-directed/forceatlas2.d.ts +1 -2
- package/dist/src/layouts/force-directed/forceatlas2.js +58 -35
- package/dist/src/layouts/force-directed/forceatlas2.js.map +1 -1
- package/dist/src/layouts/force-directed/fruchterman-reingold.d.ts +11 -12
- package/dist/src/layouts/force-directed/fruchterman-reingold.js +17 -17
- package/dist/src/layouts/force-directed/fruchterman-reingold.js.map +1 -1
- package/dist/src/layouts/force-directed/index.d.ts +5 -5
- package/dist/src/layouts/force-directed/index.js +5 -5
- package/dist/src/layouts/force-directed/index.js.map +1 -1
- package/dist/src/layouts/force-directed/kamada-kawai.d.ts +2 -3
- package/dist/src/layouts/force-directed/kamada-kawai.js +12 -10
- package/dist/src/layouts/force-directed/kamada-kawai.js.map +1 -1
- package/dist/src/layouts/force-directed/spring.d.ts +11 -12
- package/dist/src/layouts/force-directed/spring.js +11 -12
- package/dist/src/layouts/force-directed/spring.js.map +1 -1
- package/dist/src/layouts/geometric/circular.d.ts +1 -2
- package/dist/src/layouts/geometric/circular.js +10 -9
- package/dist/src/layouts/geometric/circular.js.map +1 -1
- package/dist/src/layouts/geometric/index.d.ts +3 -3
- package/dist/src/layouts/geometric/index.js +3 -3
- package/dist/src/layouts/geometric/shell.d.ts +1 -2
- package/dist/src/layouts/geometric/shell.js +5 -5
- package/dist/src/layouts/geometric/shell.js.map +1 -1
- package/dist/src/layouts/geometric/spiral.d.ts +1 -2
- package/dist/src/layouts/geometric/spiral.js +5 -9
- package/dist/src/layouts/geometric/spiral.js.map +1 -1
- package/dist/src/layouts/hierarchical/bfs.d.ts +2 -3
- package/dist/src/layouts/hierarchical/bfs.js +5 -6
- package/dist/src/layouts/hierarchical/bfs.js.map +1 -1
- package/dist/src/layouts/hierarchical/bipartite.d.ts +2 -3
- package/dist/src/layouts/hierarchical/bipartite.js +8 -9
- package/dist/src/layouts/hierarchical/bipartite.js.map +1 -1
- package/dist/src/layouts/hierarchical/index.d.ts +3 -3
- package/dist/src/layouts/hierarchical/index.js +3 -3
- package/dist/src/layouts/hierarchical/index.js.map +1 -1
- package/dist/src/layouts/hierarchical/multipartite.d.ts +2 -3
- package/dist/src/layouts/hierarchical/multipartite.js +7 -8
- package/dist/src/layouts/hierarchical/multipartite.js.map +1 -1
- package/dist/src/layouts/index.d.ts +5 -5
- package/dist/src/layouts/index.js +5 -5
- package/dist/src/layouts/index.js.map +1 -1
- package/dist/src/layouts/specialized/index.d.ts +2 -2
- package/dist/src/layouts/specialized/index.js +2 -2
- package/dist/src/layouts/specialized/index.js.map +1 -1
- package/dist/src/layouts/specialized/planar.d.ts +3 -3
- package/dist/src/layouts/specialized/planar.js +8 -8
- package/dist/src/layouts/specialized/planar.js.map +1 -1
- package/dist/src/layouts/specialized/spectral.d.ts +1 -2
- package/dist/src/layouts/specialized/spectral.js +30 -20
- package/dist/src/layouts/specialized/spectral.js.map +1 -1
- package/dist/src/types/embedding.d.ts +1 -1
- package/dist/src/types/index.d.ts +3 -3
- package/dist/src/types/index.js +3 -3
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/layout.d.ts +1 -1
- package/dist/src/utils/graph.d.ts +1 -5
- package/dist/src/utils/graph.js +4 -6
- package/dist/src/utils/graph.js.map +1 -1
- package/dist/src/utils/index.d.ts +5 -5
- package/dist/src/utils/index.js +5 -5
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/numpy.js +22 -16
- package/dist/src/utils/numpy.js.map +1 -1
- package/dist/src/utils/params.d.ts +1 -2
- package/dist/src/utils/params.js +0 -1
- package/dist/src/utils/params.js.map +1 -1
- package/dist/src/utils/random.d.ts +17 -0
- package/dist/src/utils/random.js +18 -1
- package/dist/src/utils/random.js.map +1 -1
- package/dist/src/utils/rescale.d.ts +1 -3
- package/dist/src/utils/rescale.js +12 -10
- package/dist/src/utils/rescale.js.map +1 -1
- package/dist/vitest.config.js +14 -14
- package/dist/vitest.config.js.map +1 -1
- package/package.json +29 -24
- package/src/algorithms/index.ts +2 -2
- package/src/algorithms/optimization/index.ts +4 -8
- package/src/algorithms/optimization/kamada-kawai-solver.ts +170 -179
- package/src/algorithms/optimization/lbfgs.ts +41 -48
- package/src/algorithms/optimization/line-search.ts +27 -28
- package/src/algorithms/optimization/types.ts +2 -2
- package/src/algorithms/planarity/check.ts +22 -21
- package/src/algorithms/planarity/embedding.ts +159 -165
- package/src/algorithms/planarity/index.ts +4 -8
- package/src/algorithms/planarity/lr-test.ts +46 -45
- package/src/algorithms/planarity/special-graphs.ts +73 -83
- package/src/generators/basic.ts +57 -57
- package/src/generators/bipartite.ts +37 -31
- package/src/generators/grid.ts +28 -28
- package/src/generators/index.ts +5 -11
- package/src/generators/random.ts +26 -25
- package/src/generators/scale-free.ts +56 -55
- package/src/index.ts +5 -5
- package/src/layouts/basic/index.ts +1 -1
- package/src/layouts/basic/random.ts +20 -16
- package/src/layouts/force-directed/arf.ts +107 -105
- package/src/layouts/force-directed/forceatlas2.ts +365 -351
- package/src/layouts/force-directed/fruchterman-reingold.ts +133 -134
- package/src/layouts/force-directed/index.ts +5 -5
- package/src/layouts/force-directed/kamada-kawai.ts +78 -80
- package/src/layouts/force-directed/spring.ts +24 -25
- package/src/layouts/geometric/circular.ts +63 -57
- package/src/layouts/geometric/index.ts +3 -3
- package/src/layouts/geometric/shell.ts +58 -53
- package/src/layouts/geometric/spiral.ts +60 -64
- package/src/layouts/hierarchical/bfs.ts +58 -59
- package/src/layouts/hierarchical/bipartite.ts +62 -63
- package/src/layouts/hierarchical/index.ts +3 -3
- package/src/layouts/hierarchical/multipartite.ts +62 -63
- package/src/layouts/index.ts +5 -5
- package/src/layouts/specialized/index.ts +2 -2
- package/src/layouts/specialized/planar.ts +42 -41
- package/src/layouts/specialized/spectral.ts +110 -99
- package/src/types/embedding.ts +5 -5
- package/src/types/graph.ts +4 -4
- package/src/types/index.ts +3 -3
- package/src/types/layout.ts +2 -2
- package/src/utils/graph.ts +31 -35
- package/src/utils/index.ts +5 -5
- package/src/utils/numpy.ts +96 -92
- package/src/utils/params.ts +14 -11
- package/src/utils/random.ts +57 -40
- package/src/utils/rescale.ts +169 -174
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import type { Graph, Node,
|
|
2
|
-
import { getNodesFromGraph
|
|
3
|
-
import { _processParams } from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import type { Edge, Graph, Node, PositionMap } from "../../types";
|
|
2
|
+
import { getEdgesFromGraph,getNodesFromGraph } from "../../utils/graph";
|
|
3
|
+
import { _processParams } from "../../utils/params";
|
|
4
|
+
import { RandomNumberGenerator } from "../../utils/random";
|
|
5
|
+
import { rescaleLayout } from "../../utils/rescale";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Position nodes using the ForceAtlas2 force-directed algorithm.
|
|
9
|
-
*
|
|
10
9
|
* @param G - Graph
|
|
11
10
|
* @param pos - Initial positions for nodes
|
|
12
11
|
* @param maxIter - Maximum number of iterations
|
|
@@ -25,402 +24,417 @@ import { RandomNumberGenerator } from '../../utils/random';
|
|
|
25
24
|
* @returns Positions dictionary keyed by node
|
|
26
25
|
*/
|
|
27
26
|
export function forceatlas2Layout(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
G: Graph,
|
|
28
|
+
pos: PositionMap | null = null,
|
|
29
|
+
maxIter: number = 100,
|
|
30
|
+
jitterTolerance: number = 1.0,
|
|
31
|
+
scalingRatio: number = 2.0,
|
|
32
|
+
gravity: number = 1.0,
|
|
33
|
+
distributedAction: boolean = false,
|
|
34
|
+
strongGravity: boolean = false,
|
|
35
|
+
nodeMass: Record<Node, number> | null = null,
|
|
36
|
+
nodeSize: Record<Node, number> | null = null,
|
|
37
|
+
weight: string | null = null,
|
|
38
|
+
dissuadeHubs: boolean = false,
|
|
39
|
+
linlog: boolean = false,
|
|
40
|
+
seed: number | null = null,
|
|
41
|
+
dim: number = 2,
|
|
43
42
|
): PositionMap {
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const processed = _processParams(G, null, dim);
|
|
44
|
+
const graph = processed.G;
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
const nodes = getNodesFromGraph(graph);
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Initialize random number generator
|
|
54
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
55
|
-
|
|
56
|
-
// Initialize positions if not provided
|
|
57
|
-
let posArray: number[][];
|
|
58
|
-
if (pos === null) {
|
|
59
|
-
pos = {};
|
|
60
|
-
posArray = new Array(nodes.length);
|
|
61
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
62
|
-
posArray[i] = Array(dim).fill(0).map(() => rng.rand() as number * 2 - 1);
|
|
63
|
-
pos[nodes[i]] = posArray[i];
|
|
64
|
-
}
|
|
65
|
-
} else if (Object.keys(pos).length === nodes.length) {
|
|
66
|
-
// Use provided positions
|
|
67
|
-
posArray = new Array(nodes.length);
|
|
68
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
69
|
-
const nodePos = pos[nodes[i]];
|
|
70
|
-
posArray[i] = new Array(dim);
|
|
71
|
-
for (let d = 0; d < dim; d++) {
|
|
72
|
-
posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number * 2 - 1);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
// Some nodes don't have positions, initialize within the range of existing positions
|
|
77
|
-
let minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
78
|
-
let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
79
|
-
|
|
80
|
-
// Find min and max of existing positions
|
|
81
|
-
for (const node in pos) {
|
|
82
|
-
const nodePos = pos[node];
|
|
83
|
-
for (let d = 0; d < dim; d++) {
|
|
84
|
-
if (d < nodePos.length) {
|
|
85
|
-
minPos[d] = Math.min(minPos[d], nodePos[d]);
|
|
86
|
-
maxPos[d] = Math.max(maxPos[d], nodePos[d]);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
48
|
+
if (nodes.length === 0) {
|
|
49
|
+
return {};
|
|
89
50
|
}
|
|
90
51
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
52
|
+
// Initialize random number generator
|
|
53
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
54
|
+
|
|
55
|
+
// Initialize positions if not provided
|
|
56
|
+
let posArray: number[][];
|
|
57
|
+
if (pos === null) {
|
|
58
|
+
pos = {};
|
|
59
|
+
posArray = new Array(nodes.length);
|
|
60
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
61
|
+
posArray[i] = Array(dim)
|
|
62
|
+
.fill(0)
|
|
63
|
+
.map(() => (rng.rand() as number) * 2 - 1);
|
|
64
|
+
pos[nodes[i]] = posArray[i];
|
|
65
|
+
}
|
|
66
|
+
} else if (Object.keys(pos).length === nodes.length) {
|
|
67
|
+
// Use provided positions
|
|
68
|
+
posArray = new Array(nodes.length);
|
|
69
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
70
|
+
const nodePos = pos[nodes[i]];
|
|
71
|
+
posArray[i] = new Array(dim);
|
|
72
|
+
for (let d = 0; d < dim; d++) {
|
|
73
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number) * 2 - 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Some nodes don't have positions, initialize within the range of existing positions
|
|
78
|
+
const minPos = Array(dim).fill(Number.POSITIVE_INFINITY);
|
|
79
|
+
const maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY);
|
|
80
|
+
|
|
81
|
+
// Find min and max of existing positions
|
|
82
|
+
for (const node in pos) {
|
|
83
|
+
const nodePos = pos[node];
|
|
84
|
+
for (let d = 0; d < dim; d++) {
|
|
85
|
+
if (d < nodePos.length) {
|
|
86
|
+
minPos[d] = Math.min(minPos[d], nodePos[d]);
|
|
87
|
+
maxPos[d] = Math.max(maxPos[d], nodePos[d]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
101
|
-
const node = nodes[i];
|
|
102
|
-
if (pos[node]) {
|
|
103
|
-
const nodePos = pos[node];
|
|
104
|
-
posArray[i] = new Array(dim);
|
|
92
|
+
// If min/max are still infinity for some dimensions, use default range
|
|
105
93
|
for (let d = 0; d < dim; d++) {
|
|
106
|
-
|
|
94
|
+
if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) {
|
|
95
|
+
minPos[d] = -1;
|
|
96
|
+
maxPos[d] = 1;
|
|
97
|
+
}
|
|
107
98
|
}
|
|
108
|
-
} else {
|
|
109
|
-
posArray[i] = Array(dim).fill(0).map((_, d) =>
|
|
110
|
-
minPos[d] + (rng.rand() as number) * (maxPos[d] - minPos[d])
|
|
111
|
-
);
|
|
112
|
-
pos[node] = posArray[i];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Initialize mass and size arrays
|
|
118
|
-
const mass = new Array(nodes.length).fill(0);
|
|
119
|
-
const size = new Array(nodes.length).fill(0);
|
|
120
|
-
|
|
121
|
-
// Flag to track whether to adjust for node sizes
|
|
122
|
-
const adjustSizes = nodeSize !== null;
|
|
123
|
-
|
|
124
|
-
// Set node masses and sizes
|
|
125
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
126
|
-
const node = nodes[i];
|
|
127
|
-
mass[i] = nodeMass && nodeMass[node] ?
|
|
128
|
-
nodeMass[node] :
|
|
129
|
-
(Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1);
|
|
130
|
-
|
|
131
|
-
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Create adjacency matrix
|
|
135
|
-
const n = nodes.length;
|
|
136
|
-
const A = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
137
|
-
|
|
138
|
-
// Populate adjacency matrix with edge weights
|
|
139
|
-
const edges = Array.isArray(graph) ? [] as Edge[] : graph.edges();
|
|
140
|
-
const nodeIndices: Record<Node, number> = {};
|
|
141
|
-
nodes.forEach((node, i) => { nodeIndices[node] = i; });
|
|
142
|
-
|
|
143
|
-
for (const [source, target] of edges) {
|
|
144
|
-
const i = nodeIndices[source];
|
|
145
|
-
const j = nodeIndices[target];
|
|
146
|
-
|
|
147
|
-
// Use edge weight if provided, otherwise default to 1
|
|
148
|
-
let edgeWeight = 1;
|
|
149
|
-
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
150
|
-
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
151
|
-
}
|
|
152
99
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
function estimateFactor(
|
|
170
|
-
n: number,
|
|
171
|
-
swing: number,
|
|
172
|
-
traction: number,
|
|
173
|
-
speed: number,
|
|
174
|
-
speedEfficiency: number,
|
|
175
|
-
jitterTolerance: number
|
|
176
|
-
): [number, number] {
|
|
177
|
-
// Optimal jitter parameters
|
|
178
|
-
const optJitter = 0.05 * Math.sqrt(n);
|
|
179
|
-
const minJitter = Math.sqrt(optJitter);
|
|
180
|
-
const maxJitter = 10;
|
|
181
|
-
const minSpeedEfficiency = 0.05;
|
|
182
|
-
|
|
183
|
-
// Estimate jitter based on current state
|
|
184
|
-
const other = Math.min(maxJitter, optJitter * traction / (n * n));
|
|
185
|
-
let jitter = jitterTolerance * Math.max(minJitter, other);
|
|
186
|
-
|
|
187
|
-
// Adjust speed efficiency based on swing/traction ratio
|
|
188
|
-
if (swing / traction > 2.0) {
|
|
189
|
-
if (speedEfficiency > minSpeedEfficiency) {
|
|
190
|
-
speedEfficiency *= 0.5;
|
|
191
|
-
}
|
|
192
|
-
jitter = Math.max(jitter, jitterTolerance);
|
|
100
|
+
posArray = new Array(nodes.length);
|
|
101
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
102
|
+
const node = nodes[i];
|
|
103
|
+
if (pos[node]) {
|
|
104
|
+
const nodePos = pos[node];
|
|
105
|
+
posArray[i] = new Array(dim);
|
|
106
|
+
for (let d = 0; d < dim; d++) {
|
|
107
|
+
posArray[i][d] = d < nodePos.length ? nodePos[d] : (rng.rand() as number) * 2 - 1;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
posArray[i] = Array(dim)
|
|
111
|
+
.fill(0)
|
|
112
|
+
.map((_, d) => minPos[d] + (rng.rand() as number) * (maxPos[d] - minPos[d]));
|
|
113
|
+
pos[node] = posArray[i];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
193
116
|
}
|
|
194
117
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
jitter * speedEfficiency * traction / swing;
|
|
199
|
-
|
|
200
|
-
// Further adjust speed efficiency
|
|
201
|
-
if (swing > jitter * traction) {
|
|
202
|
-
if (speedEfficiency > minSpeedEfficiency) {
|
|
203
|
-
speedEfficiency *= 0.7;
|
|
204
|
-
}
|
|
205
|
-
} else if (speed < 1000) {
|
|
206
|
-
speedEfficiency *= 1.3;
|
|
207
|
-
}
|
|
118
|
+
// Initialize mass and size arrays
|
|
119
|
+
const mass = new Array(nodes.length).fill(0);
|
|
120
|
+
const size = new Array(nodes.length).fill(0);
|
|
208
121
|
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
|
|
122
|
+
// Flag to track whether to adjust for node sizes
|
|
123
|
+
const adjustSizes = nodeSize !== null;
|
|
212
124
|
|
|
213
|
-
|
|
214
|
-
|
|
125
|
+
// Set node masses and sizes
|
|
126
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
127
|
+
const node = nodes[i];
|
|
128
|
+
mass[i] =
|
|
129
|
+
nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1;
|
|
215
130
|
|
|
216
|
-
|
|
217
|
-
for (let iter = 0; iter < maxIter; iter++) {
|
|
218
|
-
// Reset forces for this iteration
|
|
219
|
-
for (let i = 0; i < n; i++) {
|
|
220
|
-
for (let d = 0; d < dim; d++) {
|
|
221
|
-
attraction[i][d] = 0;
|
|
222
|
-
repulsion[i][d] = 0;
|
|
223
|
-
gravities[i][d] = 0;
|
|
224
|
-
}
|
|
131
|
+
size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1;
|
|
225
132
|
}
|
|
226
133
|
|
|
227
|
-
//
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
134
|
+
// Create adjacency matrix
|
|
135
|
+
const n = nodes.length;
|
|
136
|
+
const A = Array(n)
|
|
137
|
+
.fill(0)
|
|
138
|
+
.map(() => Array(n).fill(0));
|
|
139
|
+
|
|
140
|
+
// Populate adjacency matrix with edge weights
|
|
141
|
+
const edges = Array.isArray(graph) ? ([] as Edge[]) : graph.edges();
|
|
142
|
+
const nodeIndices: Record<Node, number> = {};
|
|
143
|
+
nodes.forEach((node, i) => {
|
|
144
|
+
nodeIndices[node] = i;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for (const [source, target] of edges) {
|
|
148
|
+
const i = nodeIndices[source];
|
|
149
|
+
const j = nodeIndices[target];
|
|
150
|
+
|
|
151
|
+
// Use edge weight if provided, otherwise default to 1
|
|
152
|
+
let edgeWeight = 1;
|
|
153
|
+
if (weight && !Array.isArray(graph) && graph.getEdgeData) {
|
|
154
|
+
edgeWeight = graph.getEdgeData(source, target, weight) || 1;
|
|
240
155
|
}
|
|
241
156
|
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
if (distance[i][j] < 0.01) distance[i][j] = 0.01;
|
|
245
|
-
}
|
|
157
|
+
A[i][j] = edgeWeight;
|
|
158
|
+
A[j][i] = edgeWeight; // For undirected graphs
|
|
246
159
|
}
|
|
247
160
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
161
|
+
// Initialize force arrays
|
|
162
|
+
const gravities = Array(n)
|
|
163
|
+
.fill(0)
|
|
164
|
+
.map(() => Array(dim).fill(0));
|
|
165
|
+
const attraction = Array(n)
|
|
166
|
+
.fill(0)
|
|
167
|
+
.map(() => Array(dim).fill(0));
|
|
168
|
+
const repulsion = Array(n)
|
|
169
|
+
.fill(0)
|
|
170
|
+
.map(() => Array(dim).fill(0));
|
|
171
|
+
|
|
172
|
+
// Simulation parameters
|
|
173
|
+
let speed = 1;
|
|
174
|
+
let speedEfficiency = 1;
|
|
175
|
+
const swing = 1;
|
|
176
|
+
const traction = 1;
|
|
177
|
+
|
|
178
|
+
// Helper function to estimate factor for force scaling
|
|
179
|
+
function estimateFactor(
|
|
180
|
+
n: number,
|
|
181
|
+
swing: number,
|
|
182
|
+
traction: number,
|
|
183
|
+
speed: number,
|
|
184
|
+
speedEfficiency: number,
|
|
185
|
+
jitterTolerance: number,
|
|
186
|
+
): [number, number] {
|
|
187
|
+
// Optimal jitter parameters
|
|
188
|
+
const optJitter = 0.05 * Math.sqrt(n);
|
|
189
|
+
const minJitter = Math.sqrt(optJitter);
|
|
190
|
+
const maxJitter = 10;
|
|
191
|
+
const minSpeedEfficiency = 0.05;
|
|
192
|
+
|
|
193
|
+
// Estimate jitter based on current state
|
|
194
|
+
const other = Math.min(maxJitter, (optJitter * traction) / (n * n));
|
|
195
|
+
let jitter = jitterTolerance * Math.max(minJitter, other);
|
|
196
|
+
|
|
197
|
+
// Adjust speed efficiency based on swing/traction ratio
|
|
198
|
+
if (swing / traction > 2.0) {
|
|
199
|
+
if (speedEfficiency > minSpeedEfficiency) {
|
|
200
|
+
speedEfficiency *= 0.5;
|
|
201
|
+
}
|
|
202
|
+
jitter = Math.max(jitter, jitterTolerance);
|
|
203
|
+
}
|
|
254
204
|
|
|
255
|
-
|
|
256
|
-
|
|
205
|
+
// Calculate target speed
|
|
206
|
+
const targetSpeed = swing === 0 ? Number.POSITIVE_INFINITY : (jitter * speedEfficiency * traction) / swing;
|
|
257
207
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Linear attraction model
|
|
266
|
-
for (let i = 0; i < n; i++) {
|
|
267
|
-
for (let j = 0; j < n; j++) {
|
|
268
|
-
if (i === j || A[i][j] === 0) continue;
|
|
269
|
-
|
|
270
|
-
for (let d = 0; d < dim; d++) {
|
|
271
|
-
const force = -diff[i][j][d] * A[i][j];
|
|
272
|
-
attraction[i][d] += force;
|
|
273
|
-
}
|
|
208
|
+
// Further adjust speed efficiency
|
|
209
|
+
if (swing > jitter * traction) {
|
|
210
|
+
if (speedEfficiency > minSpeedEfficiency) {
|
|
211
|
+
speedEfficiency *= 0.7;
|
|
212
|
+
}
|
|
213
|
+
} else if (speed < 1000) {
|
|
214
|
+
speedEfficiency *= 1.3;
|
|
274
215
|
}
|
|
275
|
-
|
|
216
|
+
|
|
217
|
+
// Limit the speed increase
|
|
218
|
+
const maxRise = 0.5;
|
|
219
|
+
speed = speed + Math.min(targetSpeed - speed, maxRise * speed);
|
|
220
|
+
|
|
221
|
+
return [speed, speedEfficiency];
|
|
276
222
|
}
|
|
277
223
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
for (let
|
|
282
|
-
|
|
224
|
+
// Main simulation loop
|
|
225
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
226
|
+
// Reset forces for this iteration
|
|
227
|
+
for (let i = 0; i < n; i++) {
|
|
228
|
+
for (let d = 0; d < dim; d++) {
|
|
229
|
+
attraction[i][d] = 0;
|
|
230
|
+
repulsion[i][d] = 0;
|
|
231
|
+
gravities[i][d] = 0;
|
|
232
|
+
}
|
|
283
233
|
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
234
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
235
|
+
// Compute pairwise differences and distances
|
|
236
|
+
const diff = Array(n)
|
|
237
|
+
.fill(0)
|
|
238
|
+
.map(() =>
|
|
239
|
+
Array(n)
|
|
240
|
+
.fill(0)
|
|
241
|
+
.map(() => Array(dim).fill(0)),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const distance = Array(n)
|
|
245
|
+
.fill(0)
|
|
246
|
+
.map(() => Array(n).fill(0));
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < n; i++) {
|
|
249
|
+
for (let j = 0; j < n; j++) {
|
|
250
|
+
if (i === j) {continue;}
|
|
251
|
+
|
|
252
|
+
for (let d = 0; d < dim; d++) {
|
|
253
|
+
diff[i][j][d] = posArray[i][d] - posArray[j][d];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0));
|
|
257
|
+
// Prevent division by zero
|
|
258
|
+
if (distance[i][j] < 0.01) {distance[i][j] = 0.01;}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
291
261
|
|
|
292
|
-
|
|
262
|
+
// Calculate attraction forces
|
|
263
|
+
if (linlog) {
|
|
264
|
+
// Logarithmic attraction model
|
|
265
|
+
for (let i = 0; i < n; i++) {
|
|
266
|
+
for (let j = 0; j < n; j++) {
|
|
267
|
+
if (i === j || A[i][j] === 0) {continue;}
|
|
268
|
+
|
|
269
|
+
const dist = distance[i][j];
|
|
270
|
+
const factor = (-Math.log(1 + dist) / dist) * A[i][j];
|
|
271
|
+
|
|
272
|
+
for (let d = 0; d < dim; d++) {
|
|
273
|
+
const force = factor * diff[i][j][d];
|
|
274
|
+
attraction[i][d] += force;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// Linear attraction model
|
|
280
|
+
for (let i = 0; i < n; i++) {
|
|
281
|
+
for (let j = 0; j < n; j++) {
|
|
282
|
+
if (i === j || A[i][j] === 0) {continue;}
|
|
283
|
+
|
|
284
|
+
for (let d = 0; d < dim; d++) {
|
|
285
|
+
const force = -diff[i][j][d] * A[i][j];
|
|
286
|
+
attraction[i][d] += force;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
293
291
|
|
|
294
|
-
//
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
292
|
+
// Apply distributed attraction if enabled
|
|
293
|
+
if (distributedAction) {
|
|
294
|
+
for (let i = 0; i < n; i++) {
|
|
295
|
+
for (let d = 0; d < dim; d++) {
|
|
296
|
+
attraction[i][d] /= mass[i];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
// Calculate repulsion forces
|
|
302
|
+
for (let i = 0; i < n; i++) {
|
|
303
|
+
for (let j = 0; j < n; j++) {
|
|
304
|
+
if (i === j) {continue;}
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
const direction = diff[i][j][d] / dist;
|
|
306
|
-
repulsion[i][d] += direction * factor;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
306
|
+
let dist = distance[i][j];
|
|
310
307
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
centerOfMass[d] += posArray[i][d] / n;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
308
|
+
// Adjust distance for node sizes if needed
|
|
309
|
+
if (adjustSizes) {
|
|
310
|
+
dist -= size[i] - size[j];
|
|
311
|
+
dist = Math.max(dist, 0.01); // Prevent negative or zero distances
|
|
312
|
+
}
|
|
319
313
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
324
|
-
}
|
|
314
|
+
const distSquared = dist * dist;
|
|
315
|
+
const massProduct = mass[i] * mass[j];
|
|
316
|
+
const factor = (massProduct / distSquared) * scalingRatio;
|
|
325
317
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
318
|
+
for (let d = 0; d < dim; d++) {
|
|
319
|
+
const direction = diff[i][j][d] / dist;
|
|
320
|
+
repulsion[i][d] += direction * factor;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
330
323
|
}
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
324
|
+
|
|
325
|
+
// Calculate gravity forces
|
|
326
|
+
// First find the center of mass
|
|
327
|
+
const centerOfMass = Array(dim).fill(0);
|
|
328
|
+
for (let i = 0; i < n; i++) {
|
|
329
|
+
for (let d = 0; d < dim; d++) {
|
|
330
|
+
centerOfMass[d] += posArray[i][d] / n;
|
|
331
|
+
}
|
|
340
332
|
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
333
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
334
|
+
for (let i = 0; i < n; i++) {
|
|
335
|
+
const posCentered = Array(dim);
|
|
336
|
+
for (let d = 0; d < dim; d++) {
|
|
337
|
+
posCentered[d] = posArray[i][d] - centerOfMass[d];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (strongGravity) {
|
|
341
|
+
// Strong gravity model
|
|
342
|
+
for (let d = 0; d < dim; d++) {
|
|
343
|
+
gravities[i][d] = -gravity * mass[i] * posCentered[d];
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
// Regular gravity model
|
|
347
|
+
const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0));
|
|
348
|
+
|
|
349
|
+
if (dist > 0.01) {
|
|
350
|
+
for (let d = 0; d < dim; d++) {
|
|
351
|
+
const direction = posCentered[d] / dist;
|
|
352
|
+
gravities[i][d] = -gravity * mass[i] * direction;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
348
357
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
// Calculate total forces and update positions
|
|
359
|
+
const update = Array(n)
|
|
360
|
+
.fill(0)
|
|
361
|
+
.map(() => Array(dim).fill(0));
|
|
362
|
+
let totalSwing = 0;
|
|
363
|
+
let totalTraction = 0;
|
|
353
364
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
365
|
+
for (let i = 0; i < n; i++) {
|
|
366
|
+
for (let d = 0; d < dim; d++) {
|
|
367
|
+
update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d];
|
|
368
|
+
}
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
|
|
370
|
+
// Calculate swing and traction for this node
|
|
371
|
+
const oldPos = [...posArray[i]];
|
|
372
|
+
const newPos = oldPos.map((p, d) => p + update[i][d]);
|
|
360
373
|
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
const swingVector = oldPos.map((p, d) => p - newPos[d]);
|
|
375
|
+
const tractionVector = oldPos.map((p, d) => p + newPos[d]);
|
|
363
376
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
377
|
+
const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0));
|
|
378
|
+
const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0));
|
|
379
|
+
|
|
380
|
+
totalSwing += mass[i] * swingMagnitude;
|
|
381
|
+
totalTraction += 0.5 * mass[i] * tractionMagnitude;
|
|
382
|
+
}
|
|
367
383
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
384
|
+
// Update speed and efficiency
|
|
385
|
+
[speed, speedEfficiency] = estimateFactor(
|
|
386
|
+
n,
|
|
387
|
+
totalSwing,
|
|
388
|
+
totalTraction,
|
|
389
|
+
speed,
|
|
390
|
+
speedEfficiency,
|
|
391
|
+
jitterTolerance,
|
|
392
|
+
);
|
|
377
393
|
|
|
378
|
-
|
|
379
|
-
|
|
394
|
+
// Apply forces to update positions
|
|
395
|
+
let totalMovement = 0;
|
|
396
|
+
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
let factor;
|
|
399
|
+
|
|
400
|
+
if (adjustSizes) {
|
|
401
|
+
// Calculate displacement magnitude
|
|
402
|
+
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
403
|
+
const swinging = mass[i] * df;
|
|
404
|
+
|
|
405
|
+
// Determine scaling factor with size adjustments
|
|
406
|
+
factor = (0.1 * speed) / (1 + Math.sqrt(speed * swinging));
|
|
407
|
+
factor = Math.min(factor * df, 10) / df;
|
|
408
|
+
} else {
|
|
409
|
+
// Standard scaling factor
|
|
410
|
+
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
411
|
+
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Apply factor to update position
|
|
415
|
+
for (let d = 0; d < dim; d++) {
|
|
416
|
+
const movement = update[i][d] * factor;
|
|
417
|
+
posArray[i][d] += movement;
|
|
418
|
+
totalMovement += Math.abs(movement);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
380
421
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// Calculate displacement magnitude
|
|
386
|
-
const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
387
|
-
const swinging = mass[i] * df;
|
|
388
|
-
|
|
389
|
-
// Determine scaling factor with size adjustments
|
|
390
|
-
factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging));
|
|
391
|
-
factor = Math.min(factor * df, 10) / df;
|
|
392
|
-
} else {
|
|
393
|
-
// Standard scaling factor
|
|
394
|
-
const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0));
|
|
395
|
-
factor = speed / (1 + Math.sqrt(speed * swinging));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Apply factor to update position
|
|
399
|
-
for (let d = 0; d < dim; d++) {
|
|
400
|
-
const movement = update[i][d] * factor;
|
|
401
|
-
posArray[i][d] += movement;
|
|
402
|
-
totalMovement += Math.abs(movement);
|
|
403
|
-
}
|
|
422
|
+
// Check for convergence
|
|
423
|
+
if (totalMovement < 1e-10) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
404
426
|
}
|
|
405
427
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
428
|
+
// Create position dictionary
|
|
429
|
+
const positions: PositionMap = {};
|
|
430
|
+
for (let i = 0; i < n; i++) {
|
|
431
|
+
positions[nodes[i]] = posArray[i];
|
|
409
432
|
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Create position dictionary
|
|
413
|
-
const positions: PositionMap = {};
|
|
414
|
-
for (let i = 0; i < n; i++) {
|
|
415
|
-
positions[nodes[i]] = posArray[i];
|
|
416
|
-
}
|
|
417
433
|
|
|
418
|
-
|
|
434
|
+
return rescaleLayout(positions) as PositionMap;
|
|
419
435
|
}
|
|
420
436
|
|
|
421
437
|
// Helper function to get node degree
|
|
422
438
|
function getNodeDegree(graph: Graph, node: Node): number {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
).length;
|
|
426
|
-
}
|
|
439
|
+
return graph.edges().filter((edge: Edge) => edge[0] === node || edge[1] === node).length;
|
|
440
|
+
}
|