UNPKG

@graphty/layout

Version:

graph layout algorithms based on networkx

1,612 lines 53.4 kB
function rescaleLayout(pos, scale = 1, center = null) { if (Array.isArray(pos)) { if (pos.length === 0) return []; } else { if (Object.keys(pos).length === 0) return {}; } const posValues = Array.isArray(pos) ? pos : Object.values(pos); const dim = posValues[0].length; if (!center) { center = Array(dim).fill(0); } const targetDim = Math.max(dim, center.length); const posCenter = Array(dim).fill(0); const counts = Array(dim).fill(0); for (const p of posValues) { for (let i = 0; i < dim; i++) { if (!isNaN(p[i])) { posCenter[i] += p[i]; counts[i]++; } } } for (let i = 0; i < dim; i++) { posCenter[i] = counts[i] > 0 ? posCenter[i] / counts[i] : 0; } let centeredPos = {}; if (Array.isArray(pos)) { centeredPos = pos.map((p) => { const centered = Array(targetDim).fill(0); for (let i = 0; i < targetDim; i++) { centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0); } return centered; }); } else { for (const [node, p] of Object.entries(pos)) { const centered = Array(targetDim).fill(0); for (let i = 0; i < targetDim; i++) { centered[i] = (i < p.length ? p[i] : 0) - (i < posCenter.length ? posCenter[i] : 0); } centeredPos[node] = centered; } } let maxDistance = 0; const centeredValues = Array.isArray(centeredPos) ? centeredPos : Object.values(centeredPos); for (const p of centeredValues) { let sumSquares = 0; for (const val of p) { if (!isNaN(val)) { sumSquares += val * val; } } const distance = Math.sqrt(sumSquares); if (!isNaN(distance)) { maxDistance = Math.max(maxDistance, distance); } } let scaledPos = Array.isArray(pos) ? [] : {}; if (maxDistance > 0) { const scaleFactor = scale / maxDistance; if (Array.isArray(pos)) { scaledPos = centeredPos.map( (p, idx) => p.map((val, i) => { const origPos = pos[idx]; if (i >= origPos.length) { return center[i]; } if (isNaN(val)) return NaN; return val * scaleFactor + center[i]; }) ); } else { for (const [node, p] of Object.entries(centeredPos)) { const origPos = pos[node]; scaledPos[node] = p.map((val, i) => { if (i >= origPos.length) { return center[i]; } if (isNaN(val)) return NaN; return val * scaleFactor + center[i]; }); } } } else { if (Array.isArray(pos)) { scaledPos = pos.map((p) => { const result = Array(targetDim); for (let i = 0; i < targetDim; i++) { if (i < p.length) { result[i] = isNaN(p[i]) ? NaN : center[i]; } else { result[i] = center[i]; } } return result; }); } else { for (const [node, p] of Object.entries(pos)) { const result = Array(targetDim); for (let i = 0; i < targetDim; i++) { if (i < p.length) { result[i] = isNaN(p[i]) ? NaN : center[i]; } else { result[i] = center[i]; } } scaledPos[node] = result; } } } return scaledPos; } function rescaleLayoutDict(pos, scale = 1) { if (Object.keys(pos).length === 0) { return {}; } const posArray = Object.values(pos); const center = []; for (let d = 0; d < posArray[0].length; d++) { center[d] = posArray.reduce((sum, p) => sum + p[d], 0) / posArray.length; } const centeredPos = {}; for (const [node, p] of Object.entries(pos)) { centeredPos[node] = p.map((val, d) => val - center[d]); } let maxDist = 0; for (const p of Object.values(centeredPos)) { const dist = Math.sqrt(p.reduce((sum, val) => sum + val * val, 0)); maxDist = Math.max(maxDist, dist); } const scaledPos = {}; if (maxDist > 0) { for (const [node, p] of Object.entries(centeredPos)) { scaledPos[node] = p.map((val) => val * scale / maxDist); } } else { for (const node of Object.keys(centeredPos)) { scaledPos[node] = Array(centeredPos[node].length).fill(0); } } return scaledPos; } function _processParams(G, center, dim) { if (!center) { center = Array(dim).fill(0); } if (center.length !== dim) { throw new Error("length of center coordinates must match dimension of layout"); } return { G, center }; } function getNodesFromGraph(G) { if (Array.isArray(G)) { return G; } return G.nodes(); } function getEdgesFromGraph(G) { if (Array.isArray(G)) { return []; } return G.edges(); } function getNeighbors(graph, node) { if (!graph.edges) return []; const neighbors = /* @__PURE__ */ new Set(); const edges = graph.edges(); for (const [source, target] of edges) { if (source === node) { neighbors.add(target); } else if (target === node) { neighbors.add(source); } } return Array.from(neighbors); } class RandomNumberGenerator { constructor(seed) { this.seed = seed || Math.floor(Math.random() * 1e6); this.m = 2 ** 35 - 31; this.a = 185852; this.c = 1; this._state = this.seed % this.m; } _next() { this._state = (this.a * this._state + this.c) % this.m; return this._state / this.m; } rand(shape = null) { if (shape === null) { return this._next(); } if (typeof shape === "number") { const result2 = []; for (let i = 0; i < shape; i++) { result2.push(this._next()); } return result2; } if (shape.length === 1) { const result2 = []; for (let i = 0; i < shape[0]; i++) { result2.push(this._next()); } return result2; } const result = []; for (let i = 0; i < shape[0]; i++) { result.push(this.rand(shape.slice(1))); } return result; } } function randomLayout(G, center = null, dim = 2, seed = null) { const processed = _processParams(G, center, dim); const nodes = getNodesFromGraph(processed.G); center = processed.center; const rng = new RandomNumberGenerator(seed ?? void 0); const pos = {}; nodes.forEach((node) => { pos[node] = rng.rand(dim).map((val, i) => val + center[i]); }); return pos; } const np = { zeros: function(shape) { if (typeof shape === "number") { return Array(shape).fill(0); } if (shape.length === 1) { return Array(shape[0]).fill(0); } return Array(shape[0]).fill(0).map(() => this.zeros(shape.slice(1))); }, ones: function(shape) { if (typeof shape === "number") { return Array(shape).fill(1); } if (shape.length === 1) { return Array(shape[0]).fill(1); } return Array(shape[0]).fill(1).map(() => this.ones(shape.slice(1))); }, linspace: function(start, stop, num) { if (num === 1) { return [start]; } const step = (stop - start) / (num - 1); return Array.from({ length: num }, (_, i) => start + i * step); }, array: function(arr) { return Array.isArray(arr) ? [...arr] : [arr]; }, repeat: function(a, repeats) { const result = []; for (let i = 0; i < repeats; i++) { result.push(...np.array(a)); } return result; }, mean: function(arr, axis = null) { if (axis === null) { const flatArr = Array.isArray(arr[0]) ? arr.flat(Infinity) : arr; const sum = flatArr.reduce((a, b) => a + b, 0); return sum / flatArr.length; } if (axis === 0) { const result = []; const matrix = arr; for (let i = 0; i < matrix[0].length; i++) { let sum = 0; for (let j = 0; j < matrix.length; j++) { sum += matrix[j][i]; } result.push(sum / matrix.length); } return result; } return arr.map((row) => np.mean(row)); }, add: function(a, b) { if (!Array.isArray(a) && !Array.isArray(b)) { return a + b; } if (!Array.isArray(a)) { return b.map((val) => a + val); } if (!Array.isArray(b)) { return a.map((val) => val + b); } return a.map((val, i) => val + b[i]); }, subtract: function(a, b) { if (!Array.isArray(a) && !Array.isArray(b)) { return a - b; } if (!Array.isArray(a)) { return b.map((val) => a - val); } if (!Array.isArray(b)) { return a.map((val) => val - b); } return a.map((val, i) => val - b[i]); }, max: function(arr) { if (!Array.isArray(arr)) return arr; return Math.max(...arr.flat(Infinity)); }, min: function(arr) { if (!Array.isArray(arr)) return arr; return Math.min(...arr.flat(Infinity)); }, norm: function(arr) { return Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0)); } }; function circularLayout(G, scale = 1, center = null, dim = 2) { if (dim < 2) { throw new Error("cannot handle dimensions < 2"); } const processed = _processParams(G, center, dim); const nodes = getNodesFromGraph(processed.G); center = processed.center; const pos = {}; if (nodes.length === 0) { return pos; } if (nodes.length === 1) { pos[nodes[0]] = center; return pos; } if (dim === 2) { const theta = np.linspace(0, 2 * Math.PI, nodes.length + 1).slice(0, -1); nodes.forEach((node, i) => { const x = Math.cos(theta[i]) * scale + center[0]; const y = Math.sin(theta[i]) * scale + center[1]; pos[node] = [x, y]; }); } else if (dim === 3) { const n = nodes.length; const goldenRatio = (1 + Math.sqrt(5)) / 2; nodes.forEach((node, i) => { const theta = 2 * Math.PI * i / goldenRatio; const phi = Math.acos(1 - 2 * (i + 0.5) / n); const x = Math.sin(phi) * Math.cos(theta) * scale + center[0]; const y = Math.sin(phi) * Math.sin(theta) * scale + center[1]; const z = Math.cos(phi) * scale + center[2]; pos[node] = [x, y, z]; }); } else { const rng = new RandomNumberGenerator(); nodes.forEach((node) => { const coords = Array(dim).fill(0).map(() => rng.rand() * 2 - 1); const norm = Math.sqrt(coords.reduce((sum, c) => sum + c * c, 0)); pos[node] = coords.map((c, j) => c / norm * scale + center[j]); }); } return pos; } function shellLayout(G, nlist = null, scale = 1, center = null, dim = 2) { if (dim !== 2) { throw new Error("can only handle 2 dimensions"); } const processed = _processParams(G, center, dim); const nodes = getNodesFromGraph(processed.G); center = processed.center; const pos = {}; if (nodes.length === 0) { return pos; } if (nodes.length === 1) { pos[nodes[0]] = center; return pos; } if (!nlist) { nlist = [nodes]; } const radiusBump = scale / nlist.length; let radius; if (nlist[0].length === 1) { radius = 0; pos[nlist[0][0]] = [...center]; radius += radiusBump; } else { radius = radiusBump; } for (let i = 0; i < nlist.length; i++) { const shell = nlist[i]; if (shell.length === 0) continue; if (shell.length === 1 && i === 0) { continue; } const theta = np.linspace(0, 2 * Math.PI, shell.length + 1).slice(0, -1); shell.forEach((node, j) => { const x = Math.cos(theta[j]) * radius + center[0]; const y = Math.sin(theta[j]) * radius + center[1]; pos[node] = [x, y]; }); radius += radiusBump; } return pos; } function spiralLayout(G, scale = 1, center = null, dim = 2, resolution = 0.35, equidistant = false) { if (dim !== 2) { throw new Error("can only handle 2 dimensions"); } const processed = _processParams(G, center || [0, 0], dim); const nodes = getNodesFromGraph(processed.G); center = processed.center; const pos = {}; if (nodes.length === 0) { return pos; } if (nodes.length === 1) { pos[nodes[0]] = [...center]; return pos; } let positions = []; if (equidistant) { const chord = 1; const step = 0.5; let theta = resolution; theta += chord / (step * theta); for (let i = 0; i < nodes.length; i++) { const r = step * theta; theta += chord / r; positions.push([Math.cos(theta) * r, Math.sin(theta) * r]); } } else { const dist = Array.from({ length: nodes.length }, (_, i) => parseFloat(String(i))); const angle = dist.map((d) => resolution * d); positions = dist.map((d, i) => [ Math.cos(angle[i]) * d, Math.sin(angle[i]) * d ]); } const posArray = []; for (let i = 0; i < positions.length; i++) { posArray.push(positions[i]); } const scaledPositions = rescaleLayout(posArray, scale); for (let i = 0; i < scaledPositions.length; i++) { scaledPositions[i][0] += center[0]; scaledPositions[i][1] += center[1]; } for (let i = 0; i < nodes.length; i++) { pos[nodes[i]] = scaledPositions[i]; } return pos; } 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; } let positions = {}; if (pos) { for (const node of nodes) { if (pos[node]) { positions[node] = [...pos[node]]; } else { const rng = new RandomNumberGenerator(seed ?? void 0); positions[node] = rng.rand(dim); } } } else { const rng = new RandomNumberGenerator(seed ?? void 0); for (const node of nodes) { positions[node] = rng.rand(dim); } } const fixedNodes = new Set(fixed || []); if (!k) { k = 1 / Math.sqrt(nodes.length); } let t = 0.1; const dt = t / (iterations + 1); for (let i = 0; i < iterations; i++) { const displacement = {}; for (const node of nodes) { displacement[node] = Array(dim).fill(0); } 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]; const delta = positions[v1].map((p, i2) => p - positions[v2][i2]); const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1; const force = k * k / distance; for (let j = 0; j < dim; j++) { const direction = delta[j] / distance; displacement[v1][j] += direction * force; displacement[v2][j] -= direction * force; } } } for (const [source, target] of edges) { const delta = positions[source].map((p, i2) => p - positions[target][i2]); const distance = Math.sqrt(delta.reduce((sum, d) => sum + d * d, 0)) || 0.1; const force = distance * distance / k; for (let j = 0; j < dim; j++) { const direction = delta[j] / distance; displacement[source][j] -= direction * force; displacement[target][j] += direction * force; } } for (const node of nodes) { if (fixedNodes.has(node)) continue; const magnitude = Math.sqrt(displacement[node].reduce((sum, d) => sum + d * d, 0)); const limitedMagnitude = Math.min(magnitude, t); for (let j = 0; j < dim; j++) { const direction = magnitude === 0 ? 0 : displacement[node][j] / magnitude; positions[node][j] += direction * limitedMagnitude; } } t -= dt; } if (!fixed) { positions = rescaleLayout(positions, scale, center); } return positions; } function springLayout(G, k = null, pos = null, fixed = null, iterations = 50, scale = 1, center = null, dim = 2, seed = null) { return fruchtermanReingoldLayout(G, k, pos, fixed, iterations, scale, center, dim, seed); } function _lbfgsDirection(grad, sList, yList, m) { if (sList.length === 0) { return grad.map((g) => -g); } const q = grad.slice(); const alpha = Array(sList.length).fill(0); const rho = []; for (let i = 0; i < sList.length; i++) { const s = sList[i]; const y = yList[i]; rho.push(1 / y.reduce((sum, val, j) => sum + val * s[j], 0)); } for (let i = sList.length - 1; i >= 0; i--) { const s = sList[i]; alpha[i] = rho[i] * s.reduce((sum, val, j) => sum + val * q[j], 0); for (let j = 0; j < q.length; j++) { q[j] -= alpha[i] * yList[i][j]; } } let gamma = 1; if (sList.length > 0 && yList.length > 0) { const y = yList[yList.length - 1]; const s = sList[sList.length - 1]; gamma = s.reduce((sum, val, i) => sum + val * y[i], 0) / y.reduce((sum, val) => sum + val * val, 0); } const direction = q.map((val) => -gamma * val); for (let i = 0; i < sList.length; i++) { const s = sList[i]; const y = yList[i]; const beta = rho[i] * y.reduce((sum, val, j) => sum + val * direction[j], 0); for (let j = 0; j < direction.length; j++) { direction[j] += s[j] * (alpha[i] - beta); } } return direction; } function _backtrackingLineSearch(x, direction, f, grad, func, alpha0) { const c1 = 1e-4; const c2 = 0.9; const initialSlope = grad.reduce((sum, g, i) => sum + g * direction[i], 0); if (initialSlope >= 0) { return 1e-8; } let alpha = alpha0; const maxIter = 20; for (let i = 0; i < maxIter; i++) { const newX = x.map((val, i2) => val + alpha * direction[i2]); const newF = func(newX); if (newF <= f + c1 * alpha * initialSlope) { return alpha; } alpha *= c2; } return alpha; } function _computeShortestPathDistances(G, weight) { const distances = {}; const nodes = getNodesFromGraph(G); const edges = getEdgesFromGraph(G); for (const node of nodes) { distances[node] = {}; distances[node][node] = 0; for (const other of nodes) { if (node !== other) { distances[node][other] = Infinity; } } } for (const [source, target] of edges) { let edgeWeight = 1; if (G.getEdgeData) { edgeWeight = G.getEdgeData(source, target, weight) || 1; } distances[source][target] = edgeWeight; distances[target][source] = edgeWeight; } for (const k of nodes) { for (const i of nodes) { for (const j of nodes) { if (distances[i][k] + distances[k][j] < distances[i][j]) { distances[i][j] = distances[i][k] + distances[k][j]; } } } } return distances; } function _kamadaKawaiSolve(distMatrix, positions, dim) { const nNodes = positions.length; const meanWeight = 1e-3; const invDistMatrix = distMatrix.map( (row) => row.map((d) => d === 0 ? 0 : 1 / (d + 1e-3)) ); let posVec = positions.flat(); const maxIter = 500; const gtol = 1e-5; const m = 10; let alpha = 1; const oldValues = []; const oldGrads = []; for (let iter = 0; iter < maxIter; iter++) { const [cost, grad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim); const direction = _lbfgsDirection(grad, oldValues, oldGrads); alpha = _backtrackingLineSearch( posVec, direction, cost, grad, (x) => _kamadaKawaiCostfn(x, invDistMatrix, meanWeight, dim)[0], alpha ); const oldPos = [...posVec]; for (let i = 0; i < posVec.length; i++) { posVec[i] += alpha * direction[i]; } const [, newGrad] = _kamadaKawaiCostfn(posVec, invDistMatrix, meanWeight, dim); oldValues.push(posVec.map((val, i) => val - oldPos[i])); oldGrads.push(newGrad.map((val, i) => val - grad[i])); if (oldValues.length > m) { oldValues.shift(); oldGrads.shift(); } const gradNorm = Math.sqrt(newGrad.reduce((sum, g) => sum + g * g, 0)); if (gradNorm < gtol) { break; } } const result = []; for (let i = 0; i < nNodes; i++) { result.push(posVec.slice(i * dim, (i + 1) * dim)); } return result; } function _kamadaKawaiCostfn(posVec, invDist, meanWeight, dim) { const nNodes = invDist.length; const positions = []; for (let i = 0; i < nNodes; i++) { positions.push(posVec.slice(i * dim, (i + 1) * dim)); } let cost = 0; const sumPos = Array(dim).fill(0); for (let i = 0; i < nNodes; i++) { for (let d = 0; d < dim; d++) { sumPos[d] += positions[i][d]; } } cost += 0.5 * meanWeight * sumPos.reduce((sum, val) => sum + val * val, 0); for (let i = 0; i < nNodes; i++) { for (let j = i + 1; j < nNodes; j++) { const diff = positions[i].map((val, d) => val - positions[j][d]); const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)); const idealInvDist = invDist[i][j]; const offset = distance * idealInvDist - 1; cost += 0.5 * offset * offset; } } const grad = new Array(posVec.length).fill(0); for (let i = 0; i < nNodes; i++) { for (let d = 0; d < dim; d++) { grad[i * dim + d] += meanWeight * sumPos[d]; } } for (let i = 0; i < nNodes; i++) { for (let j = i + 1; j < nNodes; j++) { const diff = positions[i].map((val, d) => val - positions[j][d]); const distance = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 1e-10; const direction = diff.map((d) => d / distance); const idealInvDist = invDist[i][j]; const offset = distance * idealInvDist - 1; for (let d = 0; d < dim; d++) { const force = idealInvDist * offset * direction[d]; grad[i * dim + d] += force; grad[j * dim + d] -= force; } } } return [cost, grad]; } function kamadaKawaiLayout(G, dist = null, pos = null, weight = "weight", 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 === 0) { return {}; } if (nodes.length === 1) { return { [nodes[0]]: center }; } if (!dist) { if (Array.isArray(graph)) { throw new Error("Kamada-Kawai layout requires a Graph with edges, not just a list of nodes"); } dist = _computeShortestPathDistances(graph, weight); } const nodesArray = Array.from(nodes); const nNodes = nodesArray.length; const distMatrix = Array(nNodes).fill(0).map(() => Array(nNodes).fill(1e6)); for (let i = 0; i < nNodes; i++) { const nodeI = nodesArray[i]; distMatrix[i][i] = 0; if (!dist[nodeI]) continue; for (let j = 0; j < nNodes; j++) { const nodeJ = nodesArray[j]; if (dist[nodeI][nodeJ] !== void 0) { distMatrix[i][j] = dist[nodeI][nodeJ]; } } } if (!pos) { if (dim >= 2) { pos = circularLayout(G, 1, center, dim); } else { const posArray2 = {}; nodesArray.forEach((node, i) => { posArray2[node] = [i / (nNodes - 1 || 1)]; }); pos = posArray2; } } const posArray = new Array(nNodes); for (let i = 0; i < nNodes; i++) { const node = nodesArray[i]; posArray[i] = pos[node] ? [...pos[node]] : Array(dim).fill(0); while (posArray[i].length < dim) { posArray[i].push(0); } } const newPositions = _kamadaKawaiSolve(distMatrix, posArray, dim); const finalPos = {}; for (let i = 0; i < nNodes; i++) { finalPos[nodesArray[i]] = newPositions[i]; } return rescaleLayout(finalPos, scale, center); } function forceatlas2Layout(G, pos = null, maxIter = 100, jitterTolerance = 1, scalingRatio = 2, gravity = 1, distributedAction = false, strongGravity = false, nodeMass = null, nodeSize = null, weight = null, dissuadeHubs = false, linlog = false, seed = null, dim = 2) { const processed = _processParams(G, null, dim); const graph = processed.G; const nodes = getNodesFromGraph(graph); if (nodes.length === 0) { return {}; } const rng = new RandomNumberGenerator(seed ?? void 0); let posArray; if (pos === null) { pos = {}; posArray = new Array(nodes.length); for (let i = 0; i < nodes.length; i++) { posArray[i] = Array(dim).fill(0).map(() => rng.rand() * 2 - 1); pos[nodes[i]] = posArray[i]; } } else if (Object.keys(pos).length === nodes.length) { posArray = new Array(nodes.length); for (let i = 0; i < nodes.length; i++) { const nodePos = pos[nodes[i]]; posArray[i] = new Array(dim); for (let d = 0; d < dim; d++) { posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1; } } } else { let minPos = Array(dim).fill(Number.POSITIVE_INFINITY); let maxPos = Array(dim).fill(Number.NEGATIVE_INFINITY); for (const node in pos) { const nodePos = pos[node]; for (let d = 0; d < dim; d++) { if (d < nodePos.length) { minPos[d] = Math.min(minPos[d], nodePos[d]); maxPos[d] = Math.max(maxPos[d], nodePos[d]); } } } for (let d = 0; d < dim; d++) { if (!isFinite(minPos[d]) || !isFinite(maxPos[d])) { minPos[d] = -1; maxPos[d] = 1; } } posArray = new Array(nodes.length); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (pos[node]) { const nodePos = pos[node]; posArray[i] = new Array(dim); for (let d = 0; d < dim; d++) { posArray[i][d] = d < nodePos.length ? nodePos[d] : rng.rand() * 2 - 1; } } else { posArray[i] = Array(dim).fill(0).map( (_, d) => minPos[d] + rng.rand() * (maxPos[d] - minPos[d]) ); pos[node] = posArray[i]; } } } const mass = new Array(nodes.length).fill(0); const size = new Array(nodes.length).fill(0); const adjustSizes = nodeSize !== null; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; mass[i] = nodeMass && nodeMass[node] ? nodeMass[node] : Array.isArray(graph) ? 1 : getNodeDegree(graph, node) + 1; size[i] = nodeSize && nodeSize[node] ? nodeSize[node] : 1; } const n = nodes.length; const A = Array(n).fill(0).map(() => Array(n).fill(0)); const edges = Array.isArray(graph) ? [] : graph.edges(); const nodeIndices = {}; nodes.forEach((node, i) => { nodeIndices[node] = i; }); for (const [source, target] of edges) { const i = nodeIndices[source]; const j = nodeIndices[target]; let edgeWeight = 1; if (weight && !Array.isArray(graph) && graph.getEdgeData) { edgeWeight = graph.getEdgeData(source, target, weight) || 1; } A[i][j] = edgeWeight; A[j][i] = edgeWeight; } const gravities = Array(n).fill(0).map(() => Array(dim).fill(0)); const attraction = Array(n).fill(0).map(() => Array(dim).fill(0)); const repulsion = Array(n).fill(0).map(() => Array(dim).fill(0)); let speed = 1; let speedEfficiency = 1; function estimateFactor(n2, swing2, traction2, speed2, speedEfficiency2, jitterTolerance2) { const optJitter = 0.05 * Math.sqrt(n2); const minJitter = Math.sqrt(optJitter); const maxJitter = 10; const minSpeedEfficiency = 0.05; const other = Math.min(maxJitter, optJitter * traction2 / (n2 * n2)); let jitter = jitterTolerance2 * Math.max(minJitter, other); if (swing2 / traction2 > 2) { if (speedEfficiency2 > minSpeedEfficiency) { speedEfficiency2 *= 0.5; } jitter = Math.max(jitter, jitterTolerance2); } let targetSpeed = swing2 === 0 ? Number.POSITIVE_INFINITY : jitter * speedEfficiency2 * traction2 / swing2; if (swing2 > jitter * traction2) { if (speedEfficiency2 > minSpeedEfficiency) { speedEfficiency2 *= 0.7; } } else if (speed2 < 1e3) { speedEfficiency2 *= 1.3; } const maxRise = 0.5; speed2 = speed2 + Math.min(targetSpeed - speed2, maxRise * speed2); return [speed2, speedEfficiency2]; } for (let iter = 0; iter < maxIter; iter++) { for (let i = 0; i < n; i++) { for (let d = 0; d < dim; d++) { attraction[i][d] = 0; repulsion[i][d] = 0; gravities[i][d] = 0; } } const diff = Array(n).fill(0).map( () => Array(n).fill(0).map(() => Array(dim).fill(0)) ); const distance = Array(n).fill(0).map(() => Array(n).fill(0)); for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) continue; for (let d = 0; d < dim; d++) { diff[i][j][d] = posArray[i][d] - posArray[j][d]; } distance[i][j] = Math.sqrt(diff[i][j].reduce((sum, d) => sum + d * d, 0)); if (distance[i][j] < 0.01) distance[i][j] = 0.01; } } if (linlog) { for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j || A[i][j] === 0) continue; const dist = distance[i][j]; const factor = -Math.log(1 + dist) / dist * A[i][j]; for (let d = 0; d < dim; d++) { const force = factor * diff[i][j][d]; attraction[i][d] += force; } } } } else { for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j || A[i][j] === 0) continue; for (let d = 0; d < dim; d++) { const force = -diff[i][j][d] * A[i][j]; attraction[i][d] += force; } } } } if (distributedAction) { for (let i = 0; i < n; i++) { for (let d = 0; d < dim; d++) { attraction[i][d] /= mass[i]; } } } for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) continue; let dist = distance[i][j]; if (adjustSizes) { dist -= size[i] - size[j]; dist = Math.max(dist, 0.01); } const distSquared = dist * dist; const massProduct = mass[i] * mass[j]; const factor = massProduct / distSquared * scalingRatio; for (let d = 0; d < dim; d++) { const direction = diff[i][j][d] / dist; repulsion[i][d] += direction * factor; } } } const centerOfMass = Array(dim).fill(0); for (let i = 0; i < n; i++) { for (let d = 0; d < dim; d++) { centerOfMass[d] += posArray[i][d] / n; } } for (let i = 0; i < n; i++) { const posCentered = Array(dim); for (let d = 0; d < dim; d++) { posCentered[d] = posArray[i][d] - centerOfMass[d]; } if (strongGravity) { for (let d = 0; d < dim; d++) { gravities[i][d] = -gravity * mass[i] * posCentered[d]; } } else { const dist = Math.sqrt(posCentered.reduce((sum, val) => sum + val * val, 0)); if (dist > 0.01) { for (let d = 0; d < dim; d++) { const direction = posCentered[d] / dist; gravities[i][d] = -gravity * mass[i] * direction; } } } } const update = Array(n).fill(0).map(() => Array(dim).fill(0)); let totalSwing = 0; let totalTraction = 0; for (let i = 0; i < n; i++) { for (let d = 0; d < dim; d++) { update[i][d] = attraction[i][d] + repulsion[i][d] + gravities[i][d]; } const oldPos = [...posArray[i]]; const newPos = oldPos.map((p, d) => p + update[i][d]); const swingVector = oldPos.map((p, d) => p - newPos[d]); const tractionVector = oldPos.map((p, d) => p + newPos[d]); const swingMagnitude = Math.sqrt(swingVector.reduce((sum, val) => sum + val * val, 0)); const tractionMagnitude = Math.sqrt(tractionVector.reduce((sum, val) => sum + val * val, 0)); totalSwing += mass[i] * swingMagnitude; totalTraction += 0.5 * mass[i] * tractionMagnitude; } [speed, speedEfficiency] = estimateFactor( n, totalSwing, totalTraction, speed, speedEfficiency, jitterTolerance ); let totalMovement = 0; for (let i = 0; i < n; i++) { let factor; if (adjustSizes) { const df = Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0)); const swinging = mass[i] * df; factor = 0.1 * speed / (1 + Math.sqrt(speed * swinging)); factor = Math.min(factor * df, 10) / df; } else { const swinging = mass[i] * Math.sqrt(update[i].reduce((sum, val) => sum + val * val, 0)); factor = speed / (1 + Math.sqrt(speed * swinging)); } for (let d = 0; d < dim; d++) { const movement = update[i][d] * factor; posArray[i][d] += movement; totalMovement += Math.abs(movement); } } if (totalMovement < 1e-10) { break; } } const positions = {}; for (let i = 0; i < n; i++) { positions[nodes[i]] = posArray[i]; } return rescaleLayout(positions); } function getNodeDegree(graph, node) { return graph.edges().filter( (edge) => edge[0] === node || edge[1] === node ).length; } function arfLayout(G, pos = null, scaling = 1, a = 1.1, maxIter = 1e3, 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 {}; } if (!pos) { pos = randomLayout(G, null, 2, seed); } else { const rng = new RandomNumberGenerator(seed ?? void 0); const defaultPos = {}; nodes.forEach((node) => { if (!pos[node]) { defaultPos[node] = [rng.rand(), rng.rand()]; } }); pos = { ...pos, ...defaultPos }; } const nodeIndex = {}; nodes.forEach((node, i) => { nodeIndex[node] = i; }); const positions = nodes.map((node) => [...pos[node]]); const N = nodes.length; const K = Array(N).fill(0).map(() => Array(N).fill(1)); for (let i = 0; i < N; i++) { K[i][i] = 0; } 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; } const rho = scaling * Math.sqrt(N); const dt = 1e-3; const etol = 1e-6; let error = etol + 1; let nIter = 0; while (error > etol && nIter < maxIter) { 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; const diff = positions[i].map((coord, dim) => coord - positions[j][dim]); const dist = Math.sqrt(diff.reduce((sum, d) => sum + d * d, 0)) || 0.01; for (let d = 0; d < diff.length; d++) { change[i][d] += K[i][j] * diff[d] - rho / dist * diff[d]; } } } for (let i = 0; i < N; i++) { for (let d = 0; d < positions[i].length; d++) { positions[i][d] += change[i][d] * dt; } } error = change.reduce((sum, c) => sum + Math.sqrt(c.reduce((s, v) => s + v * v, 0)), 0); nIter++; } const finalPos = {}; nodes.forEach((node, i) => { finalPos[node] = positions[i]; }); return finalPos; } function bipartiteLayout(G, nodes = null, align = "vertical", scale = 1, center = null, aspectRatio = 4 / 3) { if (align !== "vertical" && align !== "horizontal") { throw new Error("align must be either vertical or horizontal"); } const processed = _processParams(G, center || [0, 0], 2); const graph = processed.G; center = processed.center; const allNodes = getNodesFromGraph(graph); if (allNodes.length === 0) { return {}; } if (!nodes) { nodes = allNodes.filter((_, i) => i % 2 === 0); } const left = new Set(nodes); const right = new Set(allNodes.filter((n) => !left.has(n))); const height = 1; const width = aspectRatio * height; const offset = [width / 2, height / 2]; const pos = {}; const leftNodes = [...left]; leftNodes.forEach((node, i) => { const x = 0; const y = i * height / (leftNodes.length || 1); pos[node] = [x, y]; }); const rightNodes = [...right]; rightNodes.forEach((node, i) => { const x = width; const y = i * height / (rightNodes.length || 1); pos[node] = [x, y]; }); for (const node in pos) { pos[node][0] -= offset[0]; pos[node][1] -= offset[1]; } const scaledPos = rescaleLayout(pos, scale, center); if (align === "horizontal") { for (const node in scaledPos) { const temp = scaledPos[node][0]; scaledPos[node][0] = scaledPos[node][1]; scaledPos[node][1] = temp; } } return scaledPos; } function multipartiteLayout(G, subsetKey = "subset", align = "vertical", scale = 1, center = null) { if (align !== "vertical" && align !== "horizontal") { throw new Error("align must be either vertical or horizontal"); } const processed = _processParams(G, center || [0, 0], 2); const graph = processed.G; center = processed.center; const allNodes = getNodesFromGraph(graph); if (allNodes.length === 0) { return {}; } let layers = {}; if (typeof subsetKey === "string") { console.warn("Using string subsetKey requires node attributes, using default partitioning"); layers = { 0: allNodes }; } else { for (const [key, value] of Object.entries(subsetKey)) { if (Array.isArray(value)) { layers[key] = value; } else { layers[key] = [value]; } } } const layerCount = Object.keys(layers).length; let pos = {}; Object.entries(layers).forEach(([layer, nodes], layerIdx) => { const layerNodes = Array.isArray(nodes) ? nodes : [nodes]; const layerSize = layerNodes.length; layerNodes.forEach((node, nodeIdx) => { const x = layerIdx - (layerCount - 1) / 2; const y = nodeIdx - (layerSize - 1) / 2; pos[node] = [x, y]; }); }); pos = rescaleLayout(pos, scale, center); if (align === "horizontal") { for (const node in pos) { const temp = pos[node][0]; pos[node][0] = pos[node][1]; pos[node][1] = temp; } } return pos; } function bfsLayout(G, start, align = "vertical", scale = 1, center = null) { const processed = _processParams(G, center || [0, 0], 2); if (Array.isArray(processed.G)) { throw new Error("BFS layout requires a Graph with edges, not just a list of nodes"); } const graph = processed.G; center = processed.center; const allNodes = getNodesFromGraph(graph); if (allNodes.length === 0) { return {}; } const layers = {}; const visited = /* @__PURE__ */ new Set(); let currentLayer = 0; layers[currentLayer] = [start]; visited.add(start); while (Object.values(layers).flat().length < allNodes.length) { const nextLayer = []; const currentNodes = layers[currentLayer]; for (const node of currentNodes) { const neighbors = getNeighbors(graph, node); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { nextLayer.push(neighbor); visited.add(neighbor); } } } if (nextLayer.length === 0) { const unvisited = allNodes.filter((node) => !visited.has(node)); if (unvisited.length > 0) { throw new Error("bfs_layout didn't include all nodes. Graph may be disconnected."); } break; } currentLayer++; layers[currentLayer] = nextLayer; } return multipartiteLayout(graph, layers, align, scale, center); } 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) }; } } 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; } const L = Array(N).fill(0).map(() => Array(N).fill(0)); for (let i = 0; i < N; i++) { L[i][i] = A[i].reduce((sum, val) => sum + val, 0); for (let j = 0; j < N; j++) { L[i][j] -= A[i][j]; } } const eigenvectors = []; for (let d = 0; d < dim; d++) { let vector = Array(N).fill(0).map(() => Math.random() - 0.5); 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]); } const norm = Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0)); vector = vector.map((val) => val / norm); for (let iter = 0; iter < 100; iter++) { 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]; } } const mean = newVec.reduce((acc, val) => acc + val, 0) / N; newVec.forEach((val, idx, arr) => { arr[idx] = val - mean; }); const newNorm = Math.sqrt(newVec.reduce((acc, val) => acc + val * val, 0)); if (newNorm < 1e-10) continue; vector = newVec.map((val) => val / newNorm); } eigenvectors.push(vector); } 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]; } } const scaledPositions = rescaleLayout(positions, scale); const pos = {}; nodes.forEach((node, i) => { pos[node] = scaledPositions[i].map((val, j) => val + center[j]); }); return pos; } function isK5(nodes, edges) { if (nodes.length !== 5) return false; if (edges.length !== 10) return false; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const hasEdge = edges.some( (e) => e[0] === nodes[i] && e[1] === nodes[j] || e[0] === nodes[j] && e[1] === nodes[i] ); if (!hasEdge) return false; } } return true; } function isK33(nodes, edges) { if (nodes.length !== 6) return false; if (edges.length !== 9) return false; const nodePartitions = tryFindBipartitePartition(nodes, edges); if (!nodePartitions) return false; const [part1, part2] = nodePartitions; if (part1.length !== 3 || part2.length !== 3) return false; for (const n1 of part1) { for (const n2 of part2) { const hasEdge = edges.some( (e) => e[0] === n1 && e[1] === n2 || e[0] === n2 && e[1] === n1 ); if (!hasEdge) return false; } } return true; } function tryFindBipartitePartition(nodes, edges) { const colorMap = {}; const adjList = {}; for (const node of nodes) { adjList[node] = []; } for (const [u, v] of edges) { adjList[u].push(v); adjList[v].push(u); } const queue = [nodes[0]]; colorMap[nodes[0]] = 0; while (queue.length > 0) { const node = queue.shift(); const nodeColor = colorMap[node]; for (const neighbor of adjList[node]) { if (colorMap[neighbor] === void 0) { colorMap[neighbor] = 1 - nodeColor; queue.push(neighbor); } else if (colorMap[neighbor] === nodeColor) { return null; } } } const part0 = []; const part1 = []; for (const node of nodes) { if (colorMap[node] === 0) { part0.push(node); } else { part1.push(node); } } return [part0, part1]; } function createTriangulationEmbedding(nodes, edges, seed = null) { const rng = new RandomNumberGenerator(seed ?? void 0); const embedding = { nodeOrder: [...nodes], faceList: [], nodePositions: {} }; const adjMap = {}; for (const node of nodes) { adjMap[node] = /* @__PURE__ */ new Set(); } for (const [u, v] of edges) { adjMap[u].add(v); adjMap[v].add(u); } const outerFace = findCycle(nodes, edges, adjMap) || nodes; embedding.faceList.push(outerFace); 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)]; } const interiorNodes = nodes.filter((node) => !embedding.nodePositions[node]); for (const node of interiorNodes) { const neighbors = Array.from(adjMap[node]); if (neighbors.length === 0) { embedding.nodePositions[node] = [0, 0]; } else { 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) { const jitter = 0.1 * rng.rand(); embedding.nodePositions[node] = [ xSum / count + jitter * (rng.rand() - 0.5), ySum / count + jitter * (rng.rand() - 0.5) ]; } else { const r = 0.5 * rng.rand(); const angle = 2 * Math.PI * rng.rand(); embedding.nodePositions[node] = [r * Math.cos(angle), r * Math.sin(angle)]; } } } return embedding; } function findCycle(nodes, edges, adjMap) { if (nodes.length === 0) return null; if (nodes.length <= 2) return nodes; if (nodes.length <= 8) { let hamiltonianCycleDFS = function(node) { path.push(node); visited2.add(node); if (path.length === nodes.length) { if (adjMap[node].has(path[0])) { return true; } visited2.delete(node); path.pop(); return false; } for (const neighbor of adjMap[node]) { if (!visited2.has(neighbor)) { if (hamiltonianCycleDFS(neighbor)) { return true; } } } visited2.delete(node); path.pop(); return false; }; const visited2 = /* @__PURE__ */ new Set(); const path = []; if (hamiltonianCycleDFS(nodes[0])) { return path; } } const visited = /* @__PURE__ */ new Set(); const parent = {}; let cycleFound = null; function findCycleDFS(node, parentNode) { visited.add(node); for (const neighbor of adjMap[node]) { if (neighbor === parentNode) continue; if (visited.has(neighbor)) { cycleFound = constructCycle(node, neighbor, parent); return true; } parent[neighbor] = node; if (findCycleDFS(neighbor, node)) { return true; } } return false; } function constructCycle(u, v, parent2) { const cycle = [v, u]; let current = u; while (parent2[current] !== void 0 && parent2[current] !== v) { current = parent2[current]; cycle.push(current); } return cycle; } for (const node of nodes) { if (!visited.has(node)) { parent[node] = null; if (findCycleDFS(node, null)) { break; } } } return cycleFound || nodes; } function combinatorialEmbeddingToPos(embedding, nodes) { const pos = {}; for (const node of nodes) { if (embedding.nodePositions[node]) { pos[node] = embedding.nodePositions[node]; } else { pos[node] = [0, 0]; } } return pos; } function lrPlanarityTest(nodes, edges, seed = null) { const adjList = {}; for (const node of nodes) { adjList[node] = []; } for (const [u, v] of edges) { adjList[u].push(v); adjList[v].push(u); } const visited = /* @__PURE__ */ new Set(); const ordering = []; function dfs(node) { visited.add(node); ordering.push(node); for (const neighbor of adjList[node]) { if (!visited.has(neighbor)) { dfs(neighbor); } } } dfs(nodes[0]); if (ordering.length < nodes.length) { return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) }; } if (edges.length > 3 * nodes.length - 6) { return { isPlanar: false, embedding: null }; } const embedding = createTriangulationEmbedding(nodes, edges, seed); return { isPlanar: true, embedding }; } function checkPlanarity(G, nodes, edges, seed = null) { if (nodes.length <= 4) { return { isPlanar: true, embedding: createTriangulationEmbedding(nodes, edges, seed) }; } if (isK5(nodes, edges) || isK33(nodes, edges)) { return { isPlanar: false, embedding: null }; } const result = lrPlanarityTest(nodes, edges, seed); return result; } function planarLayout(G, scale = 1, center = null, dim = 2, seed = null) { if (dim !== 2) { throw new Error("can only handle 2 dimensions"); } const processed = _processParams(G, center || [0, 0], dim); if (Array.isArray(processed.G)) { throw new Error("Planar layout requires a Graph with edges, not just a list of nodes"); } const graph = processed.G; center = processed.center; const nodes = getNodesFromGraph(graph); const edges = getEdgesFromGraph(graph); if (nodes.length === 0) { return {}; } const { isPlanar, embedding } = checkPlanarity(graph, nodes, edges, seed); if (!isPlanar) { throw new Error("G is not planar."); } if (!embedding) { throw new Error("Failed to generate planar embedding."); } let pos = combinatorialEmbeddingToPos(embedding, nodes); pos = rescaleLayout(pos, scale, center); return pos; } function completeGraph(n) { const nodes = Array.from({ length: n }, (_, i) => i); const edges = []; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { edges.push([i, j]); } } return { nodes: () => nodes, edges: () => edges }; } function cycleGraph(n) { const nodes = Array.from({ length: n }, (_, i) => i); const edges = []; for (let i = 0; i < n; i++) { edges.push([i, (i + 1) % n]); } return { nodes: () => nodes, edges: () => edges }; } function starGraph(n) { const nodes = Array.from({ length: n }, (_, i) => i); const edges = []; for (let i = 1; i < n; i++) { edges.push([0, i]); } return { nodes: () => nodes, edges: () => edges }; } function wheelGraph(n) { const nodes = Array.from({ length: n }, (_, i) => i); const edges = []; for (let i = 1; i <