mirror of
https://gitee.com/antv/g6.git
synced 2024-12-01 03:08:33 +08:00
feat: layout for combo. feat: nonoverlap for combo layout in first stage.
This commit is contained in:
parent
811f5bc18c
commit
4631708709
@ -142,7 +142,7 @@ export default class LayoutController {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.layoutType === 'force') {
|
||||
if (this.layoutType === 'force' || this.layoutType === 'g6force') {
|
||||
const { onTick } = layoutCfg;
|
||||
const tick = () => {
|
||||
if (onTick) {
|
||||
@ -158,6 +158,8 @@ export default class LayoutController {
|
||||
}
|
||||
graph.emit('afterlayout');
|
||||
};
|
||||
} else if (this.layoutType === 'comboForce') {
|
||||
layoutCfg.comboTrees = graph.get('comboTrees');
|
||||
}
|
||||
|
||||
if (this.layoutType !== undefined) {
|
||||
@ -354,8 +356,10 @@ export default class LayoutController {
|
||||
public setDataFromGraph() {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const combos = [];
|
||||
const nodeItems = this.graph.getNodes();
|
||||
const edgeItems = this.graph.getEdges();
|
||||
const comboItems = this.graph.getCombos();
|
||||
nodeItems.forEach(nodeItem => {
|
||||
const model = nodeItem.getModel();
|
||||
nodes.push(model);
|
||||
@ -364,7 +368,11 @@ export default class LayoutController {
|
||||
const model = edgeItem.getModel();
|
||||
edges.push(model);
|
||||
});
|
||||
const data: any = { nodes, edges };
|
||||
comboItems.forEach(comboItem => {
|
||||
const model = comboItem.getModel();
|
||||
combos.push(model);
|
||||
});
|
||||
const data: any = { nodes, edges, combos };
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -983,12 +983,28 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
|
||||
// process the data to tree structure
|
||||
if (combos) {
|
||||
const comboTrees = plainCombosToTrees(combos, nodes);
|
||||
const comboTrees = plainCombosToTrees(combos, self.getNodes());
|
||||
this.set('comboTrees', comboTrees);
|
||||
// add combos
|
||||
self.addCombos(combos);
|
||||
}
|
||||
|
||||
// layout
|
||||
const layoutController = self.get('layoutController');
|
||||
if (!layoutController.layout(success)) {
|
||||
success();
|
||||
}
|
||||
function success() {
|
||||
if (combos) {
|
||||
self.updateCombos();
|
||||
}
|
||||
if (self.get('fitView')) {
|
||||
self.fitView();
|
||||
}
|
||||
self.autoPaint();
|
||||
self.emit('afterrender');
|
||||
}
|
||||
|
||||
if (!this.get('groupByTypes')) {
|
||||
if (combos) {
|
||||
this.sortCombos(data);
|
||||
@ -1012,23 +1028,6 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
}
|
||||
}
|
||||
|
||||
// layout
|
||||
const animate = self.get('animate');
|
||||
self.set('animate', false);
|
||||
const layoutController = self.get('layoutController');
|
||||
if (!layoutController.layout(success)) {
|
||||
success();
|
||||
}
|
||||
|
||||
function success() {
|
||||
self.set('animate', animate);
|
||||
if (self.get('fitView')) {
|
||||
self.fitView();
|
||||
}
|
||||
self.autoPaint();
|
||||
self.emit('afterrender');
|
||||
}
|
||||
|
||||
// 防止传入的数据不存在nodes
|
||||
if (data.nodes) {
|
||||
// 获取所有有groupID的node
|
||||
@ -1134,7 +1133,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
// process the data to tree structure
|
||||
const combosData = (data as GraphData).combos;
|
||||
if (combosData) {
|
||||
const comboTrees = plainCombosToTrees(combosData, (data as GraphData).nodes);
|
||||
const comboTrees = plainCombosToTrees(combosData, self.getNodes());
|
||||
this.set('comboTrees', comboTrees);
|
||||
// add combos
|
||||
self.addCombos(combosData);
|
||||
@ -1221,6 +1220,27 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据节点的 bbox 更新 combos 的绘制,包括 combos 的位置和范围
|
||||
*/
|
||||
private updateCombos() {
|
||||
const self = this;
|
||||
const comboTrees = this.get('comboTrees');
|
||||
const itemController: ItemController = self.get('itemController');
|
||||
|
||||
const itemMap = self.get('itemMap');
|
||||
comboTrees && comboTrees.forEach((ctree: ComboTree) => {
|
||||
traverseTreeUp<ComboTree>(ctree, child => {
|
||||
const childItem = itemMap[child.id];
|
||||
if (childItem && childItem.getType() === 'combo') {
|
||||
itemController.updateCombo(childItem, child.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
self.sortCombos(self.get('data'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据渲染群组
|
||||
* @param {GraphData} data 渲染图的数据
|
||||
@ -1359,7 +1379,6 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
return this.get('combos')
|
||||
}
|
||||
|
||||
// TODO 待实现getComboNodes方法
|
||||
/**
|
||||
* 获取指定 Combo 中所有的节点
|
||||
* @param comboId combo ID
|
||||
|
589
src/layout/comboForce.ts
Normal file
589
src/layout/comboForce.ts
Normal file
@ -0,0 +1,589 @@
|
||||
/**
|
||||
* @fileOverview Combo force layout
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap, ComboTree, ComboConfig } from '../types';
|
||||
import { BaseLayout } from './layout';
|
||||
import { isNumber, isArray, isFunction, clone } from '@antv/util';
|
||||
import { Point } from '@antv/g-base';
|
||||
import { traverseTreeUp } from '../util/graphic';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
type elementMap = {
|
||||
[key: string]: Node | ComboConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* force layout for graph with combos
|
||||
*/
|
||||
export default class ComboForce extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 停止迭代的最大迭代数 */
|
||||
public maxIteration: number = 500;
|
||||
/** 重力大小,影响图的紧凑程度 */
|
||||
public gravity: number = 10;
|
||||
/** 群组中心力大小 */
|
||||
public comboGravity: number = 10;
|
||||
/** 默认边长度 */
|
||||
public linkDistance: number | ((d?: unknown) => number) = 50;
|
||||
/** 每次迭代位移的衰减相关参数 */
|
||||
public alpha: number = 1;
|
||||
public alphaMin: number = 0.001;
|
||||
public alphaDecay: number = 1 - Math.pow(this.alphaMin, 1 / 300);
|
||||
public alphaTarget: number = 0;
|
||||
/** 节点运动速度衰减参数 */
|
||||
public velocityDecay: number = 0.6;
|
||||
/** 边引力大小 */
|
||||
public linkStrength: number | ((d?: unknown) => number) = 0.1;
|
||||
/** 节点引力大小 */
|
||||
public nodeStrength: number | ((d?: unknown) => number) = 30;
|
||||
/** 是否开启防止重叠 */
|
||||
public preventOverlap: boolean = false;
|
||||
/** 是否开启节点之间的防止重叠 */
|
||||
public preventNodeOverlap: boolean = false;
|
||||
/** 是否开启 Combo 之间的防止重叠 */
|
||||
public preventComboOverlap: boolean = false;
|
||||
/** 防止重叠的碰撞力大小 */
|
||||
public collideStrength: number = 1;
|
||||
/** 节点大小,用于防止重叠 */
|
||||
public nodeSize: number | number[] | ((d?: unknown) => number) | undefined;
|
||||
/** 节点最小间距,防止重叠时的间隙 */
|
||||
public nodeSpacing: ((d?: unknown) => number) | undefined;
|
||||
/** Combo 大小,用于防止重叠 */
|
||||
public comboSize: number | number[] | ((d?: unknown) => number) | undefined;
|
||||
/** Combo 最小间距,防止重叠时的间隙 */
|
||||
public comboSpacing: ((d?: unknown) => number) | undefined;
|
||||
/** 优化计算斥力的速度,两节点间距超过 optimizeRangeFactor * width 则不再计算斥力和重叠斥力 */
|
||||
public optimizeRangeFactor: number = 1;
|
||||
/** 每次迭代的回调函数 */
|
||||
public tick: () => void = () => {};
|
||||
/** 根据边两端节点层级差距的调整函数 */
|
||||
public depthAttractiveForceScale: number = 0.3; // ((d?: unknown) => number);
|
||||
public depthRepulsiveForceScale: number = 2; // ((d?: unknown) => number);
|
||||
|
||||
/** 内部计算参数 */
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
public combos: ComboConfig[] = [];
|
||||
private comboTrees: ComboTree[] = [];
|
||||
private width: number = 300;
|
||||
private height: number = 300;
|
||||
private bias: number[] = [];
|
||||
private nodeMap: elementMap = {};
|
||||
private oriComboMap: elementMap = {};
|
||||
private nodeIdxMap: NodeIdxMap = {};
|
||||
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
maxIteration: 1000,
|
||||
center: [0, 0],
|
||||
gravity: 10,
|
||||
speed: 1,
|
||||
comboGravity: 10,
|
||||
preventOverlap: false,
|
||||
nodeSpacing: undefined,
|
||||
collideStrength: 10
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 执行布局
|
||||
*/
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const combos = self.combos;
|
||||
const center = self.center;
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (nodes.length === 1) {
|
||||
nodes[0].x = center[0];
|
||||
nodes[0].y = center[1];
|
||||
return;
|
||||
}
|
||||
const nodeMap: elementMap = {};
|
||||
const nodeIdxMap: NodeIdxMap = {};
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = node;
|
||||
nodeIdxMap[node.id] = i;
|
||||
});
|
||||
self.nodeMap = nodeMap;
|
||||
self.nodeIdxMap = nodeIdxMap;
|
||||
|
||||
const oriComboMap: elementMap = {};
|
||||
combos.forEach(combo => {
|
||||
oriComboMap[combo.id] = combo;
|
||||
});
|
||||
self.oriComboMap = oriComboMap;
|
||||
// layout
|
||||
self.run();
|
||||
}
|
||||
|
||||
public run() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const maxIteration = self.maxIteration;
|
||||
if (!self.width && typeof window !== 'undefined') {
|
||||
self.width = window.innerWidth;
|
||||
}
|
||||
if (!self.height && typeof window !== 'undefined') {
|
||||
self.height = window.innerHeight;
|
||||
}
|
||||
const center = self.center;
|
||||
const velocityDecay = self.velocityDecay;
|
||||
|
||||
let comboMap = self.getComboMap();
|
||||
self.initVals();
|
||||
|
||||
// init positions to make the nodes with same combo gather
|
||||
...
|
||||
|
||||
// iterate
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const displacements: Point[] = [];
|
||||
nodes.forEach((_, j) => {
|
||||
displacements[j] = { x: 0, y: 0 };
|
||||
});
|
||||
self.applyCalculate(comboMap, displacements);
|
||||
|
||||
// gravity for combos
|
||||
self.applyComboCenterForce(comboMap, displacements);
|
||||
|
||||
// move
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
n.x += displacements[j].x * velocityDecay;
|
||||
n.y += displacements[j].y * velocityDecay;
|
||||
});
|
||||
this.alpha += (this.alphaTarget - this.alpha) * this.alphaDecay;
|
||||
self.tick();
|
||||
}
|
||||
|
||||
// move to center
|
||||
const meanCenter = [ 0, 0 ];
|
||||
nodes.forEach(n => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
meanCenter[0] += n.x;
|
||||
meanCenter[1] += n.y;
|
||||
});
|
||||
meanCenter[0] /= nodes.length;
|
||||
meanCenter[1] /= nodes.length;
|
||||
const centerOffset = [ center[0] - meanCenter[0], center[1] - meanCenter[1] ];
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
n.x += centerOffset[0];
|
||||
n.y += centerOffset[1];
|
||||
});
|
||||
}
|
||||
|
||||
private initVals() {
|
||||
const self = this;
|
||||
const edges = self.edges;
|
||||
const count = {};
|
||||
|
||||
// get edge bias
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
if (count[edges[i].source]) count[edges[i].source] ++;
|
||||
else count[edges[i].source] = 1;
|
||||
if (count[edges[i].target]) count[edges[i].target] ++;
|
||||
else count[edges[i].target] = 1;
|
||||
}
|
||||
const bias = [];
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
bias[i] = count[edges[i].source] / (count[edges[i].source] + count[edges[i].target]);
|
||||
}
|
||||
this.bias = bias;
|
||||
|
||||
const nodeSize = self.nodeSize;
|
||||
const nodeSpacing = self.nodeSpacing;
|
||||
let nodeSizeFunc: (d: any) => number;
|
||||
let nodeSpacingFunc: (d: any) => number;
|
||||
|
||||
// nodeSpacing to function
|
||||
if (isNumber(nodeSpacing)) {
|
||||
nodeSpacingFunc = () => nodeSpacing;
|
||||
} else if (isFunction(nodeSpacing)) {
|
||||
nodeSpacingFunc = nodeSpacing;
|
||||
} else {
|
||||
nodeSpacingFunc = () => 0;
|
||||
}
|
||||
|
||||
// nodeSize to function
|
||||
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 / 2 + nodeSpacingFunc(d);
|
||||
}
|
||||
return d.size / 2 + nodeSpacingFunc(d);
|
||||
}
|
||||
return 10 + nodeSpacingFunc(d);
|
||||
};
|
||||
} else if (isFunction(nodeSize)) {
|
||||
nodeSizeFunc = d => {
|
||||
const size = nodeSize(d);
|
||||
return size + nodeSpacingFunc(d);
|
||||
};
|
||||
} else if (isArray(nodeSize)) {
|
||||
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
|
||||
const radius = larger / 2;
|
||||
nodeSizeFunc = d => radius + nodeSpacingFunc(d);
|
||||
} else if (isNumber(nodeSize)) {
|
||||
const radius = nodeSize / 2;
|
||||
nodeSizeFunc = d => radius + nodeSpacingFunc(d);
|
||||
} else {
|
||||
nodeSizeFunc = () => 10;
|
||||
}
|
||||
this.nodeSize = nodeSizeFunc;
|
||||
|
||||
|
||||
// comboSpacing to function
|
||||
const comboSpacing = self.comboSpacing;
|
||||
let comboSpacingFunc: (d: any) => number;
|
||||
if (isNumber(comboSpacing)) {
|
||||
comboSpacingFunc = () => comboSpacing;
|
||||
} else if (isFunction(comboSpacing)) {
|
||||
comboSpacingFunc = comboSpacing;
|
||||
} else {
|
||||
comboSpacingFunc = () => 0;
|
||||
}
|
||||
this.comboSpacing = comboSpacingFunc;
|
||||
|
||||
// linkDistance to function
|
||||
let linkDistance = this.linkDistance;
|
||||
let linkDistanceFunc;
|
||||
if (linkDistance) {
|
||||
linkDistance = 50;
|
||||
}
|
||||
if (isNumber(linkDistance)) {
|
||||
linkDistanceFunc = d => {
|
||||
return linkDistance;
|
||||
}
|
||||
}
|
||||
this.linkDistance = linkDistanceFunc;
|
||||
|
||||
// linkStrength to function
|
||||
let linkStrength = this.linkStrength;
|
||||
let linkStrengthFunc;
|
||||
if (!linkStrength) {
|
||||
linkStrength = 1;
|
||||
}
|
||||
if (isNumber(linkStrength)) {
|
||||
linkStrengthFunc = d => {
|
||||
return linkStrength;
|
||||
}
|
||||
}
|
||||
this.linkStrength = linkStrengthFunc;
|
||||
|
||||
// nodeStrength to function
|
||||
let nodeStrength = this.nodeStrength;
|
||||
let nodeStrengthFunc;
|
||||
if (!nodeStrength) {
|
||||
nodeStrength = 30;
|
||||
}
|
||||
if (isNumber(nodeStrength)) {
|
||||
nodeStrengthFunc = d => {
|
||||
return nodeStrength;
|
||||
}
|
||||
}
|
||||
this.nodeStrength = nodeStrengthFunc;
|
||||
}
|
||||
|
||||
private getComboMap() {
|
||||
const self = this;
|
||||
const nodeMap = self.nodeMap;
|
||||
const comboTrees = self.comboTrees;
|
||||
let comboMap: {
|
||||
[key: string]: {
|
||||
name: string | number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
count: number;
|
||||
depth: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
comboTrees.forEach(ctree => {
|
||||
let treeChildren = [];
|
||||
traverseTreeUp<ComboTree>(ctree, treeNode => {
|
||||
if (treeNode.itemType === 'node') return;
|
||||
if (comboMap[treeNode.id] === undefined) {
|
||||
const combo = {
|
||||
name: treeNode.id,
|
||||
cx: 0,
|
||||
cy: 0,
|
||||
count: 0,
|
||||
depth: self.oriComboMap[treeNode.id].depth
|
||||
};
|
||||
comboMap[treeNode.id] = combo;
|
||||
}
|
||||
const children = treeNode.children;
|
||||
if (children) {
|
||||
children.forEach(child => {
|
||||
treeChildren.push(child);
|
||||
});
|
||||
}
|
||||
const c = comboMap[treeNode.id];
|
||||
|
||||
c.cx = 0;
|
||||
c.cy = 0;
|
||||
treeChildren.forEach(child => {
|
||||
if (child.itemType !== 'node') return;
|
||||
const node = nodeMap[child.id];
|
||||
if (isNumber(node.x)) {
|
||||
c.cx += node.x;
|
||||
}
|
||||
if (isNumber(node.y)) {
|
||||
c.cy += node.y;
|
||||
}
|
||||
c.count++;
|
||||
});
|
||||
c.cx /= c.count;
|
||||
c.cy /= c.count;
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
return comboMap;
|
||||
}
|
||||
|
||||
private applyComboCenterForce(comboMap, displacements) {
|
||||
const self = this;
|
||||
const gravity = self.gravity;
|
||||
const comboGravity = self.comboGravity || gravity;
|
||||
const alpha = this.alpha;
|
||||
const comboTrees = self.comboTrees;
|
||||
const nodeIdxMap = self.nodeIdxMap;
|
||||
const nodeMap = self.nodeMap;
|
||||
comboTrees.forEach(ctree => {
|
||||
let treeChildren = [];
|
||||
traverseTreeUp<ComboTree>(ctree, treeNode => {
|
||||
if (treeNode.itemType === 'node') return;
|
||||
const children = treeNode.children;
|
||||
if (children) {
|
||||
children.forEach(child => {
|
||||
treeChildren.push(child);
|
||||
});
|
||||
}
|
||||
const c = comboMap[treeNode.id];
|
||||
|
||||
// higher depth the combo, larger the gravity
|
||||
const gravityScale = 1 - 1 / (c.depth + 1) || 0.1;
|
||||
// apply combo center force for all the descend nodes in this combo
|
||||
// and update the center position and count for this combo
|
||||
const comboX = c.cx;
|
||||
const comboY = c.cy;
|
||||
c.cx = 0;
|
||||
c.cy = 0;
|
||||
treeChildren.forEach(child => {
|
||||
if (child.itemType !== 'node') return;
|
||||
const node = nodeMap[child.id];
|
||||
const vecX = node.x - comboX;
|
||||
const vecY = node.y - comboY;
|
||||
const l = Math.sqrt(vecX * vecX + vecY * vecY);
|
||||
const childIdx = nodeIdxMap[node.id];
|
||||
displacements[childIdx].x -= vecX * comboGravity * alpha / l * gravityScale;
|
||||
displacements[childIdx].y -= vecY * comboGravity * alpha / l * gravityScale;
|
||||
|
||||
if (isNumber(node.x)) {
|
||||
c.cx += node.x;
|
||||
}
|
||||
if (isNumber(node.y)) {
|
||||
c.cy += node.y;
|
||||
}
|
||||
});
|
||||
c.cx /= c.count;
|
||||
c.cy /= c.count;
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private applyCalculate(comboMap, displacements: Point[]) {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
// store the vx, vy, and distance to reduce dulplicate calculation
|
||||
const vecMap = {};
|
||||
nodes.forEach((v, i) => {
|
||||
nodes.forEach((u, j) => {
|
||||
if (i < j) return;
|
||||
let vx = v.x - u.x;
|
||||
let vy = v.y - u.y;
|
||||
let vl = vx * vx + vy * vy;
|
||||
if (vl < 1) vl = Math.sqrt(vl);
|
||||
if (vx === 0) {
|
||||
vx = Math.random() * 0.01;
|
||||
vl += vx * vx;
|
||||
}
|
||||
if (vy === 0) {
|
||||
vy = Math.random() * 0.01;
|
||||
vl += vy * vy;
|
||||
}
|
||||
vecMap[`${v.id}-${u.id}`] = { vx, vy, vl };
|
||||
vecMap[`${u.id}-${v.id}`] = { vx: -vx, vy: -vy, vl };
|
||||
});
|
||||
});
|
||||
// get the sizes of the combos
|
||||
self.updateComboSizes(comboMap);
|
||||
self.calRepulsive(displacements, vecMap, comboMap);
|
||||
self.calAttractive(displacements, vecMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sizes of the combos according to their children
|
||||
*/
|
||||
private updateComboSizes(comboMap) {
|
||||
const self = this;
|
||||
const comboTrees = self.comboTrees;
|
||||
const nodeMap = self.nodeMap;
|
||||
const nodeSize = self.nodeSize as ((d?: unknown) => number) | undefined;
|
||||
|
||||
comboTrees.forEach(ctree => {
|
||||
let treeChildren = [];
|
||||
traverseTreeUp<ComboTree>(ctree, treeNode => {
|
||||
if (treeNode.itemType === 'node') return;
|
||||
const children = treeNode.children;
|
||||
if (children) {
|
||||
children.forEach(child => {
|
||||
treeChildren.push(child);
|
||||
});
|
||||
}
|
||||
const c = comboMap[treeNode.id];
|
||||
|
||||
c.minX = Infinity;
|
||||
c.minY = Infinity;
|
||||
c.maxX = -Infinity;
|
||||
c.maxY = -Infinity;
|
||||
treeChildren.forEach(child => {
|
||||
if (child.itemType !== 'node') return;
|
||||
const node = nodeMap[child.id];
|
||||
const r = nodeSize(node);
|
||||
const nodeMinX = node.x - r;
|
||||
const nodeMinY = node.y - r;
|
||||
const nodeMaxX = node.x + r;
|
||||
const nodeMaxY = node.y + r;
|
||||
if (c.minX > nodeMinX) c.minX = nodeMinX;
|
||||
if (c.minY > nodeMinY) c.minY = nodeMinY;
|
||||
if (c.maxX > nodeMaxX) c.maxX = nodeMaxX;
|
||||
if (c.maxY > nodeMaxY) c.maxY = nodeMaxY;
|
||||
});
|
||||
c.r = Math.max(c.maxX - c.minX, c.maxY - c.minY) / 2;
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the repulsive force between each node pair
|
||||
* @param displacements The array stores the displacements for nodes
|
||||
* @param vecMap The map stores vector between each node pair
|
||||
*/
|
||||
private calRepulsive(displacements: Point[], vecMap: any, comboMap) {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const max = self.width * self.optimizeRangeFactor * self.width * self.optimizeRangeFactor;
|
||||
const nodeStrength = self.nodeStrength as ((d?: unknown) => number);;
|
||||
const alpha = self.alpha;
|
||||
const collideStrength = self.collideStrength;
|
||||
const preventOverlap = self.preventOverlap;
|
||||
const preventNodeOverlap = self.preventNodeOverlap;
|
||||
const preventComboOverlap = self.preventComboOverlap;
|
||||
const nodeSizeFunc = self.nodeSize as ((d?: unknown) => number) | undefined;
|
||||
const scale = self.depthRepulsiveForceScale;
|
||||
nodes.forEach((v, i) => {
|
||||
nodes.forEach((u, j) => {
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
let { vl, vx, vy } = vecMap[`${v.id}-${u.id}`];
|
||||
if (vl > max) return;
|
||||
|
||||
const depthDiff = Math.abs(u.depth - v.depth);
|
||||
let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1;
|
||||
if (u.comboId !== v.comboId && depthParam === 1) {
|
||||
depthParam = scale;
|
||||
}
|
||||
|
||||
displacements[i].x += vx * nodeStrength(u) * alpha / vl * depthParam;
|
||||
displacements[i].y += vy * nodeStrength(u) * alpha / vl * depthParam;
|
||||
|
||||
// prevent node overlappings
|
||||
if (i < j && preventNodeOverlap) {
|
||||
const ri = nodeSizeFunc(v);
|
||||
const rj = nodeSizeFunc(u);
|
||||
const r = ri + rj;
|
||||
if (vl < r * r) {
|
||||
const sqrtl = Math.sqrt(vl);
|
||||
const ll = (r - sqrtl) / sqrtl * collideStrength;
|
||||
const rj2 = rj * rj;
|
||||
let rratio = rj2 / (ri * ri + rj2);
|
||||
const xl = vx * ll;
|
||||
const yl = vy * ll;
|
||||
displacements[i].x += xl * rratio;
|
||||
displacements[i].y += yl * rratio;
|
||||
rratio = 1 - rratio;
|
||||
displacements[j].x -= xl * rratio;
|
||||
displacements[j].y -= yl * rratio;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the attractive force between the node pair with edge
|
||||
* @param displacements The array stores the displacements for nodes
|
||||
* @param vecMap The map stores vector between each node pair
|
||||
*/
|
||||
private calAttractive(displacements: Point[], vecMap: any) {
|
||||
const self = this;
|
||||
const edges = self.edges;
|
||||
const linkDistance = self.linkDistance as ((d?: unknown) => number);
|
||||
const alpha = self.alpha;
|
||||
const linkStrength = self.linkStrength as ((d?: unknown) => number);
|
||||
const bias = self.bias;
|
||||
const scale = self.depthAttractiveForceScale;
|
||||
edges.forEach((e, i) => {
|
||||
if (!e.source || !e.target) return;
|
||||
const uIndex = self.nodeIdxMap[e.source];
|
||||
const vIndex = self.nodeIdxMap[e.target];
|
||||
if (uIndex === vIndex) {
|
||||
return;
|
||||
}
|
||||
const u = self.nodeMap[e.source];
|
||||
const v = self.nodeMap[e.target];
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
let { vl, vx, vy } = vecMap[`${e.target}-${e.source}`];
|
||||
const l = (vl - linkDistance(e)) / vl * alpha * linkStrength(e);
|
||||
const vecX = vx * l;
|
||||
const vecY = vy * l;
|
||||
const b = bias[i];
|
||||
|
||||
const depthDiff = Math.abs(u.depth - v.depth);
|
||||
let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1;
|
||||
if (u.comboId !== v.comboId && depthParam === 1) {
|
||||
depthParam = scale;
|
||||
} else if (u.comboId === v.comboId) {
|
||||
depthParam = 1;
|
||||
}
|
||||
displacements[vIndex].x -= vecX * b * depthParam;
|
||||
displacements[vIndex].y -= vecY * b * depthParam;
|
||||
displacements[uIndex].x += vecX * (1 - b) * depthParam;
|
||||
displacements[uIndex].y += vecY * (1 - b) * depthParam;
|
||||
});
|
||||
}
|
||||
}
|
@ -201,7 +201,6 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: nodeMap、nodeIndexMap 等根本不需要依靠参数传递
|
||||
private applyCalculate(nodes: Node[], edges: Edge[], displacements: Point[], k: number) {
|
||||
const self = this;
|
||||
self.calRepulsive(nodes, displacements, k);
|
||||
|
444
src/layout/g6force.ts
Normal file
444
src/layout/g6force.ts
Normal file
@ -0,0 +1,444 @@
|
||||
/**
|
||||
* @fileOverview G6's force layout, supports clustering
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
|
||||
import { BaseLayout } from './layout';
|
||||
import { isNumber, isArray, isFunction } from '@antv/util';
|
||||
import { Point } from '@antv/g-base';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
cluster: string | number;
|
||||
};
|
||||
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
type NodeMap = {
|
||||
[key: string]: Node;
|
||||
};
|
||||
|
||||
/**
|
||||
* G6's force layout
|
||||
*/
|
||||
export default class G6Force extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 停止迭代的最大迭代数 */
|
||||
public maxIteration: number = 500;
|
||||
/** 重力大小,影响图的紧凑程度 */
|
||||
public gravity: number = 10;
|
||||
/** 是否产生聚类力 */
|
||||
public clustering: boolean = false;
|
||||
/** 聚类力大小 */
|
||||
public clusterGravity: number = 10;
|
||||
/** 默认边长度 */
|
||||
public linkDistance: number | ((d?: unknown) => number) = 50;
|
||||
/** 每次迭代位移的衰减相关参数 */
|
||||
public alpha: number = 1;
|
||||
public alphaMin: number = 0.001;
|
||||
public alphaDecay: number = 1 - Math.pow(this.alphaMin, 1 / 300);
|
||||
public alphaTarget: number = 0;
|
||||
/** 节点运动速度衰减参数 */
|
||||
public velocityDecay: number = 0.6;
|
||||
/** 边引力大小 */
|
||||
public linkStrength: number | ((d?: unknown) => number) = 1;
|
||||
/** 节点引力大小 */
|
||||
public nodeStrength: number | ((d?: unknown) => number) = 30;
|
||||
/** 是否开启防止重叠 */
|
||||
public preventOverlap: boolean = false;
|
||||
/** 防止重叠的碰撞力大小 */
|
||||
public collideStrength: number = 1;
|
||||
/** 节点大小,用于防止重叠 */
|
||||
public nodeSize: number | number[] | ((d?: unknown) => number) | undefined;
|
||||
/** 节点最小间距,防止重叠时的间隙 */
|
||||
public nodeSpacing: ((d?: unknown) => number) | undefined;
|
||||
/** 优化计算斥力的速度,两节点间距超过 optimizeRangeFactor * width 则不再计算斥力和重叠斥力 */
|
||||
public optimizeRangeFactor: number = 1;
|
||||
/** 每次迭代的回调函数 */
|
||||
public tick: () => void = () => {};
|
||||
|
||||
/** 内部计算参数 */
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
private width: number = 300;
|
||||
private height: number = 300;
|
||||
private bias: number[] = [];
|
||||
private nodeMap: NodeMap = {};
|
||||
private nodeIdxMap: NodeIdxMap = {};
|
||||
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
maxIteration: 1000,
|
||||
center: [0, 0],
|
||||
gravity: 10,
|
||||
speed: 1,
|
||||
clustering: false,
|
||||
clusterGravity: 10,
|
||||
preventOverlap: false,
|
||||
nodeSpacing: undefined,
|
||||
collideStrength: 10
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 执行布局
|
||||
*/
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const center = self.center;
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (nodes.length === 1) {
|
||||
nodes[0].x = center[0];
|
||||
nodes[0].y = center[1];
|
||||
return;
|
||||
}
|
||||
const nodeMap: NodeMap = {};
|
||||
const nodeIdxMap: NodeIdxMap = {};
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = node;
|
||||
nodeIdxMap[node.id] = i;
|
||||
});
|
||||
self.nodeMap = nodeMap;
|
||||
self.nodeIdxMap = nodeIdxMap;
|
||||
// layout
|
||||
self.run();
|
||||
}
|
||||
|
||||
public run() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const edges = self.edges;
|
||||
const maxIteration = self.maxIteration;
|
||||
if (!self.width && typeof window !== 'undefined') {
|
||||
self.width = window.innerWidth;
|
||||
}
|
||||
if (!self.height && typeof window !== 'undefined') {
|
||||
self.height = window.innerHeight;
|
||||
}
|
||||
const center = self.center;
|
||||
const velocityDecay = self.velocityDecay;
|
||||
const clustering = self.clustering;
|
||||
let clusterMap;
|
||||
|
||||
self.initVals();
|
||||
|
||||
if (clustering) {
|
||||
clusterMap = self.getClusterMap();
|
||||
}
|
||||
// iterate
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const displacements: Point[] = [];
|
||||
nodes.forEach((_, j) => {
|
||||
displacements[j] = { x: 0, y: 0 };
|
||||
});
|
||||
self.applyCalculate(nodes, edges, displacements);
|
||||
|
||||
// gravity for clusters
|
||||
if (clustering) {
|
||||
self.applyClusterForce(clusterMap, displacements);
|
||||
}
|
||||
|
||||
// move
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
n.x += displacements[j].x * velocityDecay;
|
||||
n.y += displacements[j].y * velocityDecay;
|
||||
});
|
||||
this.alpha += (this.alphaTarget - this.alpha) * this.alphaDecay;
|
||||
self.tick();
|
||||
}
|
||||
|
||||
// move to center
|
||||
const meanCenter = [ 0, 0 ];
|
||||
nodes.forEach(n => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
meanCenter[0] += n.x;
|
||||
meanCenter[1] += n.y;
|
||||
});
|
||||
meanCenter[0] /= nodes.length;
|
||||
meanCenter[1] /= nodes.length;
|
||||
const centerOffset = [ center[0] - meanCenter[0], center[1] - meanCenter[1] ];
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
n.x += centerOffset[0];
|
||||
n.y += centerOffset[1];
|
||||
});
|
||||
}
|
||||
|
||||
private initVals() {
|
||||
const self = this;
|
||||
const edges = self.edges;
|
||||
const count = {};
|
||||
|
||||
// get edge bias
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
if (count[edges[i].source]) count[edges[i].source] ++;
|
||||
else count[edges[i].source] = 1;
|
||||
if (count[edges[i].target]) count[edges[i].target] ++;
|
||||
else count[edges[i].target] = 1;
|
||||
}
|
||||
const bias = [];
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
bias[i] = count[edges[i].source] / (count[edges[i].source] + count[edges[i].target]);
|
||||
}
|
||||
this.bias = bias;
|
||||
|
||||
const nodeSize = self.nodeSize;
|
||||
const nodeSpacing = self.nodeSpacing;
|
||||
let nodeSizeFunc: (d: any) => number;
|
||||
let nodeSpacingFunc: (d: any) => number;
|
||||
|
||||
// nodeSpacing to function
|
||||
if (isNumber(nodeSpacing)) {
|
||||
nodeSpacingFunc = () => nodeSpacing;
|
||||
} else if (isFunction(nodeSpacing)) {
|
||||
nodeSpacingFunc = nodeSpacing;
|
||||
} else {
|
||||
nodeSpacingFunc = () => 0;
|
||||
}
|
||||
|
||||
// nodeSize to function
|
||||
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 / 2 + nodeSpacingFunc(d);
|
||||
}
|
||||
return d.size / 2 + nodeSpacingFunc(d);
|
||||
}
|
||||
return 10 + nodeSpacingFunc(d);
|
||||
};
|
||||
} else if (isFunction(nodeSize)) {
|
||||
nodeSizeFunc = d => {
|
||||
const size = nodeSize(d);
|
||||
return size + nodeSpacingFunc(d);
|
||||
};
|
||||
} else if (isArray(nodeSize)) {
|
||||
const larger = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
|
||||
const radius = larger / 2;
|
||||
nodeSizeFunc = d => radius + nodeSpacingFunc(d);
|
||||
} else if (isNumber(nodeSize)) {
|
||||
const radius = nodeSize / 2;
|
||||
nodeSizeFunc = d => radius + nodeSpacingFunc(d);
|
||||
} else {
|
||||
nodeSizeFunc = () => 10;
|
||||
}
|
||||
this.nodeSize = nodeSizeFunc;
|
||||
|
||||
// linkDistance to function
|
||||
let linkDistance = this.linkDistance;
|
||||
let linkDistanceFunc;
|
||||
if (linkDistance) {
|
||||
linkDistance = 50;
|
||||
}
|
||||
if (isNumber(linkDistance)) {
|
||||
linkDistanceFunc = d => {
|
||||
return linkDistance;
|
||||
}
|
||||
}
|
||||
this.linkDistance = linkDistanceFunc;
|
||||
|
||||
// linkStrength to function
|
||||
let linkStrength = this.linkStrength;
|
||||
let linkStrengthFunc;
|
||||
if (!linkStrength) {
|
||||
linkStrength = 1;
|
||||
}
|
||||
if (isNumber(linkStrength)) {
|
||||
linkStrengthFunc = d => {
|
||||
return linkStrength;
|
||||
}
|
||||
}
|
||||
this.linkStrength = linkStrengthFunc;
|
||||
|
||||
// nodeStrength to function
|
||||
let nodeStrength = this.nodeStrength;
|
||||
let nodeStrengthFunc;
|
||||
if (!nodeStrength) {
|
||||
nodeStrength = 30;
|
||||
}
|
||||
if (isNumber(nodeStrength)) {
|
||||
nodeStrengthFunc = d => {
|
||||
return nodeStrength;
|
||||
}
|
||||
}
|
||||
this.nodeStrength = nodeStrengthFunc;
|
||||
}
|
||||
|
||||
private getClusterMap() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
let clusterMap: {
|
||||
[key: string]: {
|
||||
name: string | number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
count: number;
|
||||
};
|
||||
} = {};
|
||||
nodes.forEach(n => {
|
||||
if (clusterMap[n.cluster] === undefined) {
|
||||
const cluster = {
|
||||
name: n.cluster,
|
||||
cx: 0,
|
||||
cy: 0,
|
||||
count: 0,
|
||||
};
|
||||
clusterMap[n.cluster] = cluster;
|
||||
}
|
||||
const c = clusterMap[n.cluster];
|
||||
if (isNumber(n.x)) {
|
||||
c.cx += n.x;
|
||||
}
|
||||
if (isNumber(n.y)) {
|
||||
c.cy += n.y;
|
||||
}
|
||||
c.count++;
|
||||
});
|
||||
for (const key in clusterMap) {
|
||||
clusterMap[key].cx /= clusterMap[key].count;
|
||||
clusterMap[key].cy /= clusterMap[key].count;
|
||||
}
|
||||
return clusterMap;
|
||||
}
|
||||
|
||||
private applyClusterForce(clusterMap, displacements) {
|
||||
const self = this;
|
||||
const gravity = self.gravity;
|
||||
const nodes = self.nodes;
|
||||
const clusterGravity = self.clusterGravity || gravity;
|
||||
const alpha = this.alpha;
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
const c = clusterMap[n.cluster];
|
||||
const vecX = n.x - c.cx;
|
||||
const vecY = n.y - c.cy;
|
||||
const l = Math.sqrt(vecX * vecX + vecY * vecY);
|
||||
displacements[j].x -= vecX * clusterGravity * alpha / l;
|
||||
displacements[j].y -= vecY * clusterGravity * alpha / l;
|
||||
});
|
||||
|
||||
for (const key in clusterMap) {
|
||||
clusterMap[key].cx = 0;
|
||||
clusterMap[key].cy = 0;
|
||||
clusterMap[key].count = 0;
|
||||
}
|
||||
|
||||
nodes.forEach(n => {
|
||||
const c = clusterMap[n.cluster];
|
||||
if (isNumber(n.x)) {
|
||||
c.cx += n.x;
|
||||
}
|
||||
if (isNumber(n.y)) {
|
||||
c.cy += n.y;
|
||||
}
|
||||
c.count++;
|
||||
});
|
||||
for (const key in clusterMap) {
|
||||
clusterMap[key].cx /= clusterMap[key].count;
|
||||
clusterMap[key].cy /= clusterMap[key].count;
|
||||
}
|
||||
}
|
||||
|
||||
private applyCalculate(nodes: Node[], edges: Edge[], displacements: Point[]) {
|
||||
const self = this;
|
||||
// store the vx, vy, and distance to reduce dulplicate calculation
|
||||
|
||||
const vecMap = {};
|
||||
nodes.forEach((v, i) => {
|
||||
displacements[i] = { x: 0, y: 0 };
|
||||
nodes.forEach((u, j) => {
|
||||
if (i < j) return;
|
||||
let vx = v.x - u.x;
|
||||
let vy = v.y - u.y;
|
||||
let vl = vx * vx + vy * vy;
|
||||
if (vl < 1) vl = Math.sqrt(vl);
|
||||
if (vx === 0) {
|
||||
vx = Math.random() * 0.01;
|
||||
vl += vx * vx;
|
||||
}
|
||||
if (vy === 0) {
|
||||
vy = Math.random() * 0.01;
|
||||
vl += vy * vy;
|
||||
}
|
||||
vecMap[`${v.id}-${u.id}`] = { vx, vy, vl };
|
||||
vecMap[`${u.id}-${v.id}`] = { vx: -vx, vy: -vy, vl };
|
||||
});
|
||||
});
|
||||
self.calRepulsive(nodes, displacements, vecMap);
|
||||
self.calAttractive(edges, displacements, vecMap);
|
||||
}
|
||||
|
||||
private calRepulsive(nodes: Node[], displacements: Point[], vecMap: any) {
|
||||
const max = this.width * this.optimizeRangeFactor * this.width * this.optimizeRangeFactor;
|
||||
const nodeStrength = this.nodeStrength as ((d?: unknown) => number);;
|
||||
const alpha = this.alpha;
|
||||
const collideStrength = this.collideStrength;
|
||||
const preventOverlap = this.preventOverlap;
|
||||
const nodeSizeFunc = this.nodeSize as ((d?: unknown) => number) | undefined;
|
||||
nodes.forEach((v, i) => {
|
||||
nodes.forEach((u, j) => {
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
let { vl, vx, vy } = vecMap[`${v.id}-${u.id}`];
|
||||
if (vl > max) return;
|
||||
displacements[i].x += vx * nodeStrength(u) * alpha / vl;
|
||||
displacements[i].y += vy * nodeStrength(u) * alpha / vl;
|
||||
|
||||
// collide strength
|
||||
if (preventOverlap && i < j) {
|
||||
const ri = nodeSizeFunc(v);
|
||||
const rj = nodeSizeFunc(u);
|
||||
const r = ri + rj;
|
||||
if (vl < r * r) {
|
||||
const sqrtl = Math.sqrt(vl);
|
||||
const ll = (r - sqrtl) / sqrtl * collideStrength;
|
||||
let rratio = rj * rj / (ri * ri + rj * rj);
|
||||
const xl = vx * ll;
|
||||
const yl = vy * ll;
|
||||
displacements[i].x += xl * rratio;
|
||||
displacements[i].y += yl * rratio;
|
||||
rratio = 1 - rratio;
|
||||
displacements[j].x -= xl * rratio;
|
||||
displacements[j].y -= yl * rratio;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private calAttractive(edges: Edge[], displacements: Point[], vecMap: any) {
|
||||
const linkDistance = this.linkDistance as ((d?: unknown) => number);
|
||||
const alpha = this.alpha;
|
||||
const linkStrength = this.linkStrength as ((d?: unknown) => number);
|
||||
const bias = this.bias;
|
||||
edges.forEach((e, i) => {
|
||||
if (!e.source || !e.target) return;
|
||||
const uIndex = this.nodeIdxMap[e.source];
|
||||
const vIndex = this.nodeIdxMap[e.target];
|
||||
if (uIndex === vIndex) {
|
||||
return;
|
||||
}
|
||||
const u = this.nodeMap[e.source];
|
||||
const v = this.nodeMap[e.target];
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
let { vl, vx, vy } = vecMap[`${e.target}-${e.source}`];
|
||||
const l = (vl - linkDistance(e)) / vl * alpha * linkStrength(e);
|
||||
const vecX = vx * l;
|
||||
const vecY = vy * l;
|
||||
const b = bias[i];
|
||||
displacements[vIndex].x -= vecX * b;
|
||||
displacements[vIndex].y -= vecY * b;
|
||||
displacements[uIndex].x += vecX * (1 - b);
|
||||
displacements[uIndex].y += vecY * (1 - b);
|
||||
});
|
||||
}
|
||||
}
|
@ -10,17 +10,21 @@ import Circular from './circular';
|
||||
import Concentric from './concentric';
|
||||
import Dagre from './dagre';
|
||||
import Force from './force';
|
||||
import G6Force from './g6force';
|
||||
import Fruchterman from './fruchterman';
|
||||
import Grid from './grid';
|
||||
import MDS from './mds';
|
||||
import Radial from './radial/radial';
|
||||
import Random from './random';
|
||||
import ComboForce from './comboForce';
|
||||
|
||||
const layouts = {
|
||||
circular: Circular,
|
||||
concentric: Concentric,
|
||||
dagre: Dagre,
|
||||
force: Force,
|
||||
g6force: G6Force,
|
||||
comboForce: ComboForce,
|
||||
fruchterman: Fruchterman,
|
||||
grid: Grid,
|
||||
mds: MDS,
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { EdgeConfig, GraphData, IPointTuple, NodeConfig } from '../types';
|
||||
import { EdgeConfig, GraphData, IPointTuple, NodeConfig, ComboConfig } from '../types';
|
||||
import { ILayout } from '../interface/layout';
|
||||
|
||||
// import augment from '@antv/util/lib/augment';
|
||||
@ -19,6 +19,7 @@ type LayoutConstructor<Cfg = any> = new () => BaseLayout<Cfg>;
|
||||
export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
|
||||
public nodes: NodeConfig[] | null = [];
|
||||
public edges: EdgeConfig[] | null = [];
|
||||
public combos: ComboConfig[] | null = [];
|
||||
public positions: IPointTuple[] | null = [];
|
||||
public destroyed: boolean = false;
|
||||
|
||||
@ -26,6 +27,7 @@ export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
|
||||
const self = this;
|
||||
self.nodes = data.nodes || [];
|
||||
self.edges = data.edges || [];
|
||||
self.combos = data.combos || [];
|
||||
}
|
||||
|
||||
public execute() {}
|
||||
|
@ -497,6 +497,7 @@ export interface ComboTree {
|
||||
depth?: number;
|
||||
parentId?: string;
|
||||
removed?: boolean;
|
||||
itemType?: 'node' | 'combo';
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import letterAspectRatio from './letterAspectRatio';
|
||||
import { isString, clone } from '@antv/util';
|
||||
import { BBox } from '@antv/g-math/lib/types';
|
||||
import { IGraph } from '../interface/graph';
|
||||
import { INode } from '../interface/item';
|
||||
|
||||
const { PI, sin, cos } = Math;
|
||||
|
||||
@ -419,7 +420,7 @@ export const getTextSize = (text, fontSize) => {
|
||||
* @param nodes the nodes array
|
||||
* @return the tree
|
||||
*/
|
||||
export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) => {
|
||||
export const plainCombosToTrees = (array: ComboConfig[], nodes?: INode[]) => {
|
||||
const result: ComboTree[] = [];
|
||||
const addedMap = {};
|
||||
const modelMap = {};
|
||||
@ -429,6 +430,7 @@ export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) =
|
||||
|
||||
array.forEach((d, i) => {
|
||||
const cd = clone(d);
|
||||
cd.itemType = 'combo';
|
||||
cd.children = undefined;
|
||||
if (cd.parentId === cd.id) {
|
||||
console.warn(`The parentId for combo ${cd.id} can not be the same as the combo's id`);
|
||||
@ -481,28 +483,42 @@ export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) =
|
||||
addedMap[cd.id] = cd;
|
||||
}
|
||||
});
|
||||
|
||||
const nodeMap = {};
|
||||
nodes && nodes.forEach(node => {
|
||||
const combo = addedMap[node.comboId];
|
||||
const nodeModel = node.getModel();
|
||||
nodeMap[nodeModel.id] = node;
|
||||
const combo = addedMap[nodeModel.comboId as string];
|
||||
if (combo) {
|
||||
if (combo.children) combo.children.push(node);
|
||||
else combo.children = [node];
|
||||
addedMap[node.id] = clone(node);
|
||||
addedMap[node.id].itemType = 'node';
|
||||
const cnode: NodeConfig = {
|
||||
id: nodeModel.id,
|
||||
comboId: nodeModel.comboId as string
|
||||
};
|
||||
if (combo.children) combo.children.push(cnode);
|
||||
else combo.children = [cnode];
|
||||
cnode.itemType = 'node';
|
||||
addedMap[nodeModel.id] = cnode;
|
||||
}
|
||||
});
|
||||
|
||||
result.forEach((tree: ComboTree) => {
|
||||
tree.depth = 0;
|
||||
traverse<ComboTree>(tree, child => {
|
||||
let parent = addedMap[child.parentId];
|
||||
let parent;
|
||||
if (addedMap[child.id]['itemType'] === 'node') {
|
||||
parent = addedMap[child['comboId'] as string];
|
||||
} else {
|
||||
parent = addedMap[child.parentId];
|
||||
}
|
||||
if (parent) {
|
||||
child.depth = parent.depth + 1;
|
||||
} else {
|
||||
child.depth = 0;
|
||||
}
|
||||
addedMap[child.id].depth = child.depth;
|
||||
const oriNodeModel = nodeMap[child.id];
|
||||
if (oriNodeModel) {
|
||||
oriNodeModel.depth = child.depth;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@ -537,7 +553,7 @@ export const getComboBBox = (children: ComboTree[], graph: IGraph): BBox => {
|
||||
};
|
||||
children && children.forEach(child => {
|
||||
const childItem = graph.findById(child.id);
|
||||
// const childModel = childItem.getModel();
|
||||
childItem.set('bboxCanvasCache', undefined);
|
||||
const childBBox = childItem.getCanvasBBox();
|
||||
if (childBBox.x && comboBBox.minX > childBBox.minX) comboBBox.minX = childBBox.minX;
|
||||
if (childBBox.y && comboBBox.minY > childBBox.minY) comboBBox.minY = childBBox.minY;
|
||||
|
555
stories/Layout/component/g6force-layout.tsx
Normal file
555
stories/Layout/component/g6force-layout.tsx
Normal file
@ -0,0 +1,555 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import G6 from '../../../src';
|
||||
import { IGraph } from '../../../src/interface/graph';
|
||||
|
||||
let graph: IGraph = null;
|
||||
|
||||
const colors = {
|
||||
a: '#BDD2FD',
|
||||
b: '#BDEFDB',
|
||||
c: '#C2C8D5',
|
||||
d: '#FBE5A2',
|
||||
e: '#F6C3B7',
|
||||
f: '#B6E3F5',
|
||||
g: '#D3C6EA',
|
||||
h: '#FFD8B8',
|
||||
i: '#AAD8D8',
|
||||
j: '#FFD6E7',
|
||||
};
|
||||
|
||||
const testData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
label: '0',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
label: '1',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: '2',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: '3',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: '4',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: '5',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
label: '6',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
label: '7',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
label: '8',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
label: '9',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
label: '10',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '11',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
label: '12',
|
||||
comboId: 'a',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
label: '13',
|
||||
comboId: 'b',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
label: '14',
|
||||
comboId: 'b',
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
label: '15',
|
||||
comboId: 'b',
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
label: '16',
|
||||
comboId: 'b',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
label: '17',
|
||||
comboId: 'b',
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
label: '18',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
label: '19',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
label: '20',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
label: '21',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
label: '22',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
label: '23',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
label: '24',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
label: '25',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '26',
|
||||
label: '26',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '27',
|
||||
label: '27',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '28',
|
||||
label: '28',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '29',
|
||||
label: '29',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
label: '30',
|
||||
comboId: 'c',
|
||||
},
|
||||
{
|
||||
id: '31',
|
||||
label: '31',
|
||||
comboId: 'd',
|
||||
},
|
||||
{
|
||||
id: '32',
|
||||
label: '32',
|
||||
comboId: 'd',
|
||||
},
|
||||
{
|
||||
id: '33',
|
||||
label: '33',
|
||||
comboId: 'd',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '2',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '3',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '4',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '5',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '7',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '8',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '9',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '10',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '11',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '15',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '16',
|
||||
},
|
||||
{
|
||||
source: '2',
|
||||
target: '3',
|
||||
},
|
||||
{
|
||||
source: '4',
|
||||
target: '5',
|
||||
},
|
||||
{
|
||||
source: '4',
|
||||
target: '6',
|
||||
},
|
||||
{
|
||||
source: '5',
|
||||
target: '6',
|
||||
},
|
||||
{
|
||||
source: '7',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '8',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '9',
|
||||
target: '10',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '12',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '12',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '17',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '18',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '17',
|
||||
target: '18',
|
||||
},
|
||||
{
|
||||
source: '17',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '18',
|
||||
target: '19',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '23',
|
||||
},
|
||||
{
|
||||
source: '20',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '21',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '25',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '26',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '23',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '28',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '30',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '31',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '32',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '28',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '27',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '29',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '30',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '31',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '32',
|
||||
target: '33',
|
||||
},
|
||||
],
|
||||
combos: [{
|
||||
id: 'a',
|
||||
label: 'combo a'
|
||||
}, {
|
||||
id: 'b',
|
||||
label: 'combo b'
|
||||
}, {
|
||||
id: 'c',
|
||||
label: 'combo c'
|
||||
}, {
|
||||
id: 'd',
|
||||
label: 'combo d',
|
||||
parentId: 'b'
|
||||
},
|
||||
// {
|
||||
// id: 'e',
|
||||
// label: 'combo e'
|
||||
// }
|
||||
]
|
||||
};
|
||||
|
||||
const testData2 = {
|
||||
nodes: [{
|
||||
id: 'node0',
|
||||
x: 100,
|
||||
y: 100
|
||||
}, {
|
||||
id: 'node1',
|
||||
x: 101,
|
||||
y: 102
|
||||
}, {
|
||||
id: 'node2',
|
||||
x: 103,
|
||||
y: 101
|
||||
}, {
|
||||
id: 'node3',
|
||||
x: 103,
|
||||
y: 104
|
||||
}],
|
||||
edges: [{
|
||||
source: 'node0',
|
||||
target: 'node1'
|
||||
}, {
|
||||
source: 'node1',
|
||||
target: 'node2'
|
||||
}, {
|
||||
source: 'node2',
|
||||
target: 'node3'
|
||||
}, {
|
||||
source: 'node3',
|
||||
target: 'node0'
|
||||
}]
|
||||
}
|
||||
const G6ForceLayout = () => {
|
||||
const container = React.useRef();
|
||||
useEffect(() => {
|
||||
if (!graph) {
|
||||
graph = new G6.Graph({
|
||||
container: container.current as string | HTMLElement,
|
||||
width: 800,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node'],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
linkDistance: 100,
|
||||
// comboIding: true,
|
||||
// comboIdGravity: 5,
|
||||
// preventOverlap: true,
|
||||
// collideStrength: 1,
|
||||
// nodeSpacing: 2,
|
||||
nodeStrength: 30,
|
||||
linkStrength: 0.1,
|
||||
// preventOverlap: true,
|
||||
preventComboOverlap: true
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 3,
|
||||
color: '#666',
|
||||
}
|
||||
});
|
||||
|
||||
graph.node(node => {
|
||||
const color = colors[node.comboId as string];
|
||||
return {
|
||||
size: 20,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: '#ccc',
|
||||
fill: color,
|
||||
},
|
||||
}
|
||||
});
|
||||
graph.combo(combo => {
|
||||
const color = colors[combo.id as string];
|
||||
return {
|
||||
size: 20,
|
||||
padding: 5,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: color,
|
||||
fillOpacity: 0.8
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json',
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
graph.data(testData);
|
||||
graph.render();
|
||||
});
|
||||
}
|
||||
});
|
||||
return <div ref={container}></div>;
|
||||
};
|
||||
|
||||
export default G6ForceLayout;
|
@ -6,6 +6,7 @@ import DagreLayout from './component/dagre-layout';
|
||||
import FruchtermanWorker from './component/fruchterman-worker-layout';
|
||||
import AddNodeLayout from './component/addNodeLayout'
|
||||
import ChangeData from './component/changeData'
|
||||
import G6ForceLayout from './component/g6force-layout';
|
||||
|
||||
export default { title: 'Layout' };
|
||||
|
||||
@ -15,3 +16,5 @@ storiesOf('Layout', module)
|
||||
.add('Fruchterman worker layout', () => <FruchtermanWorker />)
|
||||
.add('add node and layout', () => <AddNodeLayout />)
|
||||
.add('change data', () => <ChangeData />)
|
||||
.add('G6 force layout', () => <G6ForceLayout />)
|
||||
.add('Fruchterman worker layout', () => <FruchtermanWorker />);
|
||||
|
Loading…
Reference in New Issue
Block a user