@antv/g6
Version:
graph visualization frame work
361 lines (350 loc) • 11.7 kB
JavaScript
/**
* @fileOverview random layout
* @author shiwu.wyy@antfin.com
*/
const Layout = require('../layout');
const Util = require('../../util');
const RadialNonoverlapForce = require('./radialNonoverlapForce');
const MDS = require('./mds');
const isArray = require('@antv/util/lib/type/is-array');
const isNumber = require('@antv/util/lib/type/is-number');
function getWeightMatrix(M) {
const rows = M.length;
const cols = M[0].length;
const result = [];
for (let i = 0; i < rows; i++) {
const row = [];
for (let j = 0; j < cols; j++) {
if (M[i][j] !== 0) row.push(1 / Math.pow(M[i][j], 2));
else row.push(0);
}
result.push(row);
}
return result;
}
function getIndexById(array, id) {
let index = -1;
array.forEach((a, i) => {
if (a.id === id) {
index = i;
return;
}
});
return index;
}
/**
* 随机布局
*/
Layout.registerLayout('radial', {
getDefaultCfg() {
return {
center: [ 0, 0 ], // 布局中心
maxIteration: 1000, // 停止迭代的最大迭代数
focusNode: null, // 中心点,默认为数据中第一个点
unitRadius: null, // 每一圈半径
linkDistance: 50, // 默认边长度
preventOverlap: false, // 是否防止重叠
nodeSize: undefined, // 节点直径
nodeSpacing: undefined, // 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离)
strictRadial: true, // 是否必须是严格的 radial 布局,即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效。
maxPreventOverlapIteration: 200 // 防止重叠步骤的最大迭代次数
};
},
/**
* 执行布局
*/
execute() {
const self = this;
const nodes = self.nodes;
const edges = self.edges;
const center = self.center;
if (nodes.length === 0) {
return;
} else if (nodes.length === 1) {
nodes[0].x = center[0];
nodes[0].y = center[1];
return;
}
const linkDistance = self.linkDistance;
// layout
let focusNode = self.focusNode;
if (Util.isString(focusNode)) {
let found = false;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].id === focusNode) {
focusNode = nodes[i];
self.focusNode = focusNode;
found = true;
i = nodes.length;
}
}
if (!found) focusNode = null;
}
// default focus node
if (!focusNode) {
focusNode = nodes[0];
if (!focusNode) return;
self.focusNode = focusNode;
}
// the index of the focusNode in data
const focusIndex = getIndexById(nodes, focusNode.id);
self.focusIndex = focusIndex;
// the graph-theoretic distance (shortest path distance) matrix
const adjMatrix = Util.getAdjMatrix({ nodes, edges }, false);
const D = Util.floydWarshall(adjMatrix);
const maxDistance = self.maxToFocus(D, focusIndex);
// replace first node in unconnected component to the circle at (maxDistance + 1)
self.handleInfinity(D, focusIndex, (maxDistance + 1));
self.distances = D;
// the shortest path distance from each node to focusNode
const focusNodeD = D[focusIndex];
let width = self.width;
if (!width && typeof window !== 'undefined') {
width = window.innerWidth;
}
let height = self.height;
if (!height && typeof height !== 'undefined') {
height = window.innerHeight;
}
let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0];
let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1];
if (semiWidth === 0) {
semiWidth = width / 2;
}
if (semiHeight === 0) {
semiHeight = height / 2;
}
// the maxRadius of the graph
const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
const maxD = Math.max(...focusNodeD);
// the radius for each nodes away from focusNode
const radii = [];
focusNodeD.forEach((value, i) => {
if (!self.unitRadius) {
self.unitRadius = maxRadius / maxD;
}
radii[i] = value * self.unitRadius;
});
self.radii = radii;
const eIdealD = self.eIdealDisMatrix(D, linkDistance, radii);
// const eIdealD = scaleMatrix(D, linkDistance);
self.eIdealDistances = eIdealD;
// the weight matrix, Wij = 1 / dij^(-2)
const W = getWeightMatrix(eIdealD);
self.weights = W;
// the initial positions from mds
const mds = new MDS({ distances: eIdealD, linkDistance, dimension: 2 });
let positions = mds.layout();
positions.forEach(p => {
if (isNaN(p[0])) p[0] = Math.random() * linkDistance;
if (isNaN(p[1])) p[1] = Math.random() * linkDistance;
});
self.positions = positions;
positions.forEach((p, i) => {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
// move the graph to origin, centered at focusNode
positions.forEach(p => {
p[0] -= positions[focusIndex][0];
p[1] -= positions[focusIndex][1];
});
self.run();
const preventOverlap = self.preventOverlap;
const nodeSize = self.nodeSize;
let nodeSizeFunc;
const strictRadial = self.strictRadial;
// stagger the overlapped nodes
if (preventOverlap) {
const nodeSpacing = self.nodeSpacing;
let nodeSpacingFunc;
if (isNumber(nodeSpacing)) {
nodeSpacingFunc = () => {
return nodeSpacing;
};
} else if (typeof nodeSpacing === 'function') {
nodeSpacingFunc = nodeSpacing;
} else {
nodeSpacingFunc = () => {
return 0;
};
}
if (!nodeSize) {
nodeSizeFunc = d => {
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
return res + nodeSpacingFunc(d);
}
return d.size + nodeSpacingFunc(d);
}
return 10 + nodeSpacingFunc(d);
};
} else {
if (isArray(nodeSize)) {
nodeSizeFunc = d => {
const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
return res + nodeSpacingFunc(d);
};
} else {
nodeSizeFunc = d => {
return nodeSize + nodeSpacingFunc(d);
};
}
}
const nonoverlapForce = new RadialNonoverlapForce({
nodeSizeFunc, adjMatrix, positions, radii, height, width, strictRadial,
focusID: focusIndex,
iterations: self.maxPreventOverlapIteration || 200,
k: positions.length / 4.5,
nodes
});
positions = nonoverlapForce.layout();
}
// move the graph to center
positions.forEach((p, i) => {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
},
run() {
const self = this;
const maxIteration = self.maxIteration;
const positions = self.positions;
const W = self.weights;
const eIdealDis = self.eIdealDistances;
const radii = self.radii;
for (let i = 0; i <= maxIteration; i++) {
const param = i / maxIteration;
self.oneIteration(param, positions, radii, eIdealDis, W);
}
},
oneIteration(param, positions, radii, D, W) {
const self = this;
const vparam = 1 - param;
const focusIndex = self.focusIndex;
positions.forEach((v, i) => { // v
const originDis = Util.getEDistance(v, [ 0, 0 ]);
const reciODis = originDis === 0 ? 0 : 1 / originDis;
if (i === focusIndex) return;
let xMolecule = 0;
let yMolecule = 0;
let denominator = 0;
positions.forEach((u, j) => { // u
if (i === j) return;
// the euclidean distance between v and u
const edis = Util.getEDistance(v, u);
const reciEdis = edis === 0 ? 0 : 1 / edis;
const idealDis = D[j][i];
// same for x and y
denominator += W[i][j];
// x
xMolecule += W[i][j] * (u[0] + idealDis * (v[0] - u[0]) * reciEdis);
// y
yMolecule += W[i][j] * (u[1] + idealDis * (v[1] - u[1]) * reciEdis);
});
const reciR = radii[i] === 0 ? 0 : 1 / radii[i];
denominator *= vparam;
denominator += param * Math.pow(reciR, 2);
// x
xMolecule *= vparam;
xMolecule += param * reciR * v[0] * reciODis;
v[0] = xMolecule / denominator;
// y
yMolecule *= vparam;
yMolecule += param * reciR * v[1] * reciODis;
v[1] = yMolecule / denominator;
});
},
eIdealDisMatrix() {
const self = this;
const D = self.distances;
const linkDis = self.linkDistance;
const radii = self.radii;
const unitRadius = self.unitRadius;
const result = [];
D.forEach((row, i) => {
const newRow = [];
row.forEach((v, j) => {
if (i === j) newRow.push(0);
else if (radii[i] === radii[j]) { // i and j are on the same circle
newRow.push(v * linkDis / (radii[i] / unitRadius));
} else { // i and j are on different circle
const link = (linkDis + unitRadius) / 2;
newRow.push(v * link);
}
});
result.push(newRow);
});
return result;
},
handleAbnormalMatrix(adMatrix, focusIndex) {
const rows = adMatrix.length;
// 空行即代表该行是离散点,将单个离散点看作 focus 的邻居
for (let i = 0; i < rows; i++) {
if (adMatrix[i].length === 0) {
adMatrix[i][focusIndex] = 1;
adMatrix[focusIndex][i] = 1;
}
}
// 如果第一行中有
// let hasDis = true;
// for (let j = 0; j < matrix[focusIndex].length; j++) {
// if (!matrix[focusIndex][j]) hasDis = false;
// }
// if (hasDis) {
// matrix[j][focusIndex] = 1;
// matrix[focusIndex][j] = 1;
// }
// if (emptyMatrix) {
// let value = 0;
// for (let i = 0; i < rows; i++) {
// for (let j = 0; j < rows; j++) {
// if (i === focusIndex || j === focusIndex) value = 1;
// matrix[i][j] = value;
// value = 0;
// }
// value = 0;
// }
// }
},
handleInfinity(matrix, focusIndex, step) {
const length = matrix.length;
// 遍历 matrix 中遍历 focus 对应行
for (let i = 0; i < length; i++) {
// matrix 关注点对应行的 Inf 项
if (matrix[focusIndex][i] === Infinity) {
matrix[focusIndex][i] = step;
matrix[i][focusIndex] = step;
// 遍历 matrix 中的 i 行,i 行中非 Inf 项若在 focus 行为 Inf,则替换 focus 行的那个 Inf
for (let j = 0; j < length; j++) {
if (matrix[i][j] !== Infinity && matrix[focusIndex][j] === Infinity) {
matrix[focusIndex][j] = step + matrix[i][j];
matrix[j][focusIndex] = step + matrix[i][j];
}
}
}
}
// 处理其他行的 Inf。根据该行对应点与 focus 距离以及 Inf 项点 与 focus 距离,决定替换值
for (let i = 0; i < length; i++) {
if (i === focusIndex) {
continue;
}
for (let j = 0; j < length; j++) {
if (matrix[i][j] === Infinity) {
let minus = Math.abs(matrix[focusIndex][i] - matrix[focusIndex][j]);
minus = minus === 0 ? 1 : minus;
matrix[i][j] = minus;
}
}
}
},
maxToFocus(matrix, focusIndex) {
let max = 0;
for (let i = 0; i < matrix[focusIndex].length; i++) {
if (matrix[focusIndex][i] === Infinity) continue;
max = matrix[focusIndex][i] > max ? matrix[focusIndex][i] : max;
}
return max;
}
});