UNPKG

@graphty/layout

Version:

graph layout algorithms based on networkx

221 lines (187 loc) 5.86 kB
/** * Embedding functions for planar graphs */ import { Node, Edge, Embedding, PositionMap } from '../../types'; import { RandomNumberGenerator } from '../../utils/random'; /** * Create a triangulation-based embedding for a planar graph * * @param nodes - List of nodes * @param edges - List of edges * @returns Embedding object */ export function createTriangulationEmbedding( nodes: Node[], edges: Edge[], seed: number | null = null ): Embedding { // Create RNG instance for deterministic randomness const rng = new RandomNumberGenerator(seed ?? undefined); // Create a simple embedding using the incremental approach const embedding: Embedding = { nodeOrder: [...nodes], faceList: [], nodePositions: {} }; // Create a map of adjacent nodes const adjMap: Record<Node, Set<Node>> = {}; for (const node of nodes) { adjMap[node] = new Set<Node>(); } for (const [u, v] of edges) { adjMap[u].add(v); adjMap[v].add(u); } // Create outer face as a cycle (if possible) const outerFace = findCycle(nodes, edges, adjMap) || nodes; embedding.faceList.push(outerFace); // Position nodes on a convex polygon (outer face) const n = outerFace.length; for (let i = 0; i < n; i++) { const angle = 2 * Math.PI * i / n; embedding.nodePositions[outerFace[i]] = [Math.cos(angle), Math.sin(angle)]; } // Position interior nodes using barycentric coordinates const interiorNodes = nodes.filter(node => !embedding.nodePositions[node]); for (const node of interiorNodes) { const neighbors = Array.from(adjMap[node]); if (neighbors.length === 0) { // Isolated node, place at center embedding.nodePositions[node] = [0, 0]; } else { // Average position of neighbors that have positions let xSum = 0, ySum = 0, count = 0; for (const neighbor of neighbors) { if (embedding.nodePositions[neighbor]) { xSum += embedding.nodePositions[neighbor][0]; ySum += embedding.nodePositions[neighbor][1]; count++; } } if (count > 0) { // Place slightly away from center to avoid overlaps const jitter = 0.1 * (rng.rand() as number); embedding.nodePositions[node] = [ xSum / count + jitter * ((rng.rand() as number) - 0.5), ySum / count + jitter * ((rng.rand() as number) - 0.5) ]; } else { // No neighbors have positions yet, place randomly inside unit circle const r = 0.5 * (rng.rand() as number); const angle = 2 * Math.PI * (rng.rand() as number); embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)]; } } } return embedding; } /** * Find a simple cycle in the graph (for outer face) * * @param nodes - List of nodes * @param edges - List of edges * @param adjMap - Adjacency map * @returns Cycle as array of nodes, or null if none found */ export function findCycle( nodes: Node[], edges: Edge[], adjMap: Record<Node, Set<Node>> ): Node[] | null { if (nodes.length === 0) return null; if (nodes.length <= 2) return nodes; // Not a real cycle but handle it // Try to find a Hamiltonian cycle for simplicity (for small graphs) if (nodes.length <= 8) { const visited = new Set<Node>(); const path: Node[] = []; function hamiltonianCycleDFS(node: Node): boolean { path.push(node); visited.add(node); if (path.length === nodes.length) { // Check if it's a cycle (last node connects to first) if (adjMap[node].has(path[0])) { return true; } // Not a cycle visited.delete(node); path.pop(); return false; } for (const neighbor of adjMap[node]) { if (!visited.has(neighbor)) { if (hamiltonianCycleDFS(neighbor)) { return true; } } } visited.delete(node); path.pop(); return false; } if (hamiltonianCycleDFS(nodes[0])) { return path; } } // Fallback: try to find any cycle using DFS const visited = new Set<Node>(); const parent: Record<Node, Node | null> = {}; let cycleFound: Node[] | null = null; function findCycleDFS(node: Node, parentNode: Node | null): boolean { visited.add(node); for (const neighbor of adjMap[node]) { if (neighbor === parentNode) continue; if (visited.has(neighbor)) { // Found a cycle cycleFound = constructCycle(node, neighbor, parent); return true; } parent[neighbor] = node; if (findCycleDFS(neighbor, node)) { return true; } } return false; } function constructCycle(u: Node, v: Node, parent: Record<Node, Node | null>): Node[] { const cycle: Node[] = [v, u]; let current = u; while (parent[current] !== undefined && parent[current] !== v) { current = parent[current]!; cycle.push(current); } return cycle; } // Try to find a cycle for (const node of nodes) { if (!visited.has(node)) { parent[node] = null; if (findCycleDFS(node, null)) { break; } } } return cycleFound || nodes; // Fallback to all nodes if no cycle found } /** * Convert a combinatorial embedding to node positions * * @param embedding - The embedding object * @param nodes - List of nodes * @returns Dictionary mapping nodes to positions */ export function combinatorialEmbeddingToPos( embedding: Embedding, nodes: Node[] ): PositionMap { const pos: PositionMap = {}; // Use the positions from the embedding for (const node of nodes) { if (embedding.nodePositions[node]) { pos[node] = embedding.nodePositions[node]; } else { // Fallback for any nodes without positions pos[node] = [0, 0]; } } return pos; }