mirror of
https://gitee.com/antv/g6.git
synced 2024-12-01 11:18:30 +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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.layoutType === 'force') {
|
if (this.layoutType === 'force' || this.layoutType === 'g6force') {
|
||||||
const { onTick } = layoutCfg;
|
const { onTick } = layoutCfg;
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (onTick) {
|
if (onTick) {
|
||||||
@ -158,6 +158,8 @@ export default class LayoutController {
|
|||||||
}
|
}
|
||||||
graph.emit('afterlayout');
|
graph.emit('afterlayout');
|
||||||
};
|
};
|
||||||
|
} else if (this.layoutType === 'comboForce') {
|
||||||
|
layoutCfg.comboTrees = graph.get('comboTrees');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.layoutType !== undefined) {
|
if (this.layoutType !== undefined) {
|
||||||
@ -354,8 +356,10 @@ export default class LayoutController {
|
|||||||
public setDataFromGraph() {
|
public setDataFromGraph() {
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
const edges = [];
|
const edges = [];
|
||||||
|
const combos = [];
|
||||||
const nodeItems = this.graph.getNodes();
|
const nodeItems = this.graph.getNodes();
|
||||||
const edgeItems = this.graph.getEdges();
|
const edgeItems = this.graph.getEdges();
|
||||||
|
const comboItems = this.graph.getCombos();
|
||||||
nodeItems.forEach(nodeItem => {
|
nodeItems.forEach(nodeItem => {
|
||||||
const model = nodeItem.getModel();
|
const model = nodeItem.getModel();
|
||||||
nodes.push(model);
|
nodes.push(model);
|
||||||
@ -364,7 +368,11 @@ export default class LayoutController {
|
|||||||
const model = edgeItem.getModel();
|
const model = edgeItem.getModel();
|
||||||
edges.push(model);
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -983,12 +983,28 @@ export default class Graph extends EventEmitter implements IGraph {
|
|||||||
|
|
||||||
// process the data to tree structure
|
// process the data to tree structure
|
||||||
if (combos) {
|
if (combos) {
|
||||||
const comboTrees = plainCombosToTrees(combos, nodes);
|
const comboTrees = plainCombosToTrees(combos, self.getNodes());
|
||||||
this.set('comboTrees', comboTrees);
|
this.set('comboTrees', comboTrees);
|
||||||
// add combos
|
// add combos
|
||||||
self.addCombos(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 (!this.get('groupByTypes')) {
|
||||||
if (combos) {
|
if (combos) {
|
||||||
this.sortCombos(data);
|
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
|
// 防止传入的数据不存在nodes
|
||||||
if (data.nodes) {
|
if (data.nodes) {
|
||||||
// 获取所有有groupID的node
|
// 获取所有有groupID的node
|
||||||
@ -1134,7 +1133,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
|||||||
// process the data to tree structure
|
// process the data to tree structure
|
||||||
const combosData = (data as GraphData).combos;
|
const combosData = (data as GraphData).combos;
|
||||||
if (combosData) {
|
if (combosData) {
|
||||||
const comboTrees = plainCombosToTrees(combosData, (data as GraphData).nodes);
|
const comboTrees = plainCombosToTrees(combosData, self.getNodes());
|
||||||
this.set('comboTrees', comboTrees);
|
this.set('comboTrees', comboTrees);
|
||||||
// add combos
|
// add combos
|
||||||
self.addCombos(combosData);
|
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 渲染图的数据
|
* @param {GraphData} data 渲染图的数据
|
||||||
@ -1359,7 +1379,6 @@ export default class Graph extends EventEmitter implements IGraph {
|
|||||||
return this.get('combos')
|
return this.get('combos')
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 待实现getComboNodes方法
|
|
||||||
/**
|
/**
|
||||||
* 获取指定 Combo 中所有的节点
|
* 获取指定 Combo 中所有的节点
|
||||||
* @param comboId combo ID
|
* @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) {
|
private applyCalculate(nodes: Node[], edges: Edge[], displacements: Point[], k: number) {
|
||||||
const self = this;
|
const self = this;
|
||||||
self.calRepulsive(nodes, displacements, k);
|
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 Concentric from './concentric';
|
||||||
import Dagre from './dagre';
|
import Dagre from './dagre';
|
||||||
import Force from './force';
|
import Force from './force';
|
||||||
|
import G6Force from './g6force';
|
||||||
import Fruchterman from './fruchterman';
|
import Fruchterman from './fruchterman';
|
||||||
import Grid from './grid';
|
import Grid from './grid';
|
||||||
import MDS from './mds';
|
import MDS from './mds';
|
||||||
import Radial from './radial/radial';
|
import Radial from './radial/radial';
|
||||||
import Random from './random';
|
import Random from './random';
|
||||||
|
import ComboForce from './comboForce';
|
||||||
|
|
||||||
const layouts = {
|
const layouts = {
|
||||||
circular: Circular,
|
circular: Circular,
|
||||||
concentric: Concentric,
|
concentric: Concentric,
|
||||||
dagre: Dagre,
|
dagre: Dagre,
|
||||||
force: Force,
|
force: Force,
|
||||||
|
g6force: G6Force,
|
||||||
|
comboForce: ComboForce,
|
||||||
fruchterman: Fruchterman,
|
fruchterman: Fruchterman,
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
mds: MDS,
|
mds: MDS,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* @author shiwu.wyy@antfin.com
|
* @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 { ILayout } from '../interface/layout';
|
||||||
|
|
||||||
// import augment from '@antv/util/lib/augment';
|
// 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> {
|
export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
|
||||||
public nodes: NodeConfig[] | null = [];
|
public nodes: NodeConfig[] | null = [];
|
||||||
public edges: EdgeConfig[] | null = [];
|
public edges: EdgeConfig[] | null = [];
|
||||||
|
public combos: ComboConfig[] | null = [];
|
||||||
public positions: IPointTuple[] | null = [];
|
public positions: IPointTuple[] | null = [];
|
||||||
public destroyed: boolean = false;
|
public destroyed: boolean = false;
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
|
|||||||
const self = this;
|
const self = this;
|
||||||
self.nodes = data.nodes || [];
|
self.nodes = data.nodes || [];
|
||||||
self.edges = data.edges || [];
|
self.edges = data.edges || [];
|
||||||
|
self.combos = data.combos || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public execute() {}
|
public execute() {}
|
||||||
|
@ -497,6 +497,7 @@ export interface ComboTree {
|
|||||||
depth?: number;
|
depth?: number;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
removed?: boolean;
|
removed?: boolean;
|
||||||
|
itemType?: 'node' | 'combo';
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import letterAspectRatio from './letterAspectRatio';
|
|||||||
import { isString, clone } from '@antv/util';
|
import { isString, clone } from '@antv/util';
|
||||||
import { BBox } from '@antv/g-math/lib/types';
|
import { BBox } from '@antv/g-math/lib/types';
|
||||||
import { IGraph } from '../interface/graph';
|
import { IGraph } from '../interface/graph';
|
||||||
|
import { INode } from '../interface/item';
|
||||||
|
|
||||||
const { PI, sin, cos } = Math;
|
const { PI, sin, cos } = Math;
|
||||||
|
|
||||||
@ -419,7 +420,7 @@ export const getTextSize = (text, fontSize) => {
|
|||||||
* @param nodes the nodes array
|
* @param nodes the nodes array
|
||||||
* @return the tree
|
* @return the tree
|
||||||
*/
|
*/
|
||||||
export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) => {
|
export const plainCombosToTrees = (array: ComboConfig[], nodes?: INode[]) => {
|
||||||
const result: ComboTree[] = [];
|
const result: ComboTree[] = [];
|
||||||
const addedMap = {};
|
const addedMap = {};
|
||||||
const modelMap = {};
|
const modelMap = {};
|
||||||
@ -429,6 +430,7 @@ export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) =
|
|||||||
|
|
||||||
array.forEach((d, i) => {
|
array.forEach((d, i) => {
|
||||||
const cd = clone(d);
|
const cd = clone(d);
|
||||||
|
cd.itemType = 'combo';
|
||||||
cd.children = undefined;
|
cd.children = undefined;
|
||||||
if (cd.parentId === cd.id) {
|
if (cd.parentId === cd.id) {
|
||||||
console.warn(`The parentId for combo ${cd.id} can not be the same as the combo's 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;
|
addedMap[cd.id] = cd;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeMap = {};
|
||||||
nodes && nodes.forEach(node => {
|
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) {
|
||||||
if (combo.children) combo.children.push(node);
|
const cnode: NodeConfig = {
|
||||||
else combo.children = [node];
|
id: nodeModel.id,
|
||||||
addedMap[node.id] = clone(node);
|
comboId: nodeModel.comboId as string
|
||||||
addedMap[node.id].itemType = 'node';
|
};
|
||||||
|
if (combo.children) combo.children.push(cnode);
|
||||||
|
else combo.children = [cnode];
|
||||||
|
cnode.itemType = 'node';
|
||||||
|
addedMap[nodeModel.id] = cnode;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
result.forEach((tree: ComboTree) => {
|
result.forEach((tree: ComboTree) => {
|
||||||
tree.depth = 0;
|
tree.depth = 0;
|
||||||
traverse<ComboTree>(tree, child => {
|
traverse<ComboTree>(tree, child => {
|
||||||
let parent = addedMap[child.parentId];
|
let parent;
|
||||||
if (addedMap[child.id]['itemType'] === 'node') {
|
if (addedMap[child.id]['itemType'] === 'node') {
|
||||||
parent = addedMap[child['comboId'] as string];
|
parent = addedMap[child['comboId'] as string];
|
||||||
|
} else {
|
||||||
|
parent = addedMap[child.parentId];
|
||||||
}
|
}
|
||||||
if (parent) {
|
if (parent) {
|
||||||
child.depth = parent.depth + 1;
|
child.depth = parent.depth + 1;
|
||||||
} else {
|
} else {
|
||||||
child.depth = 0;
|
child.depth = 0;
|
||||||
}
|
}
|
||||||
addedMap[child.id].depth = child.depth;
|
const oriNodeModel = nodeMap[child.id];
|
||||||
|
if (oriNodeModel) {
|
||||||
|
oriNodeModel.depth = child.depth;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -537,7 +553,7 @@ export const getComboBBox = (children: ComboTree[], graph: IGraph): BBox => {
|
|||||||
};
|
};
|
||||||
children && children.forEach(child => {
|
children && children.forEach(child => {
|
||||||
const childItem = graph.findById(child.id);
|
const childItem = graph.findById(child.id);
|
||||||
// const childModel = childItem.getModel();
|
childItem.set('bboxCanvasCache', undefined);
|
||||||
const childBBox = childItem.getCanvasBBox();
|
const childBBox = childItem.getCanvasBBox();
|
||||||
if (childBBox.x && comboBBox.minX > childBBox.minX) comboBBox.minX = childBBox.minX;
|
if (childBBox.x && comboBBox.minX > childBBox.minX) comboBBox.minX = childBBox.minX;
|
||||||
if (childBBox.y && comboBBox.minY > childBBox.minY) comboBBox.minY = childBBox.minY;
|
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 FruchtermanWorker from './component/fruchterman-worker-layout';
|
||||||
import AddNodeLayout from './component/addNodeLayout'
|
import AddNodeLayout from './component/addNodeLayout'
|
||||||
import ChangeData from './component/changeData'
|
import ChangeData from './component/changeData'
|
||||||
|
import G6ForceLayout from './component/g6force-layout';
|
||||||
|
|
||||||
export default { title: 'Layout' };
|
export default { title: 'Layout' };
|
||||||
|
|
||||||
@ -15,3 +16,5 @@ storiesOf('Layout', module)
|
|||||||
.add('Fruchterman worker layout', () => <FruchtermanWorker />)
|
.add('Fruchterman worker layout', () => <FruchtermanWorker />)
|
||||||
.add('add node and layout', () => <AddNodeLayout />)
|
.add('add node and layout', () => <AddNodeLayout />)
|
||||||
.add('change data', () => <ChangeData />)
|
.add('change data', () => <ChangeData />)
|
||||||
|
.add('G6 force layout', () => <G6ForceLayout />)
|
||||||
|
.add('Fruchterman worker layout', () => <FruchtermanWorker />);
|
||||||
|
Loading…
Reference in New Issue
Block a user