@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,76 +2,82 @@
|
|
|
2
2
|
* Circular layout algorithm
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Graph, Node, PositionMap } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { Graph, Node, PositionMap } from "../../types";
|
|
6
|
+
import { getNodesFromGraph } from "../../utils/graph";
|
|
7
|
+
import { np } from "../../utils/numpy";
|
|
8
|
+
import { _processParams } from "../../utils/params";
|
|
9
|
+
import { RandomNumberGenerator } from "../../utils/random";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Position nodes on a circle (2D) or sphere (3D).
|
|
13
|
-
*
|
|
14
13
|
* @param G - Graph or list of nodes
|
|
15
14
|
* @param scale - Scale factor for positions
|
|
16
15
|
* @param center - Coordinate pair around which to center the layout
|
|
17
16
|
* @param dim - Dimension of layout (supports 2D circle or 3D sphere)
|
|
18
17
|
* @returns Positions dictionary keyed by node
|
|
19
18
|
*/
|
|
20
|
-
export function circularLayout(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
export function circularLayout(
|
|
20
|
+
G: Graph,
|
|
21
|
+
scale: number = 1,
|
|
22
|
+
center: number[] | null = null,
|
|
23
|
+
dim: number = 2,
|
|
24
|
+
): PositionMap {
|
|
25
|
+
if (dim < 2) {
|
|
26
|
+
throw new Error("cannot handle dimensions < 2");
|
|
27
|
+
}
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const processed = _processParams(G, center, dim);
|
|
30
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
31
|
+
center = processed.center;
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
const pos: PositionMap = {};
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
if (nodes.length === 0) {
|
|
36
|
+
return pos;
|
|
37
|
+
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if (nodes.length === 1) {
|
|
40
|
+
pos[nodes[0]] = center;
|
|
41
|
+
return pos;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (dim === 2) {
|
|
45
|
+
// 2D circle layout
|
|
46
|
+
const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1);
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
nodes.forEach((node: Node, i: number) => {
|
|
49
|
+
const x: number = Math.cos(theta[i]) * scale + center[0];
|
|
50
|
+
const y: number = Math.sin(theta[i]) * scale + center[1];
|
|
51
|
+
pos[node] = [x, y];
|
|
52
|
+
});
|
|
53
|
+
} else if (dim === 3) {
|
|
54
|
+
// 3D sphere layout using Fibonacci spiral
|
|
55
|
+
const n = nodes.length;
|
|
56
|
+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
} else if (dim === 3) {
|
|
50
|
-
// 3D sphere layout using Fibonacci spiral
|
|
51
|
-
const n = nodes.length;
|
|
52
|
-
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
|
53
|
-
|
|
54
|
-
nodes.forEach((node: Node, i: number) => {
|
|
55
|
-
// Use Fibonacci spiral for even distribution on sphere
|
|
56
|
-
const theta = 2 * Math.PI * i / goldenRatio;
|
|
57
|
-
const phi = Math.acos(1 - 2 * (i + 0.5) / n);
|
|
58
|
-
|
|
59
|
-
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
60
|
-
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
61
|
-
const z = Math.cos(phi) * scale + center[2];
|
|
62
|
-
|
|
63
|
-
pos[node] = [x, y, z];
|
|
64
|
-
});
|
|
65
|
-
} else {
|
|
66
|
-
// For higher dimensions, fall back to random on hypersphere
|
|
67
|
-
const rng = new RandomNumberGenerator();
|
|
68
|
-
nodes.forEach((node: Node) => {
|
|
69
|
-
// Generate random point on unit hypersphere
|
|
70
|
-
const coords = Array(dim).fill(0).map(() => rng.rand() as number * 2 - 1);
|
|
71
|
-
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
72
|
-
pos[node] = coords.map((c, j) => c / norm * scale + center[j]);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
58
|
+
nodes.forEach((node: Node, i: number) => {
|
|
59
|
+
// Use Fibonacci spiral for even distribution on sphere
|
|
60
|
+
const theta = (2 * Math.PI * i) / goldenRatio;
|
|
61
|
+
const phi = Math.acos(1 - (2 * (i + 0.5)) / n);
|
|
75
62
|
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
const x = Math.sin(phi) * Math.cos(theta) * scale + center[0];
|
|
64
|
+
const y = Math.sin(phi) * Math.sin(theta) * scale + center[1];
|
|
65
|
+
const z = Math.cos(phi) * scale + center[2];
|
|
66
|
+
|
|
67
|
+
pos[node] = [x, y, z];
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
// For higher dimensions, fall back to random on hypersphere
|
|
71
|
+
const rng = new RandomNumberGenerator();
|
|
72
|
+
nodes.forEach((node: Node) => {
|
|
73
|
+
// Generate random point on unit hypersphere
|
|
74
|
+
const coords = Array(dim)
|
|
75
|
+
.fill(0)
|
|
76
|
+
.map(() => (rng.rand() as number) * 2 - 1);
|
|
77
|
+
const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0));
|
|
78
|
+
pos[node] = coords.map((c, j) => (c / norm) * scale + center[j]);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return pos;
|
|
83
|
+
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Geometric layout algorithms
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { circularLayout } from
|
|
6
|
-
export { shellLayout } from
|
|
7
|
-
export { spiralLayout } from
|
|
5
|
+
export { circularLayout } from "./circular";
|
|
6
|
+
export { shellLayout } from "./shell";
|
|
7
|
+
export { spiralLayout } from "./spiral";
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* Shell layout algorithm
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Graph, Node, PositionMap } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { Graph, Node, PositionMap } from "../../types";
|
|
6
|
+
import { getNodesFromGraph } from "../../utils/graph";
|
|
7
|
+
import { np } from "../../utils/numpy";
|
|
8
|
+
import { _processParams } from "../../utils/params";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Position nodes in concentric circles.
|
|
12
|
-
*
|
|
13
12
|
* @param G - Graph or list of nodes
|
|
14
13
|
* @param nlist - List of node lists for each shell
|
|
15
14
|
* @param scale - Scale factor for positions
|
|
@@ -17,64 +16,70 @@ import { np } from '../../utils/numpy';
|
|
|
17
16
|
* @param dim - Dimension of layout (currently only supports dim=2)
|
|
18
17
|
* @returns Positions dictionary keyed by node
|
|
19
18
|
*/
|
|
20
|
-
export function shellLayout(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
export function shellLayout(
|
|
20
|
+
G: Graph,
|
|
21
|
+
nlist: Node[][] | null = null,
|
|
22
|
+
scale: number = 1,
|
|
23
|
+
center: number[] | null = null,
|
|
24
|
+
dim: number = 2,
|
|
25
|
+
): PositionMap {
|
|
26
|
+
if (dim !== 2) {
|
|
27
|
+
throw new Error("can only handle 2 dimensions");
|
|
28
|
+
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const processed = _processParams(G, center, dim);
|
|
31
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
32
|
+
center = processed.center;
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
const pos: PositionMap = {};
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
if (nodes.length === 0) {
|
|
37
|
+
return pos;
|
|
38
|
+
}
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if (nodes.length === 1) {
|
|
41
|
+
pos[nodes[0]] = center;
|
|
42
|
+
return pos;
|
|
43
|
+
}
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// If no nlist is specified, put all nodes in a single shell
|
|
46
|
+
if (!nlist) {
|
|
47
|
+
nlist = [nodes];
|
|
48
|
+
}
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const radiusBump = scale / nlist.length;
|
|
51
|
+
let radius: number;
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (nlist[0].length === 1) {
|
|
54
|
+
// Single node at center
|
|
55
|
+
radius = 0;
|
|
56
|
+
pos[nlist[0][0]] = [...center];
|
|
57
|
+
radius += radiusBump;
|
|
58
|
+
} else {
|
|
59
|
+
// Start at radius 1
|
|
60
|
+
radius = radiusBump;
|
|
61
|
+
}
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
for (let i = 0; i < nlist.length; i++) {
|
|
64
|
+
const shell = nlist[i];
|
|
65
|
+
if (shell.length === 0) {continue;}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
if (shell.length === 1 && i === 0) {
|
|
68
|
+
// Already handled the case of a single center node
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
// Calculate positions on a circle
|
|
73
|
+
const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1);
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
shell.forEach((node: Node, j) => {
|
|
76
|
+
const x = Math.cos(theta[j]) * radius + center[0];
|
|
77
|
+
const y = Math.sin(theta[j]) * radius + center[1];
|
|
78
|
+
pos[node] = [x, y];
|
|
79
|
+
});
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
radius += radiusBump;
|
|
82
|
+
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
}
|
|
84
|
+
return pos;
|
|
85
|
+
}
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* Spiral layout algorithm
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { Graph, Node, PositionMap } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { rescaleLayout } from
|
|
5
|
+
import { Graph, Node, PositionMap } from "../../types";
|
|
6
|
+
import { getNodesFromGraph } from "../../utils/graph";
|
|
7
|
+
import { _processParams } from "../../utils/params";
|
|
8
|
+
import { rescaleLayout } from "../../utils/rescale";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Position nodes in a spiral layout.
|
|
12
|
-
*
|
|
13
12
|
* @param G - Graph or list of nodes
|
|
14
13
|
* @param scale - Scale factor for positions
|
|
15
14
|
* @param center - Coordinate pair around which to center the layout
|
|
@@ -19,75 +18,72 @@ import { rescaleLayout } from '../../utils/rescale';
|
|
|
19
18
|
* @returns Positions dictionary keyed by node
|
|
20
19
|
*/
|
|
21
20
|
export function spiralLayout(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
G: Graph,
|
|
22
|
+
scale: number = 1,
|
|
23
|
+
center: number[] | null = null,
|
|
24
|
+
dim: number = 2,
|
|
25
|
+
resolution: number = 0.35,
|
|
26
|
+
equidistant: boolean = false,
|
|
28
27
|
): PositionMap {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (dim !== 2) {
|
|
29
|
+
throw new Error("can only handle 2 dimensions");
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const processed = _processParams(G, center || [0, 0], dim);
|
|
33
|
+
const nodes = getNodesFromGraph(processed.G);
|
|
34
|
+
center = processed.center;
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
const pos: PositionMap = {};
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (nodes.length === 0) {
|
|
39
|
+
return pos;
|
|
40
|
+
}
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
if (nodes.length === 1) {
|
|
43
|
+
pos[nodes[0]] = [...center];
|
|
44
|
+
return pos;
|
|
45
|
+
}
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
let positions: number[][] = [];
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
if (equidistant) {
|
|
50
|
+
// Create equidistant points along the spiral
|
|
51
|
+
// This matches the Python implementation logic
|
|
52
|
+
const chord = 1;
|
|
53
|
+
const step = 0.5;
|
|
54
|
+
let theta = resolution;
|
|
55
|
+
theta += chord / (step * theta);
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
58
|
+
const r = step * theta;
|
|
59
|
+
theta += chord / r;
|
|
60
|
+
positions.push([Math.cos(theta) * r, Math.sin(theta) * r]);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Create points with equal angle but increasing distance
|
|
64
|
+
const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i)));
|
|
65
|
+
const angle = dist.map((d) => resolution * d);
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Math.sin(angle[i]) * d
|
|
71
|
-
]);
|
|
72
|
-
}
|
|
67
|
+
positions = dist.map((d, i) => [Math.cos(angle[i]) * d, Math.sin(angle[i]) * d]);
|
|
68
|
+
}
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
// Convert position array to position matrix for rescaling
|
|
71
|
+
const posArray: number[][] = [];
|
|
72
|
+
for (let i = 0; i < positions.length; i++) {
|
|
73
|
+
posArray.push(positions[i]);
|
|
74
|
+
}
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
// Rescale positions and add center offset
|
|
77
|
+
const scaledPositions = rescaleLayout(posArray as any, scale) as any;
|
|
78
|
+
for (let i = 0; i < scaledPositions.length; i++) {
|
|
79
|
+
scaledPositions[i][0] += center[0];
|
|
80
|
+
scaledPositions[i][1] += center[1];
|
|
81
|
+
}
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
// Create position dictionary
|
|
84
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
85
|
+
pos[nodes[i]] = scaledPositions[i];
|
|
86
|
+
}
|
|
91
87
|
|
|
92
|
-
|
|
93
|
-
}
|
|
88
|
+
return pos;
|
|
89
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import type { Graph, Node,
|
|
2
|
-
import { getNodesFromGraph
|
|
3
|
-
import { _processParams } from
|
|
4
|
-
import { multipartiteLayout } from
|
|
1
|
+
import type { Edge, Graph, Node, PositionMap } from "../../types";
|
|
2
|
+
import { getNeighbors,getNodesFromGraph } from "../../utils/graph";
|
|
3
|
+
import { _processParams } from "../../utils/params";
|
|
4
|
+
import { multipartiteLayout } from "./multipartite";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Position nodes according to breadth-first search algorithm.
|
|
8
|
-
*
|
|
9
|
-
* @param G - Graph
|
|
8
|
+
* @param G - Graph
|
|
10
9
|
* @param start - Starting node for bfs
|
|
11
10
|
* @param align - The alignment of layers: 'vertical' or 'horizontal'
|
|
12
11
|
* @param scale - Scale factor for positions
|
|
@@ -14,68 +13,68 @@ import { multipartiteLayout } from './multipartite';
|
|
|
14
13
|
* @returns Positions dictionary keyed by node
|
|
15
14
|
*/
|
|
16
15
|
export function bfsLayout(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
G: Graph,
|
|
17
|
+
start: Node,
|
|
18
|
+
align: "vertical" | "horizontal" = "vertical",
|
|
19
|
+
scale: number = 1,
|
|
20
|
+
center: number[] | null = null,
|
|
22
21
|
): PositionMap {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// BFS layout requires a proper Graph, not just a list of nodes
|
|
26
|
-
if (Array.isArray(processed.G)) {
|
|
27
|
-
throw new Error('BFS layout requires a Graph with edges, not just a list of nodes');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const graph = processed.G;
|
|
31
|
-
center = processed.center;
|
|
22
|
+
const processed = _processParams(G, center || [0, 0], 2);
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
// BFS layout requires a proper Graph, not just a list of nodes
|
|
25
|
+
if (Array.isArray(processed.G)) {
|
|
26
|
+
throw new Error("BFS layout requires a Graph with edges, not just a list of nodes");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const graph = processed.G;
|
|
30
|
+
center = processed.center;
|
|
31
|
+
|
|
32
|
+
const allNodes = getNodesFromGraph(graph);
|
|
33
|
+
|
|
34
|
+
if (allNodes.length === 0) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// Compute BFS layers
|
|
39
|
+
const layers: Record<number, Node[]> = {};
|
|
40
|
+
const visited = new Set<Node>();
|
|
41
|
+
let currentLayer = 0;
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let currentLayer = 0;
|
|
43
|
+
// Starting layer
|
|
44
|
+
layers[currentLayer] = [start];
|
|
45
|
+
visited.add(start);
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
// BFS traversal
|
|
48
|
+
while (Object.values(layers).flat().length < allNodes.length) {
|
|
49
|
+
const nextLayer: Node[] = [];
|
|
50
|
+
const currentNodes = layers[currentLayer];
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
for (const node of currentNodes) {
|
|
53
|
+
// Get neighbors - this is a simplified approach
|
|
54
|
+
// In a real implementation, we would get neighbors from the graph
|
|
55
|
+
const neighbors = getNeighbors(graph, node);
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
for (const neighbor of neighbors) {
|
|
58
|
+
if (!visited.has(neighbor)) {
|
|
59
|
+
nextLayer.push(neighbor);
|
|
60
|
+
visited.add(neighbor);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
if (nextLayer.length === 0) {
|
|
66
|
+
// No more connected nodes
|
|
67
|
+
const unvisited: Node[] = allNodes.filter((node: Node) => !visited.has(node));
|
|
68
|
+
if (unvisited.length > 0) {
|
|
69
|
+
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
62
72
|
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const unvisited: Node[] = allNodes.filter((node: Node) => !visited.has(node));
|
|
69
|
-
if (unvisited.length > 0) {
|
|
70
|
-
throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected.");
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
74
|
+
currentLayer++;
|
|
75
|
+
layers[currentLayer] = nextLayer;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
layers
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Use multipartite_layout to position the layers
|
|
80
|
-
return multipartiteLayout(graph, layers, align, scale, center);
|
|
81
|
-
}
|
|
78
|
+
// Use multipartite_layout to position the layers
|
|
79
|
+
return multipartiteLayout(graph, layers, align, scale, center);
|
|
80
|
+
}
|