You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
14 KiB
346 lines
14 KiB
|
4 months ago
|
import { __awaiter } from "tslib";
|
||
|
|
import { Graph as IGraph } from '@antv/graphlib';
|
||
|
|
import { isNumber } from '@antv/util';
|
||
|
|
import { cloneFormatData } from './util';
|
||
|
|
const DEFAULTS_LAYOUT_OPTIONS = {
|
||
|
|
maxIteration: 1000,
|
||
|
|
gravity: 10,
|
||
|
|
speed: 5,
|
||
|
|
clustering: false,
|
||
|
|
clusterGravity: 10,
|
||
|
|
width: 300,
|
||
|
|
height: 300,
|
||
|
|
nodeClusterBy: 'cluster',
|
||
|
|
};
|
||
|
|
const SPEED_DIVISOR = 800;
|
||
|
|
/**
|
||
|
|
* <zh/> Fruchterman 力导向布局
|
||
|
|
*
|
||
|
|
* <en/> Fruchterman force-directed layout
|
||
|
|
*/
|
||
|
|
export class FruchtermanLayout {
|
||
|
|
constructor(options = {}) {
|
||
|
|
this.options = options;
|
||
|
|
this.id = 'fruchterman';
|
||
|
|
this.timeInterval = 0;
|
||
|
|
this.running = false;
|
||
|
|
this.options = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS), options);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Return the positions of nodes and edges(if needed).
|
||
|
|
*/
|
||
|
|
execute(graph, options) {
|
||
|
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
|
return this.genericFruchtermanLayout(false, graph, options);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* To directly assign the positions to the nodes.
|
||
|
|
*/
|
||
|
|
assign(graph, options) {
|
||
|
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
|
yield this.genericFruchtermanLayout(true, graph, options);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Stop simulation immediately.
|
||
|
|
*/
|
||
|
|
stop() {
|
||
|
|
if (this.timeInterval && typeof window !== 'undefined') {
|
||
|
|
window.clearInterval(this.timeInterval);
|
||
|
|
}
|
||
|
|
this.running = false;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Manually steps the simulation by the specified number of iterations.
|
||
|
|
* @see https://github.com/d3/d3-force#simulation_tick
|
||
|
|
*/
|
||
|
|
tick(iterations = this.options.maxIteration || 1) {
|
||
|
|
if (this.lastResult) {
|
||
|
|
return this.lastResult;
|
||
|
|
}
|
||
|
|
for (let i = 0; i < iterations; i++) {
|
||
|
|
this.runOneStep(this.lastGraph, this.lastClusterMap, this.lastOptions);
|
||
|
|
}
|
||
|
|
const result = {
|
||
|
|
nodes: this.lastLayoutNodes,
|
||
|
|
edges: this.lastLayoutEdges,
|
||
|
|
};
|
||
|
|
if (this.lastAssign) {
|
||
|
|
result.nodes.forEach((node) => this.lastGraph.mergeNodeData(node.id, {
|
||
|
|
x: node.data.x,
|
||
|
|
y: node.data.y,
|
||
|
|
z: this.options.dimensions === 3 ? node.data.z : undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
genericFruchtermanLayout(assign, graph, options) {
|
||
|
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
|
if (this.running)
|
||
|
|
return;
|
||
|
|
const formattedOptions = this.formatOptions(options);
|
||
|
|
const { dimensions, width, height, center, clustering, nodeClusterBy, maxIteration, onTick, } = formattedOptions;
|
||
|
|
const nodes = graph.getAllNodes();
|
||
|
|
const edges = graph.getAllEdges();
|
||
|
|
if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length)) {
|
||
|
|
const result = { nodes: [], edges };
|
||
|
|
this.lastResult = result;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
if (nodes.length === 1) {
|
||
|
|
if (assign) {
|
||
|
|
graph.mergeNodeData(nodes[0].id, {
|
||
|
|
x: center[0],
|
||
|
|
y: center[1],
|
||
|
|
z: dimensions === 3 ? center[2] : undefined,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const result = {
|
||
|
|
nodes: [
|
||
|
|
Object.assign(Object.assign({}, nodes[0]), { data: Object.assign(Object.assign({}, nodes[0].data), { x: center[0], y: center[1], z: dimensions === 3 ? center[2] : undefined }) }),
|
||
|
|
],
|
||
|
|
edges,
|
||
|
|
};
|
||
|
|
this.lastResult = result;
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
const layoutNodes = nodes.map((node) => cloneFormatData(node, [width, height]));
|
||
|
|
const calcGraph = new IGraph({
|
||
|
|
nodes: layoutNodes,
|
||
|
|
edges,
|
||
|
|
});
|
||
|
|
// clustering info
|
||
|
|
const clusterMap = {};
|
||
|
|
if (clustering) {
|
||
|
|
layoutNodes.forEach((node) => {
|
||
|
|
const clusterValue = node.data[nodeClusterBy];
|
||
|
|
if (!clusterMap[clusterValue]) {
|
||
|
|
clusterMap[clusterValue] = {
|
||
|
|
name: clusterValue,
|
||
|
|
cx: 0,
|
||
|
|
cy: 0,
|
||
|
|
count: 0,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// Use them later in `tick`.
|
||
|
|
this.lastLayoutNodes = layoutNodes;
|
||
|
|
this.lastLayoutEdges = edges;
|
||
|
|
this.lastAssign = assign;
|
||
|
|
this.lastGraph = calcGraph;
|
||
|
|
this.lastOptions = formattedOptions;
|
||
|
|
this.lastClusterMap = clusterMap;
|
||
|
|
if (typeof window === 'undefined')
|
||
|
|
return;
|
||
|
|
let iter = 0;
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
// interval for render the result after each iteration
|
||
|
|
this.timeInterval = window.setInterval(() => {
|
||
|
|
if (!this.running) {
|
||
|
|
resolve({ nodes: layoutNodes, edges });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.runOneStep(calcGraph, clusterMap, formattedOptions);
|
||
|
|
if (assign) {
|
||
|
|
layoutNodes.forEach(({ id, data }) => graph.mergeNodeData(id, {
|
||
|
|
x: data.x,
|
||
|
|
y: data.y,
|
||
|
|
z: dimensions === 3 ? data.z : undefined,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
onTick === null || onTick === void 0 ? void 0 : onTick({
|
||
|
|
nodes: layoutNodes,
|
||
|
|
edges,
|
||
|
|
});
|
||
|
|
iter++;
|
||
|
|
if (iter >= maxIteration) {
|
||
|
|
window.clearInterval(this.timeInterval);
|
||
|
|
resolve({ nodes: layoutNodes, edges });
|
||
|
|
}
|
||
|
|
}, 0);
|
||
|
|
this.running = true;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
formatOptions(options = {}) {
|
||
|
|
const mergedOptions = Object.assign(Object.assign({}, this.options), options);
|
||
|
|
const { clustering, nodeClusterBy } = mergedOptions;
|
||
|
|
const { center: propsCenter, width: propsWidth, height: propsHeight, } = mergedOptions;
|
||
|
|
mergedOptions.width =
|
||
|
|
!propsWidth && typeof window !== 'undefined'
|
||
|
|
? window.innerWidth
|
||
|
|
: propsWidth;
|
||
|
|
mergedOptions.height =
|
||
|
|
!propsHeight && typeof window !== 'undefined'
|
||
|
|
? window.innerHeight
|
||
|
|
: propsHeight;
|
||
|
|
mergedOptions.center = !propsCenter
|
||
|
|
? [mergedOptions.width / 2, mergedOptions.height / 2]
|
||
|
|
: propsCenter;
|
||
|
|
mergedOptions.clustering = clustering && !!nodeClusterBy;
|
||
|
|
return mergedOptions;
|
||
|
|
}
|
||
|
|
runOneStep(calcGraph, clusterMap, options) {
|
||
|
|
const { dimensions, height, width, gravity, center, speed, clustering, nodeClusterBy, clusterGravity: propsClusterGravity, } = options;
|
||
|
|
const area = height * width;
|
||
|
|
const maxDisplace = Math.sqrt(area) / 10;
|
||
|
|
const nodes = calcGraph.getAllNodes();
|
||
|
|
const k2 = area / (nodes.length + 1);
|
||
|
|
const k = Math.sqrt(k2);
|
||
|
|
const displacements = {};
|
||
|
|
this.applyCalculate(calcGraph, displacements, k, k2);
|
||
|
|
// gravity for clusters
|
||
|
|
if (clustering) {
|
||
|
|
// reset the clustering centers
|
||
|
|
for (const key in clusterMap) {
|
||
|
|
clusterMap[key].cx = 0;
|
||
|
|
clusterMap[key].cy = 0;
|
||
|
|
clusterMap[key].count = 0;
|
||
|
|
}
|
||
|
|
// re-compute clustering centers
|
||
|
|
nodes.forEach((node) => {
|
||
|
|
const { data } = node; // node is one of layoutNodes, which is formatted and data field exists
|
||
|
|
const c = clusterMap[data[nodeClusterBy]];
|
||
|
|
if (isNumber(data.x)) {
|
||
|
|
c.cx += data.x;
|
||
|
|
}
|
||
|
|
if (isNumber(data.y)) {
|
||
|
|
c.cy += data.y;
|
||
|
|
}
|
||
|
|
c.count++;
|
||
|
|
});
|
||
|
|
for (const key in clusterMap) {
|
||
|
|
clusterMap[key].cx /= clusterMap[key].count;
|
||
|
|
clusterMap[key].cy /= clusterMap[key].count;
|
||
|
|
}
|
||
|
|
// compute the cluster gravity forces
|
||
|
|
const clusterGravity = (propsClusterGravity || gravity);
|
||
|
|
nodes.forEach((node, j) => {
|
||
|
|
const { id, data } = node;
|
||
|
|
if (!isNumber(data.x) || !isNumber(data.y))
|
||
|
|
return;
|
||
|
|
const c = clusterMap[data[nodeClusterBy]];
|
||
|
|
const distLength = Math.sqrt((data.x - c.cx) * (data.x - c.cx) + (data.y - c.cy) * (data.y - c.cy));
|
||
|
|
const gravityForce = k * clusterGravity;
|
||
|
|
displacements[id].x -= (gravityForce * (data.x - c.cx)) / distLength;
|
||
|
|
displacements[id].y -= (gravityForce * (data.y - c.cy)) / distLength;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// gravity
|
||
|
|
nodes.forEach((node, j) => {
|
||
|
|
const { id, data } = node;
|
||
|
|
if (!isNumber(data.x) || !isNumber(data.y))
|
||
|
|
return;
|
||
|
|
const gravityForce = 0.01 * k * gravity;
|
||
|
|
displacements[id].x -= gravityForce * (data.x - center[0]);
|
||
|
|
displacements[id].y -= gravityForce * (data.y - center[1]);
|
||
|
|
if (dimensions === 3) {
|
||
|
|
displacements[id].z -= gravityForce * (data.z - center[2]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
// move
|
||
|
|
nodes.forEach((node, j) => {
|
||
|
|
const { id, data } = node;
|
||
|
|
if (isNumber(data.fx) && isNumber(data.fy)) {
|
||
|
|
data.x = data.fx;
|
||
|
|
data.y = data.fy;
|
||
|
|
if (dimensions === 3) {
|
||
|
|
data.z = data.fz;
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!isNumber(data.x) || !isNumber(data.y))
|
||
|
|
return;
|
||
|
|
const distLength = Math.sqrt(displacements[id].x * displacements[id].x +
|
||
|
|
displacements[id].y * displacements[id].y +
|
||
|
|
(dimensions === 3 ? displacements[id].z * displacements[id].z : 0));
|
||
|
|
if (distLength > 0) {
|
||
|
|
// && !n.isFixed()
|
||
|
|
const limitedDist = Math.min(maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||
|
|
calcGraph.mergeNodeData(id, {
|
||
|
|
x: data.x + (displacements[id].x / distLength) * limitedDist,
|
||
|
|
y: data.y + (displacements[id].y / distLength) * limitedDist,
|
||
|
|
z: dimensions === 3
|
||
|
|
? data.z + (displacements[id].z / distLength) * limitedDist
|
||
|
|
: undefined,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
applyCalculate(calcGraph, displacements, k, k2) {
|
||
|
|
this.calRepulsive(calcGraph, displacements, k2);
|
||
|
|
this.calAttractive(calcGraph, displacements, k);
|
||
|
|
}
|
||
|
|
calRepulsive(calcGraph, displacements, k2) {
|
||
|
|
const nodes = calcGraph.getAllNodes();
|
||
|
|
nodes.forEach(({ data: v, id: vid }, i) => {
|
||
|
|
displacements[vid] = { x: 0, y: 0, z: 0 };
|
||
|
|
nodes.forEach(({ data: u, id: uid }, j) => {
|
||
|
|
if (i <= j ||
|
||
|
|
!isNumber(v.x) ||
|
||
|
|
!isNumber(u.x) ||
|
||
|
|
!isNumber(v.y) ||
|
||
|
|
!isNumber(u.y)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let vecX = v.x - u.x;
|
||
|
|
let vecY = v.y - u.y;
|
||
|
|
let vecZ = this.options.dimensions === 3 ? v.z - u.z : 0;
|
||
|
|
let lengthSqr = vecX * vecX + vecY * vecY + vecZ * vecZ;
|
||
|
|
if (lengthSqr === 0) {
|
||
|
|
lengthSqr = 1;
|
||
|
|
vecX = 0.01;
|
||
|
|
vecY = 0.01;
|
||
|
|
vecZ = 0.01;
|
||
|
|
}
|
||
|
|
const common = k2 / lengthSqr;
|
||
|
|
const dispX = vecX * common;
|
||
|
|
const dispY = vecY * common;
|
||
|
|
const dispZ = vecZ * common;
|
||
|
|
displacements[vid].x += dispX;
|
||
|
|
displacements[vid].y += dispY;
|
||
|
|
displacements[uid].x -= dispX;
|
||
|
|
displacements[uid].y -= dispY;
|
||
|
|
if (this.options.dimensions === 3) {
|
||
|
|
displacements[vid].z += dispZ;
|
||
|
|
displacements[uid].z -= dispZ;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
calAttractive(calcGraph, displacements, k) {
|
||
|
|
const edges = calcGraph.getAllEdges();
|
||
|
|
edges.forEach((e) => {
|
||
|
|
const { source, target } = e;
|
||
|
|
if (!source || !target || source === target) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const { data: u } = calcGraph.getNode(source);
|
||
|
|
const { data: v } = calcGraph.getNode(target);
|
||
|
|
if (!isNumber(v.x) ||
|
||
|
|
!isNumber(u.x) ||
|
||
|
|
!isNumber(v.y) ||
|
||
|
|
!isNumber(u.y)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const vecX = v.x - u.x;
|
||
|
|
const vecY = v.y - u.y;
|
||
|
|
const vecZ = this.options.dimensions === 3 ? v.z - u.z : 0;
|
||
|
|
const common = Math.sqrt(vecX * vecX + vecY * vecY + vecZ * vecZ) / k;
|
||
|
|
const dispX = vecX * common;
|
||
|
|
const dispY = vecY * common;
|
||
|
|
const dispZ = vecZ * common;
|
||
|
|
displacements[source].x += dispX;
|
||
|
|
displacements[source].y += dispY;
|
||
|
|
displacements[target].x -= dispX;
|
||
|
|
displacements[target].y -= dispY;
|
||
|
|
if (this.options.dimensions === 3) {
|
||
|
|
displacements[source].z += dispZ;
|
||
|
|
displacements[target].z -= dispZ;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=fruchterman.js.map
|