@graphty/layout
Version:
graph layout algorithms based on networkx
1,612 lines • 53.4 kB
JavaScript
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 <