@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,215 +2,209 @@
|
|
|
2
2
|
* Embedding functions for planar graphs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Edge, Embedding, Node, PositionMap } from "../../types";
|
|
6
|
+
import { RandomNumberGenerator } from "../../utils/random";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Create a triangulation-based embedding for a planar graph
|
|
9
|
-
*
|
|
10
10
|
* @param nodes - List of nodes
|
|
11
11
|
* @param edges - List of edges
|
|
12
|
+
* @param seed - Random seed for deterministic embedding generation
|
|
12
13
|
* @returns Embedding object
|
|
13
14
|
*/
|
|
14
|
-
export function createTriangulationEmbedding(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
65
84
|
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (count > 0) {
|
|
69
|
-
// Place slightly away from center to avoid overlaps
|
|
70
|
-
const jitter = 0.1 * Math.random();
|
|
71
|
-
embedding.nodePositions[node] = [
|
|
72
|
-
xSum / count + jitter * (Math.random() - 0.5),
|
|
73
|
-
ySum / count + jitter * (Math.random() - 0.5)
|
|
74
|
-
];
|
|
75
|
-
} else {
|
|
76
|
-
// No neighbors have positions yet, place randomly inside unit circle
|
|
77
|
-
const r = 0.5 * Math.random();
|
|
78
|
-
const angle = 2 * Math.PI * Math.random();
|
|
79
|
-
embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)];
|
|
80
|
-
}
|
|
81
85
|
}
|
|
82
|
-
}
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
return embedding;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
88
91
|
* Find a simple cycle in the graph (for outer face)
|
|
89
|
-
*
|
|
90
92
|
* @param nodes - List of nodes
|
|
91
93
|
* @param edges - List of edges
|
|
92
94
|
* @param adjMap - Adjacency map
|
|
93
95
|
* @returns Cycle as array of nodes, or null if none found
|
|
94
96
|
*/
|
|
95
|
-
export function findCycle(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
132
|
}
|
|
117
|
-
// Not a cycle
|
|
118
|
-
visited.delete(node);
|
|
119
|
-
path.pop();
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (hamiltonianCycleDFS(neighbor)) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
134
|
+
if (hamiltonianCycleDFS(nodes[0])) {
|
|
135
|
+
return path;
|
|
128
136
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
visited.delete(node);
|
|
132
|
-
path.pop();
|
|
133
|
-
return false;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
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;
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const parent: Record<Node, Node | null> = {};
|
|
144
|
-
let cycleFound: Node[] | null = null;
|
|
144
|
+
function findCycleDFS(node: Node, parentNode: Node | null): boolean {
|
|
145
|
+
visited.add(node);
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
for (const neighbor of adjMap[node]) {
|
|
148
|
+
if (neighbor === parentNode) {continue;}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
if (visited.has(neighbor)) {
|
|
151
|
+
// Found a cycle
|
|
152
|
+
cycleFound = constructCycle(node, neighbor, parent);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
parent[neighbor] = node;
|
|
157
|
+
if (findCycleDFS(neighbor, node)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
if (findCycleDFS(neighbor, node)) {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
+
return false;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
function constructCycle(u: Node, v: Node, parent: Record<Node, Node | null>): Node[] {
|
|
166
|
+
const cycle: Node[] = [v, u];
|
|
167
|
+
let current = u;
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
while (parent[current] !== undefined && parent[current] !== v) {
|
|
170
|
+
current = parent[current]!;
|
|
171
|
+
cycle.push(current);
|
|
172
|
+
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
current = parent[current]!;
|
|
173
|
-
cycle.push(current);
|
|
174
|
+
return cycle;
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
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
|
+
}
|
|
186
185
|
}
|
|
187
|
-
}
|
|
188
186
|
|
|
189
|
-
|
|
187
|
+
return cycleFound || nodes; // Fallback to all nodes if no cycle found
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
/**
|
|
193
191
|
* Convert a combinatorial embedding to node positions
|
|
194
|
-
*
|
|
195
192
|
* @param embedding - The embedding object
|
|
196
193
|
* @param nodes - List of nodes
|
|
197
194
|
* @returns Dictionary mapping nodes to positions
|
|
198
195
|
*/
|
|
199
|
-
export function combinatorialEmbeddingToPos(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Fallback for any nodes without positions
|
|
211
|
-
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
|
+
}
|
|
212
207
|
}
|
|
213
|
-
}
|
|
214
208
|
|
|
215
|
-
|
|
216
|
-
}
|
|
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,69 +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
|
-
|
|
16
|
+
nodes: Node[],
|
|
17
|
+
edges: Edge[],
|
|
18
|
+
seed: number | null = null,
|
|
18
19
|
): { isPlanar: boolean; embedding: Embedding | null } {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Create adjacency list for the graph
|
|
21
|
+
const adjList: Record<Node, Node[]> = {};
|
|
22
|
+
for (const node of nodes) {
|
|
23
|
+
adjList[node] = [];
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
for (const [u, v] of edges) {
|
|
27
|
+
adjList[u].push(v);
|
|
28
|
+
adjList[v].push(u);
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Step 1: Perform DFS to get an st-numbering (ordering of nodes)
|
|
32
|
+
const visited = new Set<Node>();
|
|
33
|
+
const ordering: Node[] = [];
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
function dfs(node: Node): void {
|
|
36
|
+
visited.add(node);
|
|
37
|
+
ordering.push(node);
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
for (const neighbor of adjList[node]) {
|
|
40
|
+
if (!visited.has(neighbor)) {
|
|
41
|
+
dfs(neighbor);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
42
44
|
}
|
|
43
|
-
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// Start DFS from first node
|
|
47
|
+
dfs(nodes[0]);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// Step 2: For a general implementation, we'll use a simplified approach
|
|
56
|
+
// since this would normally require implementing the entire LR algorithm
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// For this implementation, since we can't fully implement Boyer-Myrvold,
|
|
59
|
+
// we'll create a reasonable planar embedding for most planar graphs
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// Create a planar embedding using a triangulation approach
|
|
68
|
+
const embedding = createTriangulationEmbedding(nodes, edges, seed);
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
}
|
|
70
|
+
return { isPlanar: true, embedding };
|
|
71
|
+
}
|