UNPKG

@graphty/layout

Version:

graph layout algorithms based on networkx

103 lines 3.74 kB
import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph'; import { RandomNumberGenerator } from '../../utils/random'; import { randomLayout } from '../basic/random'; /** * Layout algorithm with attractive and repulsive forces (ARF). * * @param G - Graph * @param pos - Initial positions for nodes * @param scaling - Scale factor for positions * @param a - Strength of springs between connected nodes (should be > 1) * @param maxIter - Maximum number of iterations * @param seed - Random seed for initial positions * @returns Positions dictionary keyed by node */ export function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1000, seed = null) { if (a <= 1) { throw new Error("The parameter a should be larger than 1"); } const nodes = getNodesFromGraph(G); const edges = getEdgesFromGraph(G); if (nodes.length === 0) { return {}; } // Initialize positions if not provided if (!pos) { pos = randomLayout(G, null, 2, seed); } else { // Make sure all nodes have positions const rng = new RandomNumberGenerator(seed ?? undefined); const defaultPos = {}; nodes.forEach((node) => { if (!pos[node]) { defaultPos[node] = [rng.rand(), rng.rand()]; } }); pos = { ...pos, ...defaultPos }; } // Create node index mapping const nodeIndex = {}; nodes.forEach((node, i) => { nodeIndex[node] = i; }); // Create positions array const positions = nodes.map((node) => [...pos[node]]); // Initialize spring constant matrix const N = nodes.length; const K = Array(N).fill(0).map(() => Array(N).fill(1)); // Set diagonal to zero (no self-attraction) for (let i = 0; i < N; i++) { K[i][i] = 0; } // Set stronger attraction between connected nodes for (const [source, target] of edges) { if (source === target) continue; const i = nodeIndex[source]; const j = nodeIndex[target]; K[i][j] = a; K[j][i] = a; } // Calculate rho (scale factor) const rho = scaling * Math.sqrt(N); // Optimization loop const dt = 1e-3; // Time step const etol = 1e-6; // Error tolerance let error = etol + 1; let nIter = 0; while (error > etol && nIter < maxIter) { // Calculate changes for each node const change = Array(N).fill(0).map(() => [0, 0]); for (let i = 0; i < N; i++) { for (let j = 0; j < N; j++) { if (i === j) continue; // Calculate difference vector const diff = positions[i].map((coord, dim) => coord - positions[j][dim]); // Calculate distance (with minimum to avoid division by zero) const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01; // Calculate attractive and repulsive forces for (let d = 0; d < diff.length; d++) { change[i][d] += K[i][j] * diff[d] - (rho / dist) * diff[d]; } } } // Update positions for (let i = 0; i < N; i++) { for (let d = 0; d < positions[i].length; d++) { positions[i][d] += change[i][d] * dt; } } // Calculate error (sum of force magnitudes) error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0); nIter++; } // Convert positions array back to object const finalPos = {}; nodes.forEach((node, i) => { finalPos[node] = positions[i]; }); return finalPos; } //# sourceMappingURL=arf.js.map