@graphty/layout 1.2.9 → 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 +393 -403
- package/dist/layout.js +867 -808
- 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 +2 -2
- package/dist/src/algorithms/planarity/check.js +4 -4
- package/dist/src/algorithms/planarity/check.js.map +1 -1
- package/dist/src/algorithms/planarity/embedding.d.ts +2 -4
- package/dist/src/algorithms/planarity/embedding.js +13 -12
- 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 +2 -2
- package/dist/src/algorithms/planarity/lr-test.js +2 -2
- 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 +1 -2
- package/dist/src/layouts/specialized/planar.js +5 -6
- 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 -22
- package/src/algorithms/planarity/embedding.ts +159 -170
- package/src/algorithms/planarity/index.ts +4 -8
- package/src/algorithms/planarity/lr-test.ts +46 -46
- 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 +41 -42
- 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,220 +2,209 @@
|
|
|
2
2
|
* Embedding functions for planar graphs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { RandomNumberGenerator } from
|
|
5
|
+
import { Edge, Embedding, Node, PositionMap } from "../../types";
|
|
6
|
+
import { RandomNumberGenerator } from "../../utils/random";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create a triangulation-based embedding for a planar graph
|
|
10
|
-
*
|
|
11
10
|
* @param nodes - List of nodes
|
|
12
11
|
* @param edges - List of edges
|
|
12
|
+
* @param seed - Random seed for deterministic embedding generation
|
|
13
13
|
* @returns Embedding object
|
|
14
14
|
*/
|
|
15
|
-
export function createTriangulationEmbedding(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
adjMap
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
15
|
+
export function createTriangulationEmbedding(nodes: Node[], edges: Edge[], seed: number | null = null): Embedding {
|
|
16
|
+
// Create RNG instance for deterministic randomness
|
|
17
|
+
const rng = new RandomNumberGenerator(seed ?? undefined);
|
|
18
|
+
|
|
19
|
+
// Create a simple embedding using the incremental approach
|
|
20
|
+
const embedding: Embedding = {
|
|
21
|
+
nodeOrder: [...nodes],
|
|
22
|
+
faceList: [],
|
|
23
|
+
nodePositions: {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Create a map of adjacent nodes
|
|
27
|
+
const adjMap: Record<Node, Set<Node>> = {};
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
adjMap[node] = new Set<Node>();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [u, v] of edges) {
|
|
33
|
+
adjMap[u].add(v);
|
|
34
|
+
adjMap[v].add(u);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create outer face as a cycle (if possible)
|
|
38
|
+
const outerFace = findCycle(nodes, edges, adjMap) || nodes;
|
|
39
|
+
embedding.faceList.push(outerFace);
|
|
40
|
+
|
|
41
|
+
// Position nodes on a convex polygon (outer face)
|
|
42
|
+
const n = outerFace.length;
|
|
43
|
+
for (let i = 0; i < n; i++) {
|
|
44
|
+
const angle = (2 * Math.PI * i) / n;
|
|
45
|
+
embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Position interior nodes using barycentric coordinates
|
|
49
|
+
const interiorNodes = nodes.filter((node) => !embedding.nodePositions[node]);
|
|
50
|
+
|
|
51
|
+
for (const node of interiorNodes) {
|
|
52
|
+
const neighbors = Array.from(adjMap[node]);
|
|
53
|
+
|
|
54
|
+
if (neighbors.length === 0) {
|
|
55
|
+
// Isolated node, place at center
|
|
56
|
+
embedding.nodePositions[node] = [0, 0];
|
|
57
|
+
} else {
|
|
58
|
+
// Average position of neighbors that have positions
|
|
59
|
+
let xSum = 0,
|
|
60
|
+
ySum = 0,
|
|
61
|
+
count = 0;
|
|
62
|
+
|
|
63
|
+
for (const neighbor of neighbors) {
|
|
64
|
+
if (embedding.nodePositions[neighbor]) {
|
|
65
|
+
xSum += embedding.nodePositions[neighbor][0];
|
|
66
|
+
ySum += embedding.nodePositions[neighbor][1];
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (count > 0) {
|
|
72
|
+
// Place slightly away from center to avoid overlaps
|
|
73
|
+
const jitter = 0.1 * (rng.rand() as number);
|
|
74
|
+
embedding.nodePositions[node] = [
|
|
75
|
+
xSum / count + jitter * ((rng.rand() as number) - 0.5),
|
|
76
|
+
ySum / count + jitter * ((rng.rand() as number) - 0.5),
|
|
77
|
+
];
|
|
78
|
+
} else {
|
|
79
|
+
// No neighbors have positions yet, place randomly inside unit circle
|
|
80
|
+
const r = 0.5 * (rng.rand() as number);
|
|
81
|
+
const angle = 2 * Math.PI * (rng.rand() as number);
|
|
82
|
+
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
83
|
+
}
|
|
70
84
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (count > 0) {
|
|
74
|
-
// Place slightly away from center to avoid overlaps
|
|
75
|
-
const jitter = 0.1 * (rng.rand() as number);
|
|
76
|
-
embedding.nodePositions[node] = [
|
|
77
|
-
xSum / count + jitter * ((rng.rand() as number) - 0.5),
|
|
78
|
-
ySum / count + jitter * ((rng.rand() as number) - 0.5)
|
|
79
|
-
];
|
|
80
|
-
} else {
|
|
81
|
-
// No neighbors have positions yet, place randomly inside unit circle
|
|
82
|
-
const r = 0.5 * (rng.rand() as number);
|
|
83
|
-
const angle = 2 * Math.PI * (rng.rand() as number);
|
|
84
|
-
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
85
|
-
}
|
|
86
85
|
}
|
|
87
|
-
}
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
return embedding;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
/**
|
|
93
91
|
* Find a simple cycle in the graph (for outer face)
|
|
94
|
-
*
|
|
95
92
|
* @param nodes - List of nodes
|
|
96
93
|
* @param edges - List of edges
|
|
97
94
|
* @param adjMap - Adjacency map
|
|
98
95
|
* @returns Cycle as array of nodes, or null if none found
|
|
99
96
|
*/
|
|
100
|
-
export function findCycle(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
97
|
+
export function findCycle(nodes: Node[], edges: Edge[], adjMap: Record<Node, Set<Node>>): Node[] | null {
|
|
98
|
+
if (nodes.length === 0) {return null;}
|
|
99
|
+
if (nodes.length <= 2) {return nodes;} // Not a real cycle but handle it
|
|
100
|
+
|
|
101
|
+
// Try to find a Hamiltonian cycle for simplicity (for small graphs)
|
|
102
|
+
if (nodes.length <= 8) {
|
|
103
|
+
const visited = new Set<Node>();
|
|
104
|
+
const path: Node[] = [];
|
|
105
|
+
|
|
106
|
+
function hamiltonianCycleDFS(node: Node): boolean {
|
|
107
|
+
path.push(node);
|
|
108
|
+
visited.add(node);
|
|
109
|
+
|
|
110
|
+
if (path.length === nodes.length) {
|
|
111
|
+
// Check if it's a cycle (last node connects to first)
|
|
112
|
+
if (adjMap[node].has(path[0])) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// Not a cycle
|
|
116
|
+
visited.delete(node);
|
|
117
|
+
path.pop();
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const neighbor of adjMap[node]) {
|
|
122
|
+
if (!visited.has(neighbor)) {
|
|
123
|
+
if (hamiltonianCycleDFS(neighbor)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
visited.delete(node);
|
|
130
|
+
path.pop();
|
|
131
|
+
return false;
|
|
121
132
|
}
|
|
122
|
-
// Not a cycle
|
|
123
|
-
visited.delete(node);
|
|
124
|
-
path.pop();
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
133
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (hamiltonianCycleDFS(neighbor)) {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
134
|
+
if (hamiltonianCycleDFS(nodes[0])) {
|
|
135
|
+
return path;
|
|
133
136
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
visited.delete(node);
|
|
137
|
-
path.pop();
|
|
138
|
-
return false;
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
139
|
+
// Fallback: try to find any cycle using DFS
|
|
140
|
+
const visited = new Set<Node>();
|
|
141
|
+
const parent: Record<Node, Node | null> = {};
|
|
142
|
+
let cycleFound: Node[] | null = null;
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const parent: Record<Node, Node | null> = {};
|
|
149
|
-
let cycleFound: Node[] | null = null;
|
|
144
|
+
function findCycleDFS(node: Node, parentNode: Node | null): boolean {
|
|
145
|
+
visited.add(node);
|
|
150
146
|
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
for (const neighbor of adjMap[node]) {
|
|
148
|
+
if (neighbor === parentNode) {continue;}
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
if (visited.has(neighbor)) {
|
|
151
|
+
// Found a cycle
|
|
152
|
+
cycleFound = constructCycle(node, neighbor, parent);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
parent[neighbor] = node;
|
|
157
|
+
if (findCycleDFS(neighbor, node)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
if (findCycleDFS(neighbor, node)) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
162
|
+
return false;
|
|
167
163
|
}
|
|
168
164
|
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
function constructCycle(u: Node, v: Node, parent: Record<Node, Node | null>): Node[] {
|
|
166
|
+
const cycle: Node[] = [v, u];
|
|
167
|
+
let current = u;
|
|
171
168
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
while (parent[current] !== undefined && parent[current] !== v) {
|
|
170
|
+
current = parent[current]!;
|
|
171
|
+
cycle.push(current);
|
|
172
|
+
}
|
|
175
173
|
|
|
176
|
-
|
|
177
|
-
current = parent[current]!;
|
|
178
|
-
cycle.push(current);
|
|
174
|
+
return cycle;
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
177
|
+
// Try to find a cycle
|
|
178
|
+
for (const node of nodes) {
|
|
179
|
+
if (!visited.has(node)) {
|
|
180
|
+
parent[node] = null;
|
|
181
|
+
if (findCycleDFS(node, null)) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
191
185
|
}
|
|
192
|
-
}
|
|
193
186
|
|
|
194
|
-
|
|
187
|
+
return cycleFound || nodes; // Fallback to all nodes if no cycle found
|
|
195
188
|
}
|
|
196
189
|
|
|
197
190
|
/**
|
|
198
191
|
* Convert a combinatorial embedding to node positions
|
|
199
|
-
*
|
|
200
192
|
* @param embedding - The embedding object
|
|
201
193
|
* @param nodes - List of nodes
|
|
202
194
|
* @returns Dictionary mapping nodes to positions
|
|
203
195
|
*/
|
|
204
|
-
export function combinatorialEmbeddingToPos(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Fallback for any nodes without positions
|
|
216
|
-
pos[node] = [0, 0];
|
|
196
|
+
export function combinatorialEmbeddingToPos(embedding: Embedding, nodes: Node[]): PositionMap {
|
|
197
|
+
const pos: PositionMap = {};
|
|
198
|
+
|
|
199
|
+
// Use the positions from the embedding
|
|
200
|
+
for (const node of nodes) {
|
|
201
|
+
if (embedding.nodePositions[node]) {
|
|
202
|
+
pos[node] = embedding.nodePositions[node];
|
|
203
|
+
} else {
|
|
204
|
+
// Fallback for any nodes without positions
|
|
205
|
+
pos[node] = [0, 0];
|
|
206
|
+
}
|
|
217
207
|
}
|
|
218
|
-
}
|
|
219
208
|
|
|
220
|
-
|
|
221
|
-
}
|
|
209
|
+
return pos;
|
|
210
|
+
}
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
* Re-export all planarity algorithms
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { checkPlanarity } from
|
|
6
|
-
export {
|
|
7
|
-
export { lrPlanarityTest } from
|
|
8
|
-
export {
|
|
9
|
-
createTriangulationEmbedding,
|
|
10
|
-
findCycle,
|
|
11
|
-
combinatorialEmbeddingToPos
|
|
12
|
-
} from './embedding';
|
|
5
|
+
export { checkPlanarity } from "./check";
|
|
6
|
+
export { combinatorialEmbeddingToPos,createTriangulationEmbedding, findCycle } from "./embedding";
|
|
7
|
+
export { lrPlanarityTest } from "./lr-test";
|
|
8
|
+
export { isK5, isK33, tryFindBipartitePartition } from "./special-graphs";
|
|
@@ -2,70 +2,70 @@
|
|
|
2
2
|
* Left-Right Planarity Test implementation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { createTriangulationEmbedding } from
|
|
5
|
+
import { Edge, Embedding,Node } from "../../types";
|
|
6
|
+
import { createTriangulationEmbedding } from "./embedding";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Left-Right Planarity Test for general graphs
|
|
10
|
-
*
|
|
11
10
|
* @param nodes - List of nodes
|
|
12
11
|
* @param edges - List of edges
|
|
12
|
+
* @param seed - Random seed for deterministic embedding generation
|
|
13
13
|
* @returns Object containing isPlanar flag and embedding
|
|
14
14
|
*/
|
|
15
15
|
export function lrPlanarityTest(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
nodes: Node[],
|
|
17
|
+
edges: Edge[],
|
|
18
|
+
seed: number | null = null,
|
|
19
19
|
): { isPlanar: boolean; embedding: Embedding | null } {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// Create adjacency list for the graph
|
|
21
|
+
const adjList: Record<Node, Node[]> = {};
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
adjList[node] = [];
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
for (const [u, v] of edges) {
|
|
27
|
+
adjList[u].push(v);
|
|
28
|
+
adjList[v].push(u);
|
|
29
|
+
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Step 1: Perform DFS to get an st-numbering (ordering of nodes)
|
|
32
|
+
const visited = new Set<Node>();
|
|
33
|
+
const ordering: Node[] = [];
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
function dfs(node: Node): void {
|
|
36
|
+
visited.add(node);
|
|
37
|
+
ordering.push(node);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
for (const neighbor of adjList[node]) {
|
|
40
|
+
if (!visited.has(neighbor)) {
|
|
41
|
+
dfs(neighbor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
43
44
|
}
|
|
44
|
-
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Start DFS from first node
|
|
47
|
+
dfs(nodes[0]);
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// If the graph is disconnected, it's still planar but we need to handle each component
|
|
50
|
+
if (ordering.length < nodes.length) {
|
|
51
|
+
// Create a simple triangulation embedding for disconnected graphs
|
|
52
|
+
return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) };
|
|
53
|
+
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Step 2: For a general implementation, we'll use a simplified approach
|
|
56
|
+
// since this would normally require implementing the entire LR algorithm
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// For this implementation, since we can't fully implement Boyer-Myrvold,
|
|
59
|
+
// we'll create a reasonable planar embedding for most planar graphs
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
// We assume the graph is planar if it's sparse enough (|E| <= 3|V| - 6)
|
|
62
|
+
// This is a necessary but not sufficient condition for planar graphs
|
|
63
|
+
if (edges.length > 3 * nodes.length - 6) {
|
|
64
|
+
return { isPlanar: false, embedding: null };
|
|
65
|
+
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// Create a planar embedding using a triangulation approach
|
|
68
|
+
const embedding = createTriangulationEmbedding(nodes, edges, seed);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
}
|
|
70
|
+
return { isPlanar: true, embedding };
|
|
71
|
+
}
|