@antv/g6
Version:
graph visualization frame work
415 lines (349 loc) • 12.1 kB
JavaScript
/**
* @fileOverview random layout
* @author shiwu.wyy@antfin.com
*/
var Layout = require('../layout');
var Util = require('../../util');
var RadialNonoverlapForce = require('./radialNonoverlapForce');
var MDS = require('./mds');
var isArray = require('@antv/util/lib/type/is-array');
var isNumber = require('@antv/util/lib/type/is-number');
function getWeightMatrix(M) {
var rows = M.length;
var cols = M[0].length;
var result = [];
for (var i = 0; i < rows; i++) {
var row = [];
for (var 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) {
var index = -1;
array.forEach(function (a, i) {
if (a.id === id) {
index = i;
return;
}
});
return index;
}
/**
* 随机布局
*/
Layout.registerLayout('radial', {
getDefaultCfg: function 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: function execute() {
var self = this;
var nodes = self.nodes;
var edges = self.edges;
var center = self.center;
if (nodes.length === 0) {
return;
} else if (nodes.length === 1) {
nodes[0].x = center[0];
nodes[0].y = center[1];
return;
}
var linkDistance = self.linkDistance; // layout
var focusNode = self.focusNode;
if (Util.isString(focusNode)) {
var found = false;
for (var 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
var focusIndex = getIndexById(nodes, focusNode.id);
self.focusIndex = focusIndex; // the graph-theoretic distance (shortest path distance) matrix
var adjMatrix = Util.getAdjMatrix({
nodes: nodes,
edges: edges
}, false);
var D = Util.floydWarshall(adjMatrix);
var 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
var focusNodeD = D[focusIndex];
var width = self.width;
if (!width && typeof window !== 'undefined') {
width = window.innerWidth;
}
var height = self.height;
if (!height && typeof height !== 'undefined') {
height = window.innerHeight;
}
var semiWidth = width - center[0] > center[0] ? center[0] : width - center[0];
var 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
var maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
var maxD = Math.max.apply(Math, focusNodeD); // the radius for each nodes away from focusNode
var radii = [];
focusNodeD.forEach(function (value, i) {
if (!self.unitRadius) {
self.unitRadius = maxRadius / maxD;
}
radii[i] = value * self.unitRadius;
});
self.radii = radii;
var eIdealD = self.eIdealDisMatrix(D, linkDistance, radii); // const eIdealD = scaleMatrix(D, linkDistance);
self.eIdealDistances = eIdealD; // the weight matrix, Wij = 1 / dij^(-2)
var W = getWeightMatrix(eIdealD);
self.weights = W; // the initial positions from mds
var mds = new MDS({
distances: eIdealD,
linkDistance: linkDistance,
dimension: 2
});
var positions = mds.layout();
positions.forEach(function (p) {
if (isNaN(p[0])) p[0] = Math.random() * linkDistance;
if (isNaN(p[1])) p[1] = Math.random() * linkDistance;
});
self.positions = positions;
positions.forEach(function (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(function (p) {
p[0] -= positions[focusIndex][0];
p[1] -= positions[focusIndex][1];
});
self.run();
var preventOverlap = self.preventOverlap;
var nodeSize = self.nodeSize;
var nodeSizeFunc;
var strictRadial = self.strictRadial; // stagger the overlapped nodes
if (preventOverlap) {
var nodeSpacing = self.nodeSpacing;
var nodeSpacingFunc;
if (isNumber(nodeSpacing)) {
nodeSpacingFunc = function nodeSpacingFunc() {
return nodeSpacing;
};
} else if (typeof nodeSpacing === 'function') {
nodeSpacingFunc = nodeSpacing;
} else {
nodeSpacingFunc = function nodeSpacingFunc() {
return 0;
};
}
if (!nodeSize) {
nodeSizeFunc = function nodeSizeFunc(d) {
if (d.size) {
if (isArray(d.size)) {
var 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 = function nodeSizeFunc(d) {
var res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
return res + nodeSpacingFunc(d);
};
} else {
nodeSizeFunc = function nodeSizeFunc(d) {
return nodeSize + nodeSpacingFunc(d);
};
}
}
var nonoverlapForce = new RadialNonoverlapForce({
nodeSizeFunc: nodeSizeFunc,
adjMatrix: adjMatrix,
positions: positions,
radii: radii,
height: height,
width: width,
strictRadial: strictRadial,
focusID: focusIndex,
iterations: self.maxPreventOverlapIteration || 200,
k: positions.length / 4.5,
nodes: nodes
});
positions = nonoverlapForce.layout();
} // move the graph to center
positions.forEach(function (p, i) {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
},
run: function run() {
var self = this;
var maxIteration = self.maxIteration;
var positions = self.positions;
var W = self.weights;
var eIdealDis = self.eIdealDistances;
var radii = self.radii;
for (var i = 0; i <= maxIteration; i++) {
var param = i / maxIteration;
self.oneIteration(param, positions, radii, eIdealDis, W);
}
},
oneIteration: function oneIteration(param, positions, radii, D, W) {
var self = this;
var vparam = 1 - param;
var focusIndex = self.focusIndex;
positions.forEach(function (v, i) {
// v
var originDis = Util.getEDistance(v, [0, 0]);
var reciODis = originDis === 0 ? 0 : 1 / originDis;
if (i === focusIndex) return;
var xMolecule = 0;
var yMolecule = 0;
var denominator = 0;
positions.forEach(function (u, j) {
// u
if (i === j) return; // the euclidean distance between v and u
var edis = Util.getEDistance(v, u);
var reciEdis = edis === 0 ? 0 : 1 / edis;
var 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);
});
var 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: function eIdealDisMatrix() {
var self = this;
var D = self.distances;
var linkDis = self.linkDistance;
var radii = self.radii;
var unitRadius = self.unitRadius;
var result = [];
D.forEach(function (row, i) {
var newRow = [];
row.forEach(function (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
var link = (linkDis + unitRadius) / 2;
newRow.push(v * link);
}
});
result.push(newRow);
});
return result;
},
handleAbnormalMatrix: function handleAbnormalMatrix(adMatrix, focusIndex) {
var rows = adMatrix.length; // 空行即代表该行是离散点,将单个离散点看作 focus 的邻居
for (var 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: function handleInfinity(matrix, focusIndex, step) {
var length = matrix.length; // 遍历 matrix 中遍历 focus 对应行
for (var 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 (var 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 (var _i = 0; _i < length; _i++) {
if (_i === focusIndex) {
continue;
}
for (var _j = 0; _j < length; _j++) {
if (matrix[_i][_j] === Infinity) {
var minus = Math.abs(matrix[focusIndex][_i] - matrix[focusIndex][_j]);
minus = minus === 0 ? 1 : minus;
matrix[_i][_j] = minus;
}
}
}
},
maxToFocus: function maxToFocus(matrix, focusIndex) {
var max = 0;
for (var i = 0; i < matrix[focusIndex].length; i++) {
if (matrix[focusIndex][i] === Infinity) continue;
max = matrix[focusIndex][i] > max ? matrix[focusIndex][i] : max;
}
return max;
}
});