@graphty/layout 1.1.1 → 1.2.1

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.
Files changed (100) hide show
  1. package/.env.example +11 -0
  2. package/.github/workflows/ci.yml +3 -7
  3. package/CHANGELOG.md +14 -0
  4. package/DEPLOYMENT.md +59 -0
  5. package/README.md +54 -1
  6. package/dist/vitest.config.js +2 -2
  7. package/dist/vitest.config.js.map +1 -1
  8. package/examples/3d-force-directed.html +611 -0
  9. package/examples/3d-kamada-kawai.html +379 -0
  10. package/examples/3d-layout-comparison.html +448 -0
  11. package/examples/3d-spherical-layout.html +319 -0
  12. package/examples/arf-layout.html +13 -1
  13. package/examples/bfs-layout.html +38 -32
  14. package/examples/bipartite-layout.html +16 -4
  15. package/examples/circular-layout.html +14 -2
  16. package/examples/forceatlas2-layout.html +118 -151
  17. package/examples/index.html +75 -0
  18. package/examples/kamada-kawai-layout.html +13 -1
  19. package/{dist → examples}/layout-helpers.js +58 -29
  20. package/examples/multipartite-layout.html +44 -54
  21. package/examples/planar-layout.html +13 -1
  22. package/examples/random-layout.html +13 -1
  23. package/examples/shell-layout.html +16 -2
  24. package/examples/spectral-layout.html +13 -1
  25. package/examples/spiral-layout.html +13 -1
  26. package/examples/spring-layout.html +15 -13
  27. package/package.json +6 -3
  28. package/src/algorithms/index.ts +6 -0
  29. package/src/algorithms/optimization/index.ts +12 -0
  30. package/src/algorithms/optimization/kamada-kawai-solver.ts +231 -0
  31. package/src/algorithms/optimization/lbfgs.ts +68 -0
  32. package/src/algorithms/optimization/line-search.ts +50 -0
  33. package/src/algorithms/optimization/types.ts +8 -0
  34. package/src/algorithms/planarity/check.ts +38 -0
  35. package/src/algorithms/planarity/embedding.ts +216 -0
  36. package/src/algorithms/planarity/index.ts +12 -0
  37. package/src/algorithms/planarity/lr-test.ts +70 -0
  38. package/src/algorithms/planarity/special-graphs.ts +126 -0
  39. package/src/generators/basic.ts +93 -0
  40. package/src/generators/bipartite.ts +45 -0
  41. package/src/generators/grid.ts +42 -0
  42. package/src/generators/index.ts +15 -0
  43. package/src/generators/random.ts +39 -0
  44. package/src/generators/scale-free.ts +72 -0
  45. package/src/index.ts +18 -0
  46. package/src/layouts/basic/index.ts +5 -0
  47. package/src/layouts/basic/random.ts +32 -0
  48. package/src/layouts/force-directed/arf.ts +130 -0
  49. package/src/layouts/force-directed/forceatlas2.ts +407 -0
  50. package/src/layouts/force-directed/fruchterman-reingold.ts +164 -0
  51. package/src/layouts/force-directed/index.ts +9 -0
  52. package/src/layouts/force-directed/kamada-kawai.ts +112 -0
  53. package/src/layouts/force-directed/spring.ts +35 -0
  54. package/src/layouts/geometric/circular.ts +77 -0
  55. package/src/layouts/geometric/index.ts +7 -0
  56. package/src/layouts/geometric/shell.ts +80 -0
  57. package/src/layouts/geometric/spiral.ts +93 -0
  58. package/src/layouts/hierarchical/bfs.ts +81 -0
  59. package/src/layouts/hierarchical/bipartite.ts +94 -0
  60. package/src/layouts/hierarchical/index.ts +7 -0
  61. package/src/layouts/hierarchical/multipartite.ts +88 -0
  62. package/src/layouts/index.ts +9 -0
  63. package/src/layouts/specialized/index.ts +6 -0
  64. package/src/layouts/specialized/planar.ts +65 -0
  65. package/src/layouts/specialized/spectral.ts +128 -0
  66. package/src/types/embedding.ts +11 -0
  67. package/src/types/graph.ts +13 -0
  68. package/src/types/index.ts +7 -0
  69. package/src/types/layout.ts +9 -0
  70. package/src/utils/graph.ts +77 -0
  71. package/src/utils/index.ts +9 -0
  72. package/src/utils/numpy.ts +108 -0
  73. package/src/utils/params.ts +26 -0
  74. package/src/utils/random.ts +53 -0
  75. package/src/utils/rescale.ts +137 -0
  76. package/test/arf-layout.test.ts +1 -1
  77. package/test/bfs-layout.test.ts +1 -1
  78. package/test/bipartite-layout.test.ts +1 -1
  79. package/test/circular-layout.test.ts +145 -3
  80. package/test/forceatlas2-layout.test.ts +1 -1
  81. package/test/fruchterman-reingold-layout.test.ts +1 -1
  82. package/test/graph-generators.test.ts +1 -1
  83. package/test/kamada-kawai-layout.test.ts +273 -1
  84. package/test/multipartite-layout.test.ts +1 -1
  85. package/test/planar-layout.test.ts +1 -1
  86. package/test/random-layout.test.ts +1 -1
  87. package/test/rescale-layout.test.ts +1 -1
  88. package/test/shell-layout.test.ts +1 -1
  89. package/test/spectral-layout.test.ts +1 -1
  90. package/test/spiral-layout.test.ts +1 -1
  91. package/test/spring-layout.test.ts +1 -1
  92. package/vite.config.js +36 -0
  93. package/vitest.config.ts +2 -2
  94. package/dist/layout-helpers.d.ts +0 -123
  95. package/dist/layout-helpers.js.map +0 -1
  96. package/dist/layout.d.ts +0 -275
  97. package/dist/layout.js +0 -2280
  98. package/dist/layout.js.map +0 -1
  99. package/layout-helpers.ts +0 -559
  100. package/layout.ts +0 -2867
@@ -0,0 +1,68 @@
1
+ /**
2
+ * L-BFGS optimization utilities
3
+ */
4
+
5
+ /**
6
+ * Compute the search direction using L-BFGS approximation
7
+ *
8
+ * @param grad - Current gradient
9
+ * @param sList - List of position differences (s_k)
10
+ * @param yList - List of gradient differences (y_k)
11
+ * @param m - Memory size
12
+ * @returns Direction vector
13
+ */
14
+ export function _lbfgsDirection(
15
+ grad: number[],
16
+ sList: number[][],
17
+ yList: number[][],
18
+ m: number
19
+ ): number[] {
20
+ if (sList.length === 0) {
21
+ // First iteration - use negative gradient
22
+ return grad.map(g => -g);
23
+ }
24
+
25
+ const q = grad.slice();
26
+ const alpha = Array(sList.length).fill(0);
27
+ const rho: number[] = [];
28
+
29
+ // Compute rho values
30
+ for (let i = 0; i < sList.length; i++) {
31
+ const s = sList[i];
32
+ const y = yList[i];
33
+ rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0));
34
+ }
35
+
36
+ // Forward pass
37
+ for (let i = sList.length - 1; i >= 0; i--) {
38
+ const s = sList[i];
39
+ alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0);
40
+ for (let j = 0; j < q.length; j++) {
41
+ q[j] -= alpha[i] * yList[i][j];
42
+ }
43
+ }
44
+
45
+ // Scale initial Hessian approximation
46
+ let gamma = 1;
47
+ if (sList.length > 0 && yList.length > 0) {
48
+ const y = yList[yList.length - 1];
49
+ const s = sList[sList.length - 1];
50
+ gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) /
51
+ y.reduce((sum, val) => sum + val * val, 0);
52
+ }
53
+
54
+ // Initialize direction with scaled negative gradient
55
+ const direction = q.map(val => -gamma * val);
56
+
57
+ // Backward pass
58
+ for (let i = 0; i < sList.length; i++) {
59
+ const s = sList[i];
60
+ const y = yList[i];
61
+ const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0);
62
+ for (let j = 0; j < direction.length; j++) {
63
+ direction[j] += s[j] * (alpha[i] - beta);
64
+ }
65
+ }
66
+
67
+ return direction;
68
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Line search optimization utilities
3
+ */
4
+
5
+ /**
6
+ * Backtracking line search to find step size
7
+ *
8
+ * @param x - Current position
9
+ * @param direction - Search direction
10
+ * @param f - Function value at current position
11
+ * @param grad - Gradient at current position
12
+ * @param func - Function to evaluate cost
13
+ * @param alpha0 - Initial step size
14
+ * @returns Optimal step size
15
+ */
16
+ export function _backtrackingLineSearch(
17
+ x: number[],
18
+ direction: number[],
19
+ f: number,
20
+ grad: number[],
21
+ func: (x: number[]) => number,
22
+ alpha0: number
23
+ ): number {
24
+ const c1 = 1e-4;
25
+ const c2 = 0.9;
26
+ const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0);
27
+
28
+ if (initialSlope >= 0) {
29
+ return 1e-8; // Direction is not a descent direction
30
+ }
31
+
32
+ let alpha = alpha0;
33
+ const maxIter = 20;
34
+
35
+ for (let i = 0; i < maxIter; i++) {
36
+ // Try step
37
+ const newX = x.map((val, i) => val + alpha * direction[i]);
38
+ const newF = func(newX);
39
+
40
+ // Check sufficient decrease condition (Armijo condition)
41
+ if (newF <= f + c1 * alpha * initialSlope) {
42
+ return alpha;
43
+ }
44
+
45
+ // Reduce step size
46
+ alpha *= c2;
47
+ }
48
+
49
+ return alpha; // Return last alpha even if not optimal
50
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Type definitions for optimization algorithms
3
+ */
4
+
5
+ import { Node } from '../../types';
6
+
7
+ // Type definitions for distance structure used in Kamada-Kawai
8
+ export type DistanceMap = Record<Node, Record<Node, number>>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Main planarity checking function
3
+ */
4
+
5
+ import { Graph, Node, Edge, Embedding } from '../../types';
6
+ import { isK5, isK33 } from './special-graphs';
7
+ import { lrPlanarityTest } from './lr-test';
8
+ import { createTriangulationEmbedding } from './embedding';
9
+
10
+ /**
11
+ * Check if graph is planar using a simplified version of Boyer-Myrvold algorithm.
12
+ * Returns planarity and embedding information.
13
+ *
14
+ * @param G - Graph
15
+ * @param nodes - List of nodes
16
+ * @param edges - List of edges
17
+ * @returns Object containing isPlanar flag and embedding
18
+ */
19
+ export function checkPlanarity(
20
+ G: Graph,
21
+ nodes: Node[],
22
+ edges: Edge[]
23
+ ): { isPlanar: boolean; embedding: Embedding | null } {
24
+ // For small graphs (n <= 4), all are planar
25
+ if (nodes.length <= 4) {
26
+ return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
27
+ }
28
+
29
+ // For K5 (complete graph with 5 nodes) and K3,3 (complete bipartite with 3,3 nodes)
30
+ // these are not planar by Kuratowski's theorem
31
+ if (isK5(nodes, edges) || isK33(nodes, edges)) {
32
+ return { isPlanar: false, embedding: null };
33
+ }
34
+
35
+ // For other graphs, use LR algorithm (Left-Right Planarity Test)
36
+ const result = lrPlanarityTest(nodes, edges);
37
+ return result;
38
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Embedding functions for planar graphs
3
+ */
4
+
5
+ import { Node, Edge, Embedding, PositionMap } from '../../types';
6
+
7
+ /**
8
+ * Create a triangulation-based embedding for a planar graph
9
+ *
10
+ * @param nodes - List of nodes
11
+ * @param edges - List of edges
12
+ * @returns Embedding object
13
+ */
14
+ export function createTriangulationEmbedding(
15
+ nodes: Node[],
16
+ edges: Edge[]
17
+ ): Embedding {
18
+ // Create a simple embedding using the incremental approach
19
+ const embedding: Embedding = {
20
+ nodeOrder: [...nodes],
21
+ faceList: [],
22
+ nodePositions: {}
23
+ };
24
+
25
+ // Create a map of adjacent nodes
26
+ const adjMap: Record<Node, Set<Node>> = {};
27
+ for (const node of nodes) {
28
+ adjMap[node] = new Set<Node>();
29
+ }
30
+
31
+ for (const [u, v] of edges) {
32
+ adjMap[u].add(v);
33
+ adjMap[v].add(u);
34
+ }
35
+
36
+ // Create outer face as a cycle (if possible)
37
+ const outerFace = findCycle(nodes, edges, adjMap) || nodes;
38
+ embedding.faceList.push(outerFace);
39
+
40
+ // Position nodes on a convex polygon (outer face)
41
+ const n = outerFace.length;
42
+ for (let i = 0; i < n; i++) {
43
+ const angle = 2 * Math.PI * i / n;
44
+ embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)];
45
+ }
46
+
47
+ // Position interior nodes using barycentric coordinates
48
+ const interiorNodes = nodes.filter(node => !embedding.nodePositions[node]);
49
+
50
+ for (const node of interiorNodes) {
51
+ const neighbors = Array.from(adjMap[node]);
52
+
53
+ if (neighbors.length === 0) {
54
+ // Isolated node, place at center
55
+ embedding.nodePositions[node] = [0, 0];
56
+ } else {
57
+ // Average position of neighbors that have positions
58
+ let xSum = 0, ySum = 0, count = 0;
59
+
60
+ for (const neighbor of neighbors) {
61
+ if (embedding.nodePositions[neighbor]) {
62
+ xSum += embedding.nodePositions[neighbor][0];
63
+ ySum += embedding.nodePositions[neighbor][1];
64
+ count++;
65
+ }
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
+ }
82
+ }
83
+
84
+ return embedding;
85
+ }
86
+
87
+ /**
88
+ * Find a simple cycle in the graph (for outer face)
89
+ *
90
+ * @param nodes - List of nodes
91
+ * @param edges - List of edges
92
+ * @param adjMap - Adjacency map
93
+ * @returns Cycle as array of nodes, or null if none found
94
+ */
95
+ export function findCycle(
96
+ nodes: Node[],
97
+ edges: Edge[],
98
+ adjMap: Record<Node, Set<Node>>
99
+ ): Node[] | null {
100
+ if (nodes.length === 0) return null;
101
+ if (nodes.length <= 2) return nodes; // Not a real cycle but handle it
102
+
103
+ // Try to find a Hamiltonian cycle for simplicity (for small graphs)
104
+ if (nodes.length <= 8) {
105
+ const visited = new Set<Node>();
106
+ const path: Node[] = [];
107
+
108
+ function hamiltonianCycleDFS(node: Node): boolean {
109
+ path.push(node);
110
+ visited.add(node);
111
+
112
+ if (path.length === nodes.length) {
113
+ // Check if it's a cycle (last node connects to first)
114
+ if (adjMap[node].has(path[0])) {
115
+ return true;
116
+ }
117
+ // Not a cycle
118
+ visited.delete(node);
119
+ path.pop();
120
+ return false;
121
+ }
122
+
123
+ for (const neighbor of adjMap[node]) {
124
+ if (!visited.has(neighbor)) {
125
+ if (hamiltonianCycleDFS(neighbor)) {
126
+ return true;
127
+ }
128
+ }
129
+ }
130
+
131
+ visited.delete(node);
132
+ path.pop();
133
+ return false;
134
+ }
135
+
136
+ if (hamiltonianCycleDFS(nodes[0])) {
137
+ return path;
138
+ }
139
+ }
140
+
141
+ // Fallback: try to find any cycle using DFS
142
+ const visited = new Set<Node>();
143
+ const parent: Record<Node, Node | null> = {};
144
+ let cycleFound: Node[] | null = null;
145
+
146
+ function findCycleDFS(node: Node, parentNode: Node | null): boolean {
147
+ visited.add(node);
148
+
149
+ for (const neighbor of adjMap[node]) {
150
+ if (neighbor === parentNode) continue;
151
+
152
+ if (visited.has(neighbor)) {
153
+ // Found a cycle
154
+ cycleFound = constructCycle(node, neighbor, parent);
155
+ return true;
156
+ }
157
+
158
+ parent[neighbor] = node;
159
+ if (findCycleDFS(neighbor, node)) {
160
+ return true;
161
+ }
162
+ }
163
+
164
+ return false;
165
+ }
166
+
167
+ function constructCycle(u: Node, v: Node, parent: Record<Node, Node | null>): Node[] {
168
+ const cycle: Node[] = [v, u];
169
+ let current = u;
170
+
171
+ while (parent[current] !== undefined && parent[current] !== v) {
172
+ current = parent[current]!;
173
+ cycle.push(current);
174
+ }
175
+
176
+ return cycle;
177
+ }
178
+
179
+ // Try to find a cycle
180
+ for (const node of nodes) {
181
+ if (!visited.has(node)) {
182
+ parent[node] = null;
183
+ if (findCycleDFS(node, null)) {
184
+ break;
185
+ }
186
+ }
187
+ }
188
+
189
+ return cycleFound || nodes; // Fallback to all nodes if no cycle found
190
+ }
191
+
192
+ /**
193
+ * Convert a combinatorial embedding to node positions
194
+ *
195
+ * @param embedding - The embedding object
196
+ * @param nodes - List of nodes
197
+ * @returns Dictionary mapping nodes to positions
198
+ */
199
+ export function combinatorialEmbeddingToPos(
200
+ embedding: Embedding,
201
+ nodes: Node[]
202
+ ): PositionMap {
203
+ const pos: PositionMap = {};
204
+
205
+ // Use the positions from the embedding
206
+ for (const node of nodes) {
207
+ if (embedding.nodePositions[node]) {
208
+ pos[node] = embedding.nodePositions[node];
209
+ } else {
210
+ // Fallback for any nodes without positions
211
+ pos[node] = [0, 0];
212
+ }
213
+ }
214
+
215
+ return pos;
216
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Re-export all planarity algorithms
3
+ */
4
+
5
+ export { checkPlanarity } from './check';
6
+ export { isK5, isK33, tryFindBipartitePartition } from './special-graphs';
7
+ export { lrPlanarityTest } from './lr-test';
8
+ export {
9
+ createTriangulationEmbedding,
10
+ findCycle,
11
+ combinatorialEmbeddingToPos
12
+ } from './embedding';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Left-Right Planarity Test implementation
3
+ */
4
+
5
+ import { Node, Edge, Embedding } from '../../types';
6
+ import { createTriangulationEmbedding } from './embedding';
7
+
8
+ /**
9
+ * Left-Right Planarity Test for general graphs
10
+ *
11
+ * @param nodes - List of nodes
12
+ * @param edges - List of edges
13
+ * @returns Object containing isPlanar flag and embedding
14
+ */
15
+ export function lrPlanarityTest(
16
+ nodes: Node[],
17
+ edges: Edge[]
18
+ ): { isPlanar: boolean; embedding: Embedding | null } {
19
+ // Create adjacency list for the graph
20
+ const adjList: Record<Node, Node[]> = {};
21
+ for (const node of nodes) {
22
+ adjList[node] = [];
23
+ }
24
+
25
+ for (const [u, v] of edges) {
26
+ adjList[u].push(v);
27
+ adjList[v].push(u);
28
+ }
29
+
30
+ // Step 1: Perform DFS to get an st-numbering (ordering of nodes)
31
+ const visited = new Set<Node>();
32
+ const ordering: Node[] = [];
33
+
34
+ function dfs(node: Node): void {
35
+ visited.add(node);
36
+ ordering.push(node);
37
+
38
+ for (const neighbor of adjList[node]) {
39
+ if (!visited.has(neighbor)) {
40
+ dfs(neighbor);
41
+ }
42
+ }
43
+ }
44
+
45
+ // Start DFS from first node
46
+ dfs(nodes[0]);
47
+
48
+ // If the graph is disconnected, it's still planar but we need to handle each component
49
+ if (ordering.length < nodes.length) {
50
+ // Create a simple triangulation embedding for disconnected graphs
51
+ return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges) };
52
+ }
53
+
54
+ // Step 2: For a general implementation, we'll use a simplified approach
55
+ // since this would normally require implementing the entire LR algorithm
56
+
57
+ // For this implementation, since we can't fully implement Boyer-Myrvold,
58
+ // we'll create a reasonable planar embedding for most planar graphs
59
+
60
+ // We assume the graph is planar if it's sparse enough (|E| <= 3|V| - 6)
61
+ // This is a necessary but not sufficient condition for planar graphs
62
+ if (edges.length > 3 * nodes.length - 6) {
63
+ return { isPlanar: false, embedding: null };
64
+ }
65
+
66
+ // Create a planar embedding using a triangulation approach
67
+ const embedding = createTriangulationEmbedding(nodes, edges);
68
+
69
+ return { isPlanar: true, embedding };
70
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Special graph detection functions for planarity testing
3
+ */
4
+
5
+ import { Node, Edge } from '../../types';
6
+
7
+ /**
8
+ * Check if graph is K5 (complete graph with 5 nodes)
9
+ *
10
+ * @param nodes - List of nodes
11
+ * @param edges - List of edges
12
+ * @returns True if graph is K5
13
+ */
14
+ export function isK5(nodes: Node[], edges: Edge[]): boolean {
15
+ if (nodes.length !== 5) return false;
16
+
17
+ // K5 has exactly 10 edges
18
+ if (edges.length !== 10) return false;
19
+
20
+ // Check if every pair of distinct nodes is connected
21
+ for (let i = 0; i < nodes.length; i++) {
22
+ for (let j = i + 1; j < nodes.length; j++) {
23
+ const hasEdge = edges.some(
24
+ e => (e[0] === nodes[i] && e[1] === nodes[j]) ||
25
+ (e[0] === nodes[j] && e[1] === nodes[i])
26
+ );
27
+ if (!hasEdge) return false;
28
+ }
29
+ }
30
+
31
+ return true;
32
+ }
33
+
34
+ /**
35
+ * Check if graph is K3,3 (complete bipartite with 3,3 nodes)
36
+ *
37
+ * @param nodes - List of nodes
38
+ * @param edges - List of edges
39
+ * @returns True if graph is K3,3
40
+ */
41
+ export function isK33(nodes: Node[], edges: Edge[]): boolean {
42
+ if (nodes.length !== 6) return false;
43
+
44
+ // K3,3 has exactly 9 edges
45
+ if (edges.length !== 9) return false;
46
+
47
+ // Try to find a bipartite partition
48
+ const nodePartitions = tryFindBipartitePartition(nodes, edges);
49
+ if (!nodePartitions) return false;
50
+
51
+ const [part1, part2] = nodePartitions;
52
+
53
+ // Check if both partitions have size 3
54
+ if (part1.length !== 3 || part2.length !== 3) return false;
55
+
56
+ // Check if every node in part1 is connected to every node in part2
57
+ for (const n1 of part1) {
58
+ for (const n2 of part2) {
59
+ const hasEdge = edges.some(
60
+ e => (e[0] === n1 && e[1] === n2) ||
61
+ (e[0] === n2 && e[1] === n1)
62
+ );
63
+ if (!hasEdge) return false;
64
+ }
65
+ }
66
+
67
+ return true;
68
+ }
69
+
70
+ /**
71
+ * Try to find a bipartite partition of the nodes
72
+ *
73
+ * @param nodes - List of nodes
74
+ * @param edges - List of edges
75
+ * @returns Array of two partitions, or null if not bipartite
76
+ */
77
+ export function tryFindBipartitePartition(
78
+ nodes: Node[],
79
+ edges: Edge[]
80
+ ): [Node[], Node[]] | null {
81
+ const colorMap: Record<Node, number> = {};
82
+ const adjList: Record<Node, Node[]> = {};
83
+
84
+ // Create adjacency list
85
+ for (const node of nodes) {
86
+ adjList[node] = [];
87
+ }
88
+
89
+ for (const [u, v] of edges) {
90
+ adjList[u].push(v);
91
+ adjList[v].push(u);
92
+ }
93
+
94
+ // BFS to color nodes
95
+ const queue: Node[] = [nodes[0]];
96
+ colorMap[nodes[0]] = 0;
97
+
98
+ while (queue.length > 0) {
99
+ const node = queue.shift()!;
100
+ const nodeColor = colorMap[node];
101
+
102
+ for (const neighbor of adjList[node]) {
103
+ if (colorMap[neighbor] === undefined) {
104
+ colorMap[neighbor] = 1 - nodeColor; // Toggle color (0/1)
105
+ queue.push(neighbor);
106
+ } else if (colorMap[neighbor] === nodeColor) {
107
+ // Conflict: not bipartite
108
+ return null;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Create partitions
114
+ const part0: Node[] = [];
115
+ const part1: Node[] = [];
116
+
117
+ for (const node of nodes) {
118
+ if (colorMap[node] === 0) {
119
+ part0.push(node);
120
+ } else {
121
+ part1.push(node);
122
+ }
123
+ }
124
+
125
+ return [part0, part1];
126
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Basic graph generation functions
3
+ */
4
+
5
+ import { Graph, Node, Edge } from '../types';
6
+
7
+ /**
8
+ * Create a complete graph with n nodes
9
+ * @param n - Number of nodes
10
+ * @returns Graph object with all nodes connected to all other nodes
11
+ */
12
+ export function completeGraph(n: number): Graph {
13
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
14
+ const edges: Edge[] = [];
15
+
16
+ for (let i = 0; i < n; i++) {
17
+ for (let j = i + 1; j < n; j++) {
18
+ edges.push([i, j]);
19
+ }
20
+ }
21
+
22
+ return {
23
+ nodes: () => nodes,
24
+ edges: () => edges
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Create a cycle graph with n nodes
30
+ * @param n - Number of nodes
31
+ * @returns Graph object with nodes connected in a cycle
32
+ */
33
+ export function cycleGraph(n: number): Graph {
34
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
35
+ const edges: Edge[] = [];
36
+
37
+ for (let i = 0; i < n; i++) {
38
+ edges.push([i, (i + 1) % n]);
39
+ }
40
+
41
+ return {
42
+ nodes: () => nodes,
43
+ edges: () => edges
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Create a star graph with n nodes (1 center + n-1 leaves)
49
+ * @param n - Total number of nodes
50
+ * @returns Graph object with star topology
51
+ */
52
+ export function starGraph(n: number): Graph {
53
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
54
+ const edges: Edge[] = [];
55
+
56
+ // Connect all nodes to node 0 (center)
57
+ for (let i = 1; i < n; i++) {
58
+ edges.push([0, i]);
59
+ }
60
+
61
+ return {
62
+ nodes: () => nodes,
63
+ edges: () => edges
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Create a wheel graph with n nodes (1 center + n-1 rim nodes)
69
+ * @param n - Total number of nodes
70
+ * @returns Graph object with wheel topology
71
+ */
72
+ export function wheelGraph(n: number): Graph {
73
+ const nodes: Node[] = Array.from({ length: n }, (_, i) => i);
74
+ const edges: Edge[] = [];
75
+
76
+ // Connect all rim nodes to center (node 0)
77
+ for (let i = 1; i < n; i++) {
78
+ edges.push([0, i]);
79
+ }
80
+
81
+ // Connect rim nodes in a cycle
82
+ for (let i = 1; i < n - 1; i++) {
83
+ edges.push([i, i + 1]);
84
+ }
85
+ if (n > 2) {
86
+ edges.push([n - 1, 1]);
87
+ }
88
+
89
+ return {
90
+ nodes: () => nodes,
91
+ edges: () => edges
92
+ };
93
+ }