@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
|
@@ -2,163 +2,162 @@
|
|
|
2
2
|
* Fruchterman-Reingold force-directed layout algorithm
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Graph, Node,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { RandomNumberGenerator } from
|
|
9
|
-
import { rescaleLayout } from
|
|
5
|
+
import { Edge, Graph, Node, PositionMap } from "../../types";
|
|
6
|
+
import { getEdgesFromGraph,getNodesFromGraph } from "../../utils/graph";
|
|
7
|
+
import { _processParams } from "../../utils/params";
|
|
8
|
+
import { RandomNumberGenerator } from "../../utils/random";
|
|
9
|
+
import { rescaleLayout } from "../../utils/rescale";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Position nodes using Fruchterman-Reingold force-directed algorithm.
|
|
13
|
-
*
|
|
14
|
-
* @param
|
|
15
|
-
* @param
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
* @param
|
|
21
|
-
* @param
|
|
22
|
-
* @
|
|
23
|
-
* @returns {Object} Positions dictionary keyed by node
|
|
13
|
+
* @param G - Graph or list of nodes
|
|
14
|
+
* @param k - Optimal distance between nodes
|
|
15
|
+
* @param pos - Initial positions for nodes
|
|
16
|
+
* @param fixed - Nodes to keep fixed at initial position
|
|
17
|
+
* @param iterations - Maximum number of iterations
|
|
18
|
+
* @param scale - Scale factor for positions
|
|
19
|
+
* @param center - Coordinate pair around which to center the layout
|
|
20
|
+
* @param dim - Dimension of layout
|
|
21
|
+
* @param seed - Random seed for initial positions
|
|
22
|
+
* @returns Positions dictionary keyed by node
|
|
24
23
|
*/
|
|
25
24
|
export function fruchtermanReingoldLayout(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
G: Graph,
|
|
26
|
+
k: number | null = null,
|
|
27
|
+
pos: PositionMap | null = null,
|
|
28
|
+
fixed: Node[] | null = null,
|
|
29
|
+
iterations: number = 50,
|
|
30
|
+
scale: number = 1,
|
|
31
|
+
center: number[] | null = null,
|
|
32
|
+
dim: number = 2,
|
|
33
|
+
seed: number | null = null,
|
|
35
34
|
): PositionMap {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Set up initial positions
|
|
54
|
-
let positions: PositionMap = {};
|
|
55
|
-
if (pos) {
|
|
56
|
-
// Use provided positions
|
|
57
|
-
for (const node of nodes) {
|
|
58
|
-
if (pos[node]) {
|
|
59
|
-
positions[node] = [...pos[node]];
|
|
60
|
-
} else {
|
|
61
|
-
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
62
|
-
positions[node] = rng.rand(dim) as number[];
|
|
63
|
-
}
|
|
35
|
+
const processed = _processParams(G, center, dim);
|
|
36
|
+
const graph = processed.G;
|
|
37
|
+
center = processed.center;
|
|
38
|
+
|
|
39
|
+
const nodes = getNodesFromGraph(graph);
|
|
40
|
+
const edges = getEdgesFromGraph(graph);
|
|
41
|
+
|
|
42
|
+
if (nodes.length === 0) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (nodes.length === 1) {
|
|
47
|
+
const singlePos: PositionMap = {};
|
|
48
|
+
singlePos[nodes[0]] = center;
|
|
49
|
+
return singlePos;
|
|
64
50
|
}
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
|
|
52
|
+
// Set up initial positions
|
|
53
|
+
let positions: PositionMap = {};
|
|
54
|
+
if (pos) {
|
|
55
|
+
// Use provided positions
|
|
56
|
+
for (const node of nodes) {
|
|
57
|
+
if (pos[node]) {
|
|
58
|
+
positions[node] = [...pos[node]];
|
|
59
|
+
} else {
|
|
60
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
61
|
+
positions[node] = rng.rand(dim) as number[];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Random initial positions
|
|
66
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
67
|
+
for (const node of nodes) {
|
|
68
|
+
positions[node] = rng.rand(dim) as number[];
|
|
69
|
+
}
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
k = 1.0 / Math.sqrt(nodes.length);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Initialize temperature
|
|
82
|
-
let t = 0.1;
|
|
83
|
-
// Calculate temperature reduction
|
|
84
|
-
const dt = t / (iterations + 1);
|
|
85
|
-
|
|
86
|
-
// Simple cooling schedule
|
|
87
|
-
for (let i = 0; i < iterations; i++) {
|
|
88
|
-
// Calculate repulsive forces
|
|
89
|
-
const displacement: Record<Node, number[]> = {};
|
|
90
|
-
for (const node of nodes) {
|
|
91
|
-
displacement[node] = Array(dim).fill(0);
|
|
71
|
+
|
|
72
|
+
// Set up fixed nodes
|
|
73
|
+
const fixedNodes = new Set(fixed || []);
|
|
74
|
+
|
|
75
|
+
// Optimal distance between nodes
|
|
76
|
+
if (!k) {
|
|
77
|
+
k = 1.0 / Math.sqrt(nodes.length);
|
|
92
78
|
}
|
|
93
79
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
80
|
+
// Initialize temperature
|
|
81
|
+
let t = 0.1;
|
|
82
|
+
// Calculate temperature reduction
|
|
83
|
+
const dt = t / (iterations + 1);
|
|
84
|
+
|
|
85
|
+
// Simple cooling schedule
|
|
86
|
+
for (let i = 0; i < iterations; i++) {
|
|
87
|
+
// Calculate repulsive forces
|
|
88
|
+
const displacement: Record<Node, number[]> = {};
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
displacement[node] = Array(dim).fill(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Repulsive forces between nodes
|
|
94
|
+
for (let v1i = 0; v1i < nodes.length; v1i++) {
|
|
95
|
+
const v1 = nodes[v1i];
|
|
96
|
+
for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
|
|
97
|
+
const v2 = nodes[v2i];
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// Difference vector
|
|
100
|
+
const delta = positions[v1].map((p, i) => p - positions[v2][i]);
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// Distance
|
|
103
|
+
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// Force
|
|
106
|
+
const force = (k * k) / distance;
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
// Add force to displacement
|
|
109
|
+
for (let j = 0; j < dim; j++) {
|
|
110
|
+
const direction = delta[j] / distance;
|
|
111
|
+
displacement[v1][j] += direction * force;
|
|
112
|
+
displacement[v2][j] -= direction * force;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
114
115
|
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
// Attractive forces between connected nodes
|
|
118
|
+
for (const [source, target] of edges) {
|
|
119
|
+
// Difference vector
|
|
120
|
+
const delta = positions[source].map((p, i) => p - positions[target][i]);
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// Distance
|
|
123
|
+
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// Force
|
|
126
|
+
const force = (distance * distance) / k;
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
// Add force to displacement
|
|
129
|
+
for (let j = 0; j < dim; j++) {
|
|
130
|
+
const direction = delta[j] / distance;
|
|
131
|
+
displacement[source][j] -= direction * force;
|
|
132
|
+
displacement[target][j] += direction * force;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
// Update positions
|
|
137
|
+
for (const node of nodes) {
|
|
138
|
+
if (fixedNodes.has(node)) {continue;}
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// Calculate displacement magnitude
|
|
141
|
+
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// Limit maximum displacement by temperature
|
|
144
|
+
const limitedMagnitude = Math.min(magnitude, t);
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
// Update position
|
|
147
|
+
for (let j = 0; j < dim; j++) {
|
|
148
|
+
const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
|
|
149
|
+
positions[node][j] += direction * limitedMagnitude;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
// Cool temperature
|
|
154
|
+
t -= dt;
|
|
155
|
+
}
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
// Rescale positions
|
|
158
|
+
if (!fixed) {
|
|
159
|
+
positions = rescaleLayout(positions, scale, center) as PositionMap;
|
|
160
|
+
}
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
}
|
|
162
|
+
return positions;
|
|
163
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Force-directed layout algorithms
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
5
|
+
export { arfLayout } from "./arf";
|
|
6
|
+
export { forceatlas2Layout } from "./forceatlas2";
|
|
7
|
+
export { fruchtermanReingoldLayout } from "./fruchterman-reingold";
|
|
8
|
+
export { kamadaKawaiLayout } from "./kamada-kawai";
|
|
9
|
+
export { springLayout } from "./spring";
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
_computeShortestPathDistances,
|
|
9
|
-
_kamadaKawaiSolve
|
|
10
|
-
} from '../../algorithms/optimization';
|
|
1
|
+
import type { DistanceMap } from "../../algorithms/optimization";
|
|
2
|
+
import { _computeShortestPathDistances, _kamadaKawaiSolve } from "../../algorithms/optimization";
|
|
3
|
+
import type { Graph, Node, PositionMap } from "../../types";
|
|
4
|
+
import { getNodesFromGraph } from "../../utils/graph";
|
|
5
|
+
import { _processParams } from "../../utils/params";
|
|
6
|
+
import { rescaleLayout } from "../../utils/rescale";
|
|
7
|
+
import { circularLayout } from "../geometric/circular";
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* Position nodes using Kamada-Kawai path-length cost-function.
|
|
14
|
-
*
|
|
15
11
|
* @param G - NetworkX graph or list of nodes
|
|
16
12
|
* @param dist - A two-level dictionary of optimal distances between nodes
|
|
17
13
|
* @param pos - Initial positions for nodes
|
|
@@ -22,91 +18,93 @@ import {
|
|
|
22
18
|
* @returns Positions dictionary keyed by node
|
|
23
19
|
*/
|
|
24
20
|
export function kamadaKawaiLayout(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
G: Graph,
|
|
22
|
+
dist: DistanceMap | null = null,
|
|
23
|
+
pos: PositionMap | null = null,
|
|
24
|
+
weight: string = "weight",
|
|
25
|
+
scale: number = 1,
|
|
26
|
+
center: number[] | null = null,
|
|
27
|
+
dim: number = 2,
|
|
32
28
|
): PositionMap {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
const processed = _processParams(G, center, dim);
|
|
30
|
+
const graph = processed.G;
|
|
31
|
+
center = processed.center;
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
const nodes = getNodesFromGraph(graph);
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
if (nodes.length === 0) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
if (nodes.length === 1) {
|
|
40
|
+
return { [nodes[0]]: center };
|
|
41
|
+
}
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
// Initialize distance matrix
|
|
44
|
+
if (!dist) {
|
|
45
|
+
// Kamada-Kawai requires a proper Graph, not just a list of nodes
|
|
46
|
+
if (Array.isArray(graph)) {
|
|
47
|
+
throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes");
|
|
48
|
+
}
|
|
49
|
+
dist = _computeShortestPathDistances(graph, weight);
|
|
52
50
|
}
|
|
53
|
-
dist = _computeShortestPathDistances(graph, weight);
|
|
54
|
-
}
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
// Convert distances to a matrix
|
|
53
|
+
const nodesArray: Node[] = Array.from(nodes);
|
|
54
|
+
const nNodes = nodesArray.length;
|
|
55
|
+
const distMatrix: number[][] = Array(nNodes)
|
|
56
|
+
.fill(0)
|
|
57
|
+
.map(() => Array(nNodes).fill(1e6));
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
for (let i = 0; i < nNodes; i++) {
|
|
60
|
+
const nodeI = nodesArray[i];
|
|
61
|
+
distMatrix[i][i] = 0;
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
if (!dist[nodeI]) {continue;}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
for (let j = 0; j < nNodes; j++) {
|
|
66
|
+
const nodeJ = nodesArray[j];
|
|
67
|
+
if (dist[nodeI][nodeJ] !== undefined) {
|
|
68
|
+
distMatrix[i][j] = dist[nodeI][nodeJ];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
72
71
|
}
|
|
73
|
-
}
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
// Initialize positions if not provided
|
|
74
|
+
if (!pos) {
|
|
75
|
+
if (dim >= 2) {
|
|
76
|
+
// Use circular/spherical layout for 2D and 3D
|
|
77
|
+
pos = circularLayout(G, 1, center, dim);
|
|
78
|
+
} else {
|
|
79
|
+
// For 1D, use a linear layout
|
|
80
|
+
const posArray: PositionMap = {};
|
|
81
|
+
nodesArray.forEach((node, i) => {
|
|
82
|
+
posArray[node] = [i / (nNodes - 1 || 1)];
|
|
83
|
+
});
|
|
84
|
+
pos = posArray;
|
|
85
|
+
}
|
|
87
86
|
}
|
|
88
|
-
}
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// Convert positions to array for computation
|
|
89
|
+
const posArray: number[][] = new Array(nNodes);
|
|
90
|
+
for (let i = 0; i < nNodes; i++) {
|
|
91
|
+
const node = nodesArray[i];
|
|
92
|
+
posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0);
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// Ensure correct dimensionality
|
|
95
|
+
while (posArray[i].length < dim) {
|
|
96
|
+
posArray[i].push(0);
|
|
97
|
+
}
|
|
99
98
|
}
|
|
100
|
-
}
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
// Run the Kamada-Kawai algorithm
|
|
101
|
+
const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim);
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
// Convert positions array back to dictionary and rescale
|
|
104
|
+
const finalPos: PositionMap = {};
|
|
105
|
+
for (let i = 0; i < nNodes; i++) {
|
|
106
|
+
finalPos[nodesArray[i]] = newPositions[i];
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
}
|
|
109
|
+
return rescaleLayout(finalPos, scale, center) as PositionMap;
|
|
110
|
+
}
|
|
@@ -2,34 +2,33 @@
|
|
|
2
2
|
* Spring layout algorithm (Fruchterman-Reingold variant)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Graph, Node, PositionMap } from
|
|
6
|
-
import { fruchtermanReingoldLayout } from
|
|
5
|
+
import { Graph, Node, PositionMap } from "../../types";
|
|
6
|
+
import { fruchtermanReingoldLayout } from "./fruchterman-reingold";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Position nodes using Fruchterman-Reingold force-directed algorithm.
|
|
10
|
-
*
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
13
|
-
* @param
|
|
14
|
-
* @param
|
|
15
|
-
* @param
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
18
|
-
* @param
|
|
19
|
-
* @
|
|
20
|
-
* @returns {Object} Positions dictionary keyed by node
|
|
10
|
+
* @param G - Graph or list of nodes
|
|
11
|
+
* @param k - Optimal distance between nodes
|
|
12
|
+
* @param pos - Initial positions for nodes
|
|
13
|
+
* @param fixed - Nodes to keep fixed at initial position
|
|
14
|
+
* @param iterations - Maximum number of iterations
|
|
15
|
+
* @param scale - Scale factor for positions
|
|
16
|
+
* @param center - Coordinate pair around which to center the layout
|
|
17
|
+
* @param dim - Dimension of layout
|
|
18
|
+
* @param seed - Random seed for initial positions
|
|
19
|
+
* @returns Positions dictionary keyed by node
|
|
21
20
|
*/
|
|
22
21
|
export function springLayout(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
G: Graph,
|
|
23
|
+
k: number | null = null,
|
|
24
|
+
pos: PositionMap | null = null,
|
|
25
|
+
fixed: Node[] | null = null,
|
|
26
|
+
iterations: number = 50,
|
|
27
|
+
scale: number = 1,
|
|
28
|
+
center: number[] | null = null,
|
|
29
|
+
dim: number = 2,
|
|
30
|
+
seed: number | null = null,
|
|
32
31
|
): PositionMap {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
32
|
+
// Legacy compatibility alias
|
|
33
|
+
return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed);
|
|
34
|
+
}
|