feat: layout for combo. feat: nonoverlap for combo layout in first stage.

This commit is contained in:
Yanyan-Wang 2020-04-07 20:21:07 +08:00 committed by Yanyan Wang
parent 811f5bc18c
commit 4631708709
11 changed files with 1673 additions and 33 deletions

View File

@ -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;
} }

View File

@ -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
View 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;
});
}
}

View File

@ -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
View 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);
});
}
}

View File

@ -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,

View File

@ -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() {}

View File

@ -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;
} }

View File

@ -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;

View 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;

View File

@ -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 />);