@graphty/layout
Version:
graph layout algorithms based on networkx
131 lines • 5.1 kB
JavaScript
/**
* Fruchterman-Reingold force-directed layout algorithm
*/
import { _processParams } from '../../utils/params';
import { getNodesFromGraph, getEdgesFromGraph } from '../../utils/graph';
import { RandomNumberGenerator } from '../../utils/random';
import { rescaleLayout } from '../../utils/rescale';
/**
* Position nodes using Fruchterman-Reingold force-directed algorithm.
*
* @param {Object} G - Graph or list of nodes
* @param {number} k - Optimal distance between nodes
* @param {Object} pos - Initial positions for nodes
* @param {Array} fixed - Nodes to keep fixed at initial position
* @param {number} iterations - Maximum number of iterations
* @param {number} scale - Scale factor for positions
* @param {Array|null} center - Coordinate pair around which to center the layout
* @param {number} dim - Dimension of layout
* @param {number} seed - Random seed for initial positions
* @returns {Object} Positions dictionary keyed by node
*/
export function fruchtermanReingoldLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) {
const processed = _processParams(G, center, dim);
let graph = processed.G;
center = processed.center;
const nodes = getNodesFromGraph(graph);
const edges = getEdgesFromGraph(graph);
if (nodes.length === 0) {
return {};
}
if (nodes.length === 1) {
const singlePos = {};
singlePos[nodes[0]] = center;
return singlePos;
}
// Set up initial positions
let positions = {};
if (pos) {
// Use provided positions
for (const node of nodes) {
if (pos[node]) {
positions[node] = [...pos[node]];
}
else {
const rng = new RandomNumberGenerator(seed ?? undefined);
positions[node] = rng.rand(dim);
}
}
}
else {
// Random initial positions
const rng = new RandomNumberGenerator(seed ?? undefined);
for (const node of nodes) {
positions[node] = rng.rand(dim);
}
}
// Set up fixed nodes
const fixedNodes = new Set(fixed || []);
// Optimal distance between nodes
if (!k) {
k = 1.0 / Math.sqrt(nodes.length);
}
// Initialize temperature
let t = 0.1;
// Calculate temperature reduction
const dt = t / (iterations + 1);
// Simple cooling schedule
for (let i = 0; i < iterations; i++) {
// Calculate repulsive forces
const displacement = {};
for (const node of nodes) {
displacement[node] = Array(dim).fill(0);
}
// Repulsive forces between nodes
for (let v1i = 0; v1i < nodes.length; v1i++) {
const v1 = nodes[v1i];
for (let v2i = v1i + 1; v2i < nodes.length; v2i++) {
const v2 = nodes[v2i];
// Difference vector
const delta = positions[v1].map((p, i) => p - positions[v2][i]);
// Distance
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
// Force
const force = (k * k) / distance;
// Add force to displacement
for (let j = 0; j < dim; j++) {
const direction = delta[j] / distance;
displacement[v1][j] += direction * force;
displacement[v2][j] -= direction * force;
}
}
}
// Attractive forces between connected nodes
for (const [source, target] of edges) {
// Difference vector
const delta = positions[source].map((p, i) => p - positions[target][i]);
// Distance
const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1;
// Force
const force = (distance * distance) / k;
// Add force to displacement
for (let j = 0; j < dim; j++) {
const direction = delta[j] / distance;
displacement[source][j] -= direction * force;
displacement[target][j] += direction * force;
}
}
// Update positions
for (const node of nodes) {
if (fixedNodes.has(node))
continue;
// Calculate displacement magnitude
const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0));
// Limit maximum displacement by temperature
const limitedMagnitude = Math.min(magnitude, t);
// Update position
for (let j = 0; j < dim; j++) {
const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude;
positions[node][j] += direction * limitedMagnitude;
}
}
// Cool temperature
t -= dt;
}
// Rescale positions
if (!fixed) {
positions = rescaleLayout(positions, scale, center);
}
return positions;
}
//# sourceMappingURL=fruchterman-reingold.js.map