UNPKG

@graphty/layout

Version:

graph layout algorithms based on networkx

106 lines 4.21 kB
/** * Spectral layout algorithm using eigenvectors of the graph Laplacian */ import { _processParams } from '../../utils/params'; import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph'; import { rescaleLayout } from '../../utils/rescale'; /** * Position nodes in a spectral layout using eigenvectors of the graph Laplacian. * * @param G - Graph * @param scale - Scale factor for positions * @param center - Coordinate pair around which to center the layout * @param dim - Dimension of layout * @returns Positions dictionary keyed by node */ export function spectralLayout(G, scale = 1, center = null, dim = 2) { const processed = _processParams(G, center, dim); const graph = processed.G; center = processed.center; const nodes = getNodesFromGraph(graph); if (nodes.length <= 2) { if (nodes.length === 0) { return {}; } else if (nodes.length === 1) { return { [nodes[0]]: center }; } else { return { [nodes[0]]: center.map(v => v - scale), [nodes[1]]: center.map(v => v + scale) }; } } // Create adjacency matrix const N = nodes.length; const nodeIndices = {}; nodes.forEach((node, i) => { nodeIndices[node] = i; }); const A = Array(N).fill(0).map(() => Array(N).fill(0)); const edges = getEdgesFromGraph(graph); for (const [source, target] of edges) { const i = nodeIndices[source]; const j = nodeIndices[target]; A[i][j] = 1; A[j][i] = 1; // Make symmetric for undirected graphs } // Create Laplacian matrix: L = D - A where D is degree matrix const L = Array(N).fill(0).map(() => Array(N).fill(0)); for (let i = 0; i < N; i++) { // Compute degree (sum of row) L[i][i] = A[i].reduce((sum, val) => sum + val, 0); for (let j = 0; j < N; j++) { L[i][j] -= A[i][j]; } } // Compute eigenvectors using power iteration method // We need the smallest non-zero eigenvectors of L const eigenvectors = []; // For each dimension, find an eigenvector for (let d = 0; d < dim; d++) { let vector = Array(N).fill(0).map(() => Math.random() - 0.5); // Orthogonalize against previous eigenvectors for (const ev of eigenvectors) { const dot = vector.reduce((acc, val, idx) => acc + val * ev[idx], 0); vector = vector.map((val, idx) => val - dot * ev[idx]); } // Normalize const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0)); vector = vector.map(val => val / norm); // Apply shifted inverse iteration to find smallest non-zero eigenvector // This is a simplification of the actual algorithm for (let iter = 0; iter < 100; iter++) { // Apply Laplacian const newVec = Array(N).fill(0); for (let i = 0; i < N; i++) { for (let j = 0; j < N; j++) { newVec[i] += L[i][j] * vector[j]; } } // Orthogonalize against the constant vector (eigenvector with eigenvalue 0) const mean = newVec.reduce((acc, val) => acc + val, 0) / N; newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; }); // Normalize const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0)); if (newNorm < 1e-10) continue; // Skip if vector is close to zero vector = newVec.map(val => val / newNorm); } eigenvectors.push(vector); } // Create position array from eigenvectors const positions = Array(N).fill(0).map(() => Array(dim).fill(0)); for (let i = 0; i < N; i++) { for (let d = 0; d < dim; d++) { positions[i][d] = eigenvectors[d][i]; } } // Rescale and create position dictionary const scaledPositions = rescaleLayout(positions, scale); const pos = {}; nodes.forEach((node, i) => { pos[node] = scaledPositions[i].map((val, j) => val + center[j]); }); return pos; } //# sourceMappingURL=spectral.js.map