@antv/layout
Version:
graph layout algorithm
128 lines (126 loc) • 5.02 kB
JavaScript
const SPEED_DIVISOR = 800;
const DEFAULTS_LAYOUT_OPTIONS$1 = {
maxIteration: 10,
width: 10,
speed: 100,
gravity: 10,
k: 5,
};
const radialNonoverlapForce = (model, options) => {
const mergedOptions = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS$1), options);
const { maxIteration, width, k, speed = DEFAULTS_LAYOUT_OPTIONS$1.speed, strictRadial, focusNode, radiiMap, nodeSizeFunc, } = mergedOptions;
const earlyStopThreshold = k * 0.002;
const displacements = new Map();
const maxDisplacement = width / 10;
for (let i = 0; i < maxIteration; i++) {
model.forEachNode((node) => {
displacements.set(node.id, { x: 0, y: 0 });
});
// 给重叠的节点增加斥力
getRepulsion(model, displacements, k, radiiMap, nodeSizeFunc);
const avgDisplacement = updatePositions(model, displacements, speed, strictRadial, focusNode, maxDisplacement, radiiMap);
if (avgDisplacement < earlyStopThreshold) {
break;
}
}
};
const getRepulsion = (model, displacements, k, radiiMap, nodeSizeFunc) => {
let i = 0;
model.forEachNode((nodeU) => {
let j = 0;
model.forEachNode((nodeV) => {
if (j <= i) {
j++;
return;
}
if (nodeU.id === nodeV.id)
return;
// nodeU and nodeV are not on the same circle, return
if (radiiMap.get(nodeU.id) !== radiiMap.get(nodeV.id))
return;
let vecx = nodeU.x - nodeV.x;
let vecy = nodeU.y - nodeV.y;
let vecLength = Math.sqrt(vecx * vecx + vecy * vecy);
// If the two nodes are exactly at the same position, we set a small distance between them
if (vecLength === 0) {
vecLength = 1;
const sign = i > j ? 1 : -1;
vecx = 0.01 * sign;
vecy = 0.01 * sign;
}
const nodeSizeU = Math.max(...nodeSizeFunc(nodeU._original));
const nodeSizeV = Math.max(...nodeSizeFunc(nodeV._original));
// these two nodes overlap
if (vecLength < nodeSizeV / 2 + nodeSizeU / 2) {
const common = (k * k) / vecLength;
const dispU = displacements.get(nodeU.id);
const dispV = displacements.get(nodeV.id);
const deltaX = (vecx / vecLength) * common;
const deltaY = (vecy / vecLength) * common;
displacements.set(nodeU.id, {
x: dispU.x + deltaX,
y: dispU.y + deltaY,
});
displacements.set(nodeV.id, {
x: dispV.x - deltaX,
y: dispV.y - deltaY,
});
}
j++;
});
i++;
});
};
const updatePositions = (model, displacements, speed, strictRadial, focusNode, maxDisplacement, radiiMap) => {
if (strictRadial) {
model.forEachNode((node) => {
const vx = node.x - focusNode.x;
const vy = node.y - focusNode.y;
const vLength = Math.sqrt(vx * vx + vy * vy);
let vpx = vy / vLength;
let vpy = -vx / vLength;
const disp = displacements.get(node.id);
const diLength = Math.sqrt(disp.x * disp.x + disp.y * disp.y);
let alpha = Math.acos((vpx * disp.x + vpy * disp.y) / diLength);
if (alpha > Math.PI / 2) {
alpha -= Math.PI / 2;
vpx *= -1;
vpy *= -1;
}
const tdispLength = Math.cos(alpha) * diLength;
displacements.set(node.id, {
x: vpx * tdispLength,
y: vpy * tdispLength,
});
});
}
// move
let totalDisplacement = 0;
let nodeCount = 0;
model.forEachNode((node) => {
if (node.id === focusNode.id) {
return;
}
const disp = displacements.get(node.id);
const distLength = Math.sqrt(disp.x * disp.x + disp.y * disp.y);
if (distLength > 0) {
const limitedDist = Math.min(maxDisplacement * (speed / SPEED_DIVISOR), distLength);
node.x += (disp.x / distLength) * limitedDist;
node.y += (disp.y / distLength) * limitedDist;
if (strictRadial) {
let vx = node.x - focusNode.x;
let vy = node.y - focusNode.y;
const nfDis = Math.sqrt(vx * vx + vy * vy);
vx = (vx / nfDis) * radiiMap.get(node.id);
vy = (vy / nfDis) * radiiMap.get(node.id);
node.x = focusNode.x + vx;
node.y = focusNode.y + vy;
}
totalDisplacement += limitedDist;
nodeCount++;
}
});
return nodeCount > 0 ? totalDisplacement / nodeCount : 0;
};
export { radialNonoverlapForce };
//# sourceMappingURL=radial-nonoverlap-force.js.map