fix: revert clustering for force layout pull request from community; fix: update the typings for G6Event; docs: update the event api docs

This commit is contained in:
shiwu 2020-10-27 10:56:41 +08:00 committed by Yanyan Wang
parent 1d75524307
commit 40e9626914
13 changed files with 788 additions and 90 deletions

View File

@ -15,6 +15,7 @@
- feat: add edgeConfig for create-edge behavior, closes: #2195;
- fix: remove the source node while creat-edge;
- feat: create-edge for combo, closes: #2211;
- fix: update the typings for G6Event;
#### 3.8.1
- fix: update edge states with updateItem problem, closes: #2142;

View File

@ -148,38 +148,52 @@ graph.on(timingEventName, evt => {
| Event Name | Description |
| --- | --- |
| beforeadditem | Activated before `add` / `addItem` being called. |
| afteradditem | Activated after `add` / `addItem` being called. |
| beforeremoveitem | Activated before `remove` / `removeItem` being called. |
| afterremoveitem | Activated after `remove` / `removeItem` being called. |
| beforeupdateitem | Activated before `update` / `updateItem` being called. |
| afterupdateitem | Activated after `update` / `updateItem` being called. |
| beforeitemvisibilitychange | Activated before `showItem` / `hideItem` being called. |
| afteritemvisibilitychange | Activated after `showItem` / `hideItem` being called. |
| beforeitemstatechange | Activated before `setItemState` being called. |
| afteritemstatechange | Activated after `setItemState` being called. |
| beforeitemrefresh | Activated before `refreshItem` being called. |
| afteritemrefresh | Activated after `refreshItem` being called. |
| beforeitemstatesclear | Activated before `clearItemStates` being called. |
| afteritemstatesclear | Activated after `clearItemStates` being called. |
| beforemodechange | Activated before `setMode` / `addBehaviors` / `removeBehaviors` being called. |
| aftermodechange | Activated after `setMode` / `addBehaviors` / `removeBehaviors` being called. |
| beforelayout | Activated before graph layout. `render` will layout the graph, so `render` will activate this event as well. |
| afterlayout | Activated after graph layout being done. `render` will layout the graph, so `render` will activate this event as well. |
| beforerender | Activated before `graph.render` / `graph.read` being called. |
| afterrender | Activated after `graph.render` / `graph.read` being called. |
| beforeadditem | Activated before `graph.add` / `graph.addItem` being called. |
| afteradditem | Activated after `graph.add` / `graph.addItem` being called. |
| beforeremoveitem | Activated before `graph.remove` / `graph.removeItem` being called. |
| afterremoveitem | Activated after `graph.remove` / `graph.removeItem` being called. |
| beforeupdateitem | Activated before `graph.update` / `graph.updateItem` being called. |
| afterupdateitem | Activated after `graph.update` / `graph.updateItem` being called. |
| beforeitemvisibilitychange | Activated before `graph.showItem` / `graph.hideItem` being called. |
| afteritemvisibilitychange | Activated after `graph.showItem` / `graph.hideItem` being called. |
| beforeitemstatechange | Activated before `graph.setItemState` being called. |
| afteritemstatechange | Activated after `graph.setItemState` being called. |
| beforeitemrefresh | Activated before `graph.refreshItem` being called. |
| afteritemrefresh | Activated after `graph.refreshItem` being called. |
| beforeitemstatesclear | Activated before `graph.clearItemStates` being called. |
| afteritemstatesclear | Activated after `graph.clearItemStates` being called. |
| beforemodechange | Activated before `graph.setMode` / `graph.addBehaviors` / `graph.removeBehaviors` being called. |
| aftermodechange | Activated after `graph.setMode` / `graph.addBehaviors` / `graph.removeBehaviors` being called. |
| beforelayout | Activated before graph layout. `graph.render` will layout the graph, so `graph.render` will activate this event as well. |
| afterlayout | Activated after graph layout being done. `graph.render` will layout the graph, so `graph.render` will activate this event as well. |
| beforegraphrefreshposition | Activated before `graph.refreshPositions` beging called |
| aftergraphrefreshposition | Activated after `graph.refreshPositions` beging called |
| beforegraphrefresh | Activated before `graph.refresh` beging called |
| aftergraphrefresh | Activated after `graph.refresh` beging called |
| beforeanimate | Activated before global animation |
| afteranimate | Activated after global animation |
| beforecreateedge | Activated before an edge is created by the built-in behavior `create-edge` |
| aftercreateedge | Activated after an edge is created by the built-in behavior `create-edge` |
| graphstatechange | Activated after `graph.updateItemState` being called. |
| afteractivaterelations | Activated while activating a node by `'activate-relations'` Behavior which is assigned to the the instance of Graph. |
| nodeselectchange | Activated while the selected items are changed by `'brush-select'`, `'click-select'` or `'lasso-select'` Behavior which is assigned to the instance of Graph. |
| beforecreateedge | Activated before an edge is created by the built-in behavior `create-edge` |
| aftercreateedge | Activated after an edge is created by the built-in behavior `create-edge` |
| itemcollapsed | Activated while a node is clicked to collapse or expand by `'collapse-expand'` Behavior which is assigned to the instance of TreeGraph. |
| tooltipchange | Activated after the show/hide state is changed by `'tooltip'` or `'edge-tooltip'` Behavior which is assigned to the instance of Graph. |
| wheelzoom | Activated after the canvas being zoomed by `'zoom-canvas'` Behavior which is assigned to the instance of Graph. |
| dragnodeend | Activated while drag node end by `'drag-node'` Behavior |
| wheelzoom | Activated after the canvas is zoomed by `'zoom-canvas'` Behavior which is assigned to the instance of Graph. |
| viewportchange | Activated after the canvas is translated by `graph.moveTo`, `graph.translate`, and `graph.zoom`. |
| dragnodeend | Activated while drag node end by `'drag-node'` Behavior. |
| stackchange | Activated while the redo or undo stacks are changed. |
### Callback Parameters
The callback paramters are different from custom events.
#### beforerender / afterrender
No parameters.
#### beforeadditem
| Name | Type | Description |
@ -303,9 +317,24 @@ No parameters.
| deltaY | Number | The y-axis direction of the wheel scroll, value is `1`, `0`, or `-1`, where `0` means no scrolling on this direction. |
| ... Other parameters of wheel event. | | |
#### viewportchange
| Name | Type | Description |
| ------ | ------ | ----------------------------------------------- |
| action | 'translate' / 'move' / 'zoom' | The action of view port changing. |
| matrix | Array | The matrix of the graph after the view port changed. |
#### dragnodeend
| Name | Type | Description |
| ------ | ------ | ----------------------------------------------- |
| items | Item[] | The manipulated items. |
| targetItem | null/Node/Combo | The position where the node is placed after dragging, the default is null, that is, placed on the canvas. |
#### stackchange
| Name | Type | Description |
| ------ | ------ | ----------------------------------------------- |
| redoStack | Object[] | The redo stack. |
| undoStack | Object[] | The undo stack. |

View File

@ -151,6 +151,8 @@ graph.on(timingEventName, evt => {
| 事件名称 | 描述 |
| --- | --- |
| beforerender | 调用 `graph.render` / `graph.read` 方法之前触发 |
| afterrender | 调用 `graph.render` / `graph.read` 方法之后触发 |
| beforeadditem | 调用 `graph.add` / `graph.addItem` 方法之前触发 |
| afteradditem | 调用 `graph.add` / `graph.addItem` 方法之后触发 |
| beforeremoveitem | 调用 `graph.remove` / `graph.removeItem` 方法之前触发 |
@ -169,20 +171,32 @@ graph.on(timingEventName, evt => {
| aftermodechange | 调用 `graph.setMode` / `graph.addBehaviors` / `graph.removeBehaviors` 方法之后触发 |
| beforelayout | 布局前触发。调用 `graph.render` 时会进行布局,因此 `render` 时会触发。或用户主动调用图的 `graph.layout` 时触发。 |
| afterlayout | 布局完成后触发。调用 `graph.render` 时会进行布局,因此 `render` 时布局完成后会触发。或用户主动调用图的 `lgraph.ayout` 时布局完成后触发。 |
| beforegraphrefreshposition | `graph.refreshPositions` 被调用前触发 |
| aftergraphrefreshposition | `graph.refreshPositions` 被调用后触发 |
| beforegraphrefresh | `graph.refresh` 被调用前触发 |
| aftergraphrefresh | `graph.refresh` 被调用后触发 |
| beforeanimate | 全局动画发生前触发 |
| afteranimate | 全局动画发生后触发 |
| beforecreateedge | 使用内置交互 `create-edge`,创建边之前触发 |
| aftercreateedge | 使用内置交互 `create-edge`,创建边之后触发 |
| graphstatechange | 调用 `graph.updateItemState` 方法之后触发 |
| afteractivaterelations | 使用了 `'activate-relations'` Behavior 并触发了该行为后,该事件被触发 |
| nodeselectchange | 使用了 `'brush-select'` , `'click-select'``'lasso-select'` Behavior 且选中元素发生变化时,该事件被触发 |
| beforecreateedge | 使用内置交互 `create-edge`,创建边之前触发 |
| aftercreateedge | 使用内置交互 `create-edge`,创建边之后触发 |
| itemcollapsed | 在 TreeGraph 上使用了 `'collapse-expand'` Behavior 并触发了该行为后,该事件被触发 |
| tooltipchange | 使用了 `'tooltip'``'edge-tooltip'` Behavior 且 tooltip 的显示/隐藏被改变后,该事件被触发 |
| wheelzoom | 使用了 `'zoom-canvas'` Behavior 并用滚轮对图进行缩放后,该事件被触发 |
| viewportchange |调用 `graph.moveTo``graph.translate`,或 `graph.zoom` 均会触发该事件 |
| dragnodeend | 使用了 `'drag-node'` Behavior当拖动结束时该事件被触发 |
| stackchange | 撤销/重做栈发生变化时,该事件触发 |
### 回调参数
不同时机监听事件的回调参数不同,下面针对各个自定义事件的回调参数进行说明。
#### beforerender / afterrender
无参数
#### beforeadditem
| 名称 | 类型 | 描述 |
@ -298,9 +312,24 @@ graph.on(timingEventName, evt => {
| deltaY | Number | 滚动的 y 方向,取值 `1``0``-1``0` 代表没有该方向的滚动 |
| ... 其他滚轮事件的回调参数 | | |
#### viewportchange
| 名称 | 类型 | 描述 |
| ------ | ------ | ----------------------------------------------- |
| action | 'translate' / 'move' / 'zoom' | 视窗变换的类型,`'translate'`、`'move'`、`'zoom'` 分别标识该时机是由 `graph.translate`、`graph.move`、还是 `graph.zoom` 函数的调用而产生 |
| matrix | Array | 视窗变换后的图的矩阵 |
#### dragnodeend
| 名称 | 类型 | 描述 |
| ------ | ------ | --------------------------------------------- |
| items | Item[] | 当前操作的 item 实例 |
| targetItem | null/Node/Combo | 拖动节点结束后节点是放到canvas、Node 还是 Combo 上面 |
| targetItem | null/Node/Combo | 拖动节点结束后节点是放到canvas、Node 还是 Combo 上面 |
#### stackchange
| 名称 | 类型 | 描述 |
| ------ | ------ | ----------------------------------------------- |
| redoStack | Object[] | 重做堆栈 |
| undoStack | Object[] | 撤销堆栈 |

View File

@ -3281,7 +3281,7 @@ export default class Graph extends EventEmitter implements IGraph {
this.get('layoutController').destroy();
this.get('customGroupControll').destroy();
this.get('canvas').destroy();
if (this.get('graphWaterMarker')) {
this.get('graphWaterMarker').destroy();
}
@ -3363,13 +3363,13 @@ export default class Graph extends EventEmitter implements IGraph {
* @param {WaterMarkerConfig} config
*/
public setImageWaterMarker(
imgURL: string = Global.waterMarkerImage,
config?: WaterMarkerConfig) {
imgURL: string = Global.waterMarkerImage,
config?: WaterMarkerConfig) {
let container: string | HTMLElement | null = this.get('container')
if (isString(container)) {
container = document.getElementById(container)
}
if (!container.style.position) {
container.style.position = 'relative'
}
@ -3403,23 +3403,23 @@ export default class Graph extends EventEmitter implements IGraph {
const img = new Image()
img.crossOrigin = 'anonymous'
img.src = imgURL
img.onload = () => {
img.onload = () => {
ctx.drawImage(img, x, y, image.width, image.height)
// 恢复旋转角度
ctx.rotate((rotate * Math.PI) / 180)
// 默认按照现代浏览器处理
if (!compatible) {
let box = document.querySelector('.g6-graph-watermarker') as HTMLElement
if (!box) {
box = document.createElement('div')
box.className = 'g6-graph-watermarker'
box.className = 'g6-graph-watermarker'
}
box.className = 'g6-graph-watermarker'
if (!canvas.destroyed) {
box.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;z-index:-1;`;
box.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;z-index:-1;`;
(container as HTMLElement).appendChild(box)
}
}
} else {
// 当需要兼容不支持 pointer-events属性的浏览器时将 compatible 设置为 true
(container as HTMLElement).style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;`
@ -3433,12 +3433,12 @@ export default class Graph extends EventEmitter implements IGraph {
* @param {string[]} texts
* @param {WaterMarkerConfig} config
*/
public setTextWaterMarker(texts: string[], config?: WaterMarkerConfig) {
public setTextWaterMarker(texts: string[], config?: WaterMarkerConfig) {
let container: string | HTMLElement | null = this.get('container')
if (isString(container)) {
container = document.getElementById(container)
}
if (!container.style.position) {
container.style.position = 'relative'
}
@ -3462,7 +3462,7 @@ export default class Graph extends EventEmitter implements IGraph {
canvas = new GCanvas(canvasCfg)
this.set('graphWaterMarker', canvas)
}
canvas.get('el').style.display = 'none'
canvas.get('el').style.display = 'none'
const ctx = canvas.get('context')
const { rotate, fill, fontFamily, fontSize, baseline, x, y, lineHeight } = text
@ -3475,12 +3475,12 @@ export default class Graph extends EventEmitter implements IGraph {
// 设置文字颜色
ctx.fillStyle = fill
ctx.textBaseline = baseline
ctx.textBaseline = baseline
for (let i = texts.length - 1; i >= 0; i--) {
for (let i = texts.length - 1; i >= 0; i--) {
// 将文字绘制到画布
ctx.fillText(texts[i], x, y + i * lineHeight)
}
ctx.fillText(texts[i], x, y + i * lineHeight)
}
// 恢复旋转角度
ctx.rotate((rotate * Math.PI) / 180)
@ -3490,13 +3490,13 @@ export default class Graph extends EventEmitter implements IGraph {
let box = document.querySelector('.g6-graph-watermarker') as HTMLElement
if (!box) {
box = document.createElement('div')
box.className = 'g6-graph-watermarker'
box.className = 'g6-graph-watermarker'
}
box.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;z-index:99;`;
box.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;z-index:99;`;
container.appendChild(box)
} else {
// 当需要兼容不支持 pointer-events属性的浏览器时将 compatible 设置为 true
container.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;`;
container.style.cssText = `background-image: url(${canvas.get('el').toDataURL('image/png')});background-repeat:repeat;`;
}
}
}

View File

@ -20,7 +20,8 @@ import {
GraphAnimateConfig,
StackData,
HullCfg,
WaterMarkerConfig
WaterMarkerConfig,
G6Event
} from '../types';
import { IEdge, INode, ICombo } from './item';
import Hull from '../item/hull';
@ -706,7 +707,7 @@ export interface ITreeGraph extends IGraph {
* @param {string} imgURL url地址
* @param {WaterMarkerConfig} config
*/
setImageWaterMarker(imgURL: string , config: WaterMarkerConfig);
setImageWaterMarker(imgURL: string, config: WaterMarkerConfig);
/**
*

View File

@ -0,0 +1,366 @@
import * as d3Force from 'd3-force';
// https://github.com/john-guerra/forceInABox/blob/master/src/forceInABox.js
export default function forceInABox() {
function constant(_: any): () => any {
return () => _;
}
let groupBy = (d) => {
return d.cluster;
};
let forceNodeSize: (() => number) | ((d: any) => number) = constant(1);
let forceCharge: (() => number) | ((d: any) => number) = constant(-1);
let forceLinkDistance: (() => number) | ((d: any) => number) = constant(100);
let forceLinkStrength: (() => number) | ((d: any) => number) = constant(0.1);
let offset = [0, 0];
let nodes = [];
let nodesMap = {};
let links = [];
let centerX = 100;
let centerY = 100;
let foci = { none: { x: 0, y: 0 } };
let templateNodes = [];
let templateForce;
let template = 'force';
let enableGrouping = true;
let strength = 0.1;
function force(alpha) {
if (!enableGrouping) {
return force;
}
templateForce.tick();
getFocisFromTemplate();
for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
node.vx += (foci[groupBy(node)].x - node.x) * k;
node.vy += (foci[groupBy(node)].y - node.y) * k;
}
}
function initialize() {
if (!nodes) return;
initializeWithForce();
}
function initializeWithForce() {
if (!nodes || !nodes.length) {
return;
}
if (groupBy(nodes[0]) === undefined) {
throw Error(
"Couldnt find the grouping attribute for the nodes. Make sure to set it up with forceInABox.groupBy('clusterAttr') before calling .links()",
);
}
// checkLinksAsObjects();
const net = getGroupsGraph();
templateForce = d3Force
.forceSimulation(net.nodes)
.force('x', d3Force.forceX(centerX).strength(0.1))
.force('y', d3Force.forceY(centerY).strength(0.1))
.force('collide', d3Force.forceCollide((d: { r: any }) => d.r).iterations(4))
.force('charge', d3Force.forceManyBody().strength(forceCharge))
.force(
'links',
d3Force
.forceLink(net.nodes.length ? net.links : [])
.distance(forceLinkDistance)
.strength(forceLinkStrength),
);
templateNodes = templateForce.nodes();
getFocisFromTemplate();
}
function getGroupsGraph() {
const gnodes = [];
const glinks = [];
const dNodes = {};
let clustersList = [],
clustersCounts = {},
clustersLinks = [];
clustersCounts = computeClustersNodeCounts(nodes);
clustersLinks = computeClustersLinkCounts(links);
clustersList = Object.keys(clustersCounts);
clustersList.forEach((key, index) => {
const val = clustersCounts[key];
// Uses approx meta-node size
gnodes.push({
id: key,
size: val.count,
r: Math.sqrt(val.sumforceNodeSize / Math.PI),
});
dNodes[key] = index;
});
clustersLinks.forEach((l) => {
const source = dNodes[l.source];
const target = dNodes[l.target];
if (source !== undefined && target !== undefined) {
glinks.push({
source,
target,
count: l.count,
});
}
});
return { nodes: gnodes, links: glinks };
}
function computeClustersNodeCounts(nodes) {
const clustersCounts = {};
nodes.forEach((d) => {
const key = groupBy(d);
if (!clustersCounts[key]) {
clustersCounts[key] = { count: 0, sumforceNodeSize: 0 };
}
});
nodes.forEach((d: any) => {
const key = groupBy(d);
const nodeSize = forceNodeSize(d);
const tmpCount = clustersCounts[key];
tmpCount.count = tmpCount.count + 1;
tmpCount.sumforceNodeSize = tmpCount.sumforceNodeSize + Math.PI * (nodeSize * nodeSize) * 1.3;
clustersCounts[key] = tmpCount;
});
return clustersCounts;
}
function computeClustersLinkCounts(links) {
const dClusterLinks = {};
const clusterLinks = [];
links.forEach((l) => {
const key = getLinkKey(l);
let count = 0;
if (dClusterLinks[key] !== undefined) {
count = dClusterLinks[key];
}
count += 1;
dClusterLinks[key] = count;
});
const entries = Object.entries(dClusterLinks);
entries.forEach(([key, count]) => {
const source = key.split('~')[0];
const target = key.split('~')[1];
if (source !== undefined && target !== undefined) {
clusterLinks.push({
source,
target,
count,
});
}
});
return clusterLinks;
}
function getFocisFromTemplate() {
foci = { none: { x: 0, y: 0 } };
templateNodes.forEach((d) => {
foci[d.id] = {
x: d.x - offset[0],
y: d.y - offset[1],
};
});
return foci;
}
function getLinkKey(l) {
const sourceID = groupBy(nodesMap[l.source]);
const targetID = groupBy(nodesMap[l.target]);
return sourceID <= targetID ? `${sourceID}~${targetID}` : `${targetID}~${sourceID}`;
}
function genNodesMap(nodes) {
nodesMap = {};
nodes.forEach((node) => {
nodesMap[node.id] = node;
});
}
function setTemplate(x) {
if (!arguments.length) return template;
template = x;
initialize();
return force;
}
function setGroupBy(x) {
if (!arguments.length) return groupBy;
if (typeof x === 'string') {
groupBy = (d) => {
return d[x];
};
return force;
}
groupBy = x;
return force;
}
function setEnableGrouping(x) {
if (!arguments.length) return enableGrouping;
enableGrouping = x;
return force;
}
function setStrength(x) {
if (!arguments.length) return strength;
strength = x;
return force;
}
function setCenterX(_) {
if (arguments.length) {
centerX = _;
return force;
}
return centerX;
}
function setCenterY(_) {
if (arguments.length) {
centerY = _;
return force;
}
return centerY;
}
function setNodes(_) {
if (arguments.length) {
genNodesMap(_ || []);
nodes = _ || [];
return force;
}
return nodes;
}
function setLinks(_) {
if (arguments.length) {
links = _ || [];
initialize();
return force;
}
return links;
}
function setForceNodeSize(_) {
if (arguments.length) {
if (typeof _ === 'function') {
forceNodeSize = _;
} else {
forceNodeSize = constant(+_);
}
initialize();
return force;
}
return forceNodeSize;
}
function setForceCharge(_) {
if (arguments.length) {
if (typeof _ === 'function') {
forceCharge = _;
} else {
forceCharge = constant(+_);
}
initialize();
return force;
}
return forceCharge;
}
function setForceLinkDistance(_) {
if (arguments.length) {
if (typeof _ === 'function') {
forceLinkDistance = _;
} else {
forceLinkDistance = constant(+_);
}
initialize();
return force;
}
return forceLinkDistance;
}
function setForceLinkStrength(_) {
if (arguments.length) {
if (typeof _ === 'function') {
forceLinkStrength = _;
} else {
forceLinkStrength = constant(+_);
}
initialize();
return force;
}
return forceLinkStrength;
}
function setOffset(_) {
if (arguments.length) {
offset = _;
return force;
}
return offset;
}
force.initialize = (_) => {
nodes = _;
initialize();
};
force.template = setTemplate;
force.groupBy = setGroupBy;
force.enableGrouping = setEnableGrouping;
force.strength = setStrength;
force.centerX = setCenterX;
force.centerY = setCenterY;
force.nodes = setNodes;
force.links = setLinks;
force.forceNodeSize = setForceNodeSize;
// Legacy support
force.nodeSize = force.forceNodeSize;
force.forceCharge = setForceCharge;
force.forceLinkDistance = setForceLinkDistance;
force.forceLinkStrength = setForceLinkStrength;
force.offset = setOffset;
force.getFocis = getFocisFromTemplate;
return force;
}

View File

@ -3,18 +3,18 @@
* @author shiwu.wyy@antfin.com
*/
import { GraphData, IPointTuple } from '../types';
import { GraphData, IPointTuple } from '../../types';
import * as d3Force from 'd3-force';
import forceInABox from './force-in-a-box';
import isArray from '@antv/util/lib/is-array';
import isFunction from '@antv/util/lib/is-function';
import isNumber from '@antv/util/lib/is-number';
import mix from '@antv/util/lib/mix';
import { BaseLayout } from './layout';
import { LAYOUT_MESSAGE } from './worker/layoutConst';
import { clone } from '@antv/util';
import { BaseLayout } from '../layout';
import { LAYOUT_MESSAGE } from '../worker/layoutConst';
/**
* force-directed
@ -38,6 +38,24 @@ export default class ForceLayout<Cfg = any> extends BaseLayout {
/** 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离) */
public nodeSpacing: ((d?: unknown) => number) | undefined;
/** 是否支持按类聚合 */
public clustering: boolean;
/** 聚类节点作用力 */
public clusterNodeStrength: number | null = null;
/** 聚类边作用力 */
public clusterEdgeStrength: number | null = null;
/** 聚类边长度 */
public clusterEdgeDistance: number | null = null;
/** 聚类节点大小 / 直径,直径越大,越分散 */
public clusterNodeSize: number | null = null;
/** 用于 foci 的力 */
public clusterFociStrength: number | null = null;
/** 默认边长度 */
public linkDistance: number = 50;
@ -59,19 +77,21 @@ export default class ForceLayout<Cfg = any> extends BaseLayout {
/** 是否启用web worker。前提是在web worker里执行布局否则无效 */
public workerEnabled: boolean = false;
public tick: () => void = () => {};
public tick: () => void = () => { };
/** 布局完成回调 */
public onLayoutEnd: () => void = () => {};
public onLayoutEnd: () => void = () => { };
/** 布局每一次迭代完成的回调 */
public onTick: () => void = () => {};
public onTick: () => void = () => { };
/** 是否正在布局 */
private ticking: boolean | undefined = undefined;
private edgeForce: any;
private clusterForce: any;
public getDefaultCfg() {
return {
center: [0, 0],
@ -86,9 +106,15 @@ export default class ForceLayout<Cfg = any> extends BaseLayout {
alphaMin: 0.001,
alpha: 0.3,
collideStrength: 1,
tick() {},
onLayoutEnd() {}, // 布局完成回调
onTick() {}, // 每一迭代布局回调
clustering: false,
clusterNodeStrength: -1,
clusterEdgeStrength: 0.1,
clusterEdgeDistance: 100,
clusterFociStrength: 0.8,
clusterNodeSize: 10,
tick() { },
onLayoutEnd() { }, // 布局完成回调
onTick() { }, // 每一迭代布局回调
// 是否启用web worker。前提是在web worker里执行布局否则无效
workerEnabled: false,
};
@ -137,14 +163,37 @@ export default class ForceLayout<Cfg = any> extends BaseLayout {
if (self.nodeStrength) {
nodeForce.strength(self.nodeStrength);
}
simulation = d3Force
.forceSimulation()
.nodes(nodes)
simulation = d3Force.forceSimulation().nodes(nodes);
if (self.clustering) {
const clusterForce = forceInABox() as any;
clusterForce
.centerX(self.center[0])
.centerY(self.center[1])
.template('force')
.strength(self.clusterFociStrength);
if (edges) {
clusterForce.links(edges);
}
if (nodes) {
clusterForce.nodes(nodes);
}
clusterForce
.forceLinkDistance(self.clusterEdgeDistance)
.forceLinkStrength(self.clusterEdgeStrength)
.forceCharge(self.clusterNodeStrength)
.forceNodeSize(self.clusterNodeSize);
self.clusterForce = clusterForce;
simulation.force('group', clusterForce);
}
simulation
.force('center', d3Force.forceCenter(self.center[0], self.center[1]))
.force('charge', nodeForce)
.alpha(alpha)
.alphaDecay(alphaDecay)
.alphaMin(alphaMin);
if (self.preventOverlap) {
self.overlapProcess(simulation);
}
@ -204,6 +253,10 @@ export default class ForceLayout<Cfg = any> extends BaseLayout {
}
} else {
if (reloadData) {
if (self.clustering && self.clusterForce) {
self.clusterForce.nodes(nodes);
self.clusterForce.links(edges);
}
simulation.nodes(nodes);
self.edgeForce.links(edges);
}

View File

@ -9,7 +9,7 @@ import Layout from './layout';
import Circular from './circular';
import Concentric from './concentric';
import Dagre from './dagre';
import Force from './force';
import Force from './force/force';
import G6Force from './g6force';
import Fruchterman from './fruchterman';
import Grid from './grid';

View File

@ -561,10 +561,11 @@ export interface GroupNodeIds {
// Behavior type file
export enum G6Event {
// common events
CLICK = 'click',
DBLCLICK = 'dblclick',
MOUSEDOWN = 'mousedown',
MOUDEUP = 'mouseup',
DBLCLICK = 'dblclick',
CONTEXTMENU = 'contextmenu',
MOUSEENTER = 'mouseenter',
MOUSEOUT = 'mouseout',
@ -585,57 +586,125 @@ export enum G6Event {
FOCUS = 'focus',
BLUR = 'blur',
// touch events
TOUCHSTART = 'touchstart',
TOUCHMOVE = 'touchmove',
TOUCHEND = 'touchend',
NODE_CLICK = 'node:click',
// node events
NODE_CONTEXTMENU = 'node:contextmenu',
NODE_CLICK = 'node:click',
NODE_DBLCLICK = 'node:dblclick',
NODE_DRAGSTART = 'node:dragstart',
NODE_DRAG = 'node:drag',
NODE_DRAGEND = 'node:dragend',
NODE_MOUSEDOWN = 'node:mousedown',
NODE_MOUSEUP = 'node:mouseup',
NODE_MOUSEENTER = 'node:mouseenter',
NODE_MOUSELEAVE = 'node:mouseleave',
NODE_MOUSEMOVE = 'node:mousemove',
NODE_MOUSEOUT = 'node:mouseout',
NODE_MOUSEOVER = 'node:mouseover',
NODE_DROP = 'node:drop',
NODE_DRAGOVER = 'node:dragover',
NODE_DRAGENTER = 'node:dragenter',
NODE_DRAGLEAVE = 'node:dragleave',
EDGE_CLICK = 'edge:click',
EDGE_CONTEXTMENU = 'edge:contextmenu',
EDGE_DBLCLICK = 'edge:dblclick',
EDGE_MOUSEENTER = 'edge:mouseenter',
EDGE_MOUSELEAVE = 'edge:mouseleave',
EDGE_MOUSEMOVE = 'edge:mousemove',
CANVAS_MOUSEDOWN = 'canvas:mousedown',
CANVAS_MOUSEMOVE = 'canvas:mousemove',
CANVAS_MOUSEUP = 'canvas:mouseup',
CANVAS_CLICK = 'canvas:click',
CANVAS_MOSUELEAVE = 'canvas:mouseleave',
CANVAS_DRAGSTART = 'canvas:dragstart',
CANVAS_DRAG = 'canvas:drag',
CANVAS_DRAGEND = 'canvas:dragend',
CANVAS_DRAGLEAVE = 'canvas:dragleave',
CANVAS_DROP = 'canvas:drop',
// combo
COMBO_CLICK = 'combo:click',
NODE_DRAGSTART = 'node:dragstart',
NODE_DRAG = 'node:drag',
NODE_DRAGEND = 'node:dragend',
// combo, extends from nodes
COMBO_CONTEXTMENU = 'combo:contextmenu',
COMBO_CLICK = 'combo:click',
COMBO_DBLCLICK = 'combo:dblclick',
COMBO_DRAGSTART = 'combo:dragstart',
COMBO_DRAG = 'combo:drag',
COMBO_DRAGEND = 'combo:dragend',
COMBO_MOUSEDOWN = 'combo:mousedown',
COMBO_MOUSEUP = 'combo:mouseup',
COMBO_MOUSEENTER = 'combo:mouseenter',
COMBO_MOUSELEAVE = 'combo:mouseleave',
COMBO_MOUSEMOVE = 'combo:mousemove',
COMBO_MOUSEOUT = 'combo:mouseout',
COMBO_MOUSEOVER = 'combo:mouseover',
COMBO_DROP = 'combo:drop',
COMBO_DRAGOVER = 'combo:dragover',
COMBO_DRAGLEAVE = 'combo:dragleave',
COMBO_DRAGENTER = 'combo:dragenter',
COMBO_DRAGLEAVE = 'combo:dragleave',
COMBO_DRAGSTART = 'combo:dragstart',
COMBO_DRAG = 'combo:drag',
COMBO_DRAGEND = 'combo:dragend',
// edge events
EDGE_CONTEXTMENU = 'edge:contextmenu',
EDGE_CLICK = 'edge:click',
EDGE_DBLCLICK = 'edge:dblclick',
EDGE_MOUSEDOWN = 'edge:mousedown',
EDGE_MOUSEUP = 'edge:mouseup',
EDGE_MOUSEENTER = 'edge:mouseenter',
EDGE_MOUSELEAVE = 'edge:mouseleave',
EDGE_MOUSEMOVE = 'edge:mousemove',
EDGE_MOUSEOUT = 'edge:mouseout',
EDGE_MOUSEOVER = 'edge:mouseover',
EDGE_DROP = 'edge:drop',
EDGE_DRAGOVER = 'edge:dragover',
EDGE_DRAGENTER = 'edge:dragenter',
EDGE_DRAGLEAVE = 'edge:dragleave',
// canvas events
CANVAS_CONTEXTMENU = 'canvas:contextmenu',
CANVAS_CLICK = 'canvas:click',
CANVAS_DBLCLICK = 'canvas:dblclick',
CANVAS_MOUSEDOWN = 'canvas:mousedown',
CANVAS_MOUSEUP = 'canvas:mouseup',
CANVAS_MOUSEENTER = 'canvas:mouseenter',
CANVAS_MOUSELEAVE = 'canvas:mouseleave',
CANVAS_MOUSEMOVE = 'canvas:mousemove',
CANVAS_MOUSEOUT = 'canvas:mouseout',
CANVAS_MOUSEOVER = 'canvas:mouseover',
CANVAS_DROP = 'canvas:drop',
CANVAS_DRAGENTER = 'canvas:dragenter',
CANVAS_DRAGLEAVE = 'canvas:dragleave',
CANVAS_DRAGSTART = 'canvas:dragstart',
CANVAS_DRAG = 'canvas:drag',
CANVAS_DRAGEND = 'canvas:dragend',
// timing events
BEFORERENDER = 'beforerender',
AFTERRENDER = 'afterrender',
BEFOREADDITEM = 'beforeadditem',
AFTERADDITEM = 'afteradditem',
BEFOREREMOVEITEM = 'beforeremoveitem',
AFTERREMOVEITEM = 'afterremoveitem',
BEFOREUPDATEITEM = 'beforeupdateitem',
AFTERUPDATEITEM = 'afterupdateitem',
BEFOREITEMVISIBILITYCHANGE = 'beforeitemvisibilitychange',
AFTERITEMVISIBILITYCHANGE = 'afteritemvisibilitychange',
BEFOREITEMSTATECHANGE = 'beforeitemstatechange',
AFTERITEMSTATECHANGE = 'afteritemstatechange',
BEFOREITEMREFRESH = 'beforeitemrefresh',
AFTERITEMREFRESH = 'afteritemrefresh',
BEFOREITEMSTATESCLEAR = 'beforeitemstatesclear',
AFTERITEMSTATESCLEAR = 'afteritemstatesclear',
BEFOREMODECHANGE = 'beforemodechange',
AFTERMODECHANGE = 'aftermodechange',
BEFORELAYOUT = 'beforelayout',
AFTERLAYOUT = 'afterlayout',
BEFORECREATEEDGE = 'beforecreateedge',
AFTERCREATEEDGE = 'aftercreateedge',
BEFOREGRAPHREFRESHPOSITION = 'beforegraphrefreshposition',
AFTERGRAPHREFRESHPOSITION = 'aftergraphrefreshposition',
BEFOREGRAPHREFRESH = 'beforegraphrefresh',
AFTERGRAPHREFRESH = 'aftergraphrefresh',
BEFOREANIMATE = 'beforeanimate',
AFTERANIMATE = 'afteranimate',
BEFOREPAINT = 'beforepaint',
AFTERPAINT = 'afterpaint',
GRAPHSTATECHANGE = 'graphstatechange',
AFTERACTIVATERELATIONS = 'afteractivaterelations',
NODESELECTCHANGE = 'nodeselectchange',
TOOLTIPCHANGE = 'tooltipchange',
WHEELZOOM = 'wheelzoom',
VIEWPORTCHANGE = 'viewportchange',
DRAGNODEEND = 'dragnodeend',
STACKCHANGE = 'stackchange'
}
export type DefaultBehaviorType = IG6GraphEvent | string | number | object;
export interface BehaviorOption {

View File

@ -0,0 +1,76 @@
import React, { useRef, useEffect } from 'react';
import G6 from '../../../src';
import { IGraph } from '../../../src/interface/graph';
import { G6Event } from '../../../src/types';
G6.registerNode(
"card-node",
{
drawShape: function drawShape(cfg, group) {
const shape = group.addShape("rect", {
attrs: {
x: 0,
y: 0,
width: 200,
height: 100,
fill: "#f00",
}
});
group.addGroup();
return shape;
},
update: undefined
}
);
export default () => {
const graphContainer = useRef(null);
let graph: IGraph = null;
// 图初始化
useEffect(() => {
if (!graph) {
graph = new G6.Graph({
container: graphContainer.current,
width: 500,
height: 500,
});
}
const data = {
nodes: [
{
id: "node1", // String该节点存在则必须节点的唯一标识
type: "card-node",
placeholder: "暂无内容请",
title: "节点1",
x: 100,
y: 100
},
{
id: "node2",
type: "card-node",
placeholder: "暂无内容请",
title: "节点2",
x: 3000,
y: -100
}
]
};
graph.data(data);
graph.render();
graph.on('canvas:click', (evt) => {
const node = graph.findById("node2");
graph.update(node, { placeholder: "new placeholder" });
});
}, []);
return (
<div>
<div ref={graphContainer} className={'graph-container'} />
</div>
);
};

View File

@ -9,6 +9,7 @@ import ForceLayout from './forceLayout';
import GGEditorNode from './ggeditorNode';
import MultiLayout from './forceLayout/multiLayout'
import EdgeStatus from './edgeStatus'
import AddGroup from './addGroup'
export default { title: 'Issues' };
@ -22,3 +23,4 @@ storiesOf('Issues', module)
.add('ggeditor node issue', () => <GGEditorNode />)
.add('multi layout', () => <MultiLayout />)
.add('edge status', () => <EdgeStatus />)
.add('add group', () => <AddGroup />)

View File

@ -0,0 +1,70 @@
import React, { useEffect } from 'react';
import G6 from '../../../src';
import { IGraph } from '../../../src/interface/graph';
let graph: IGraph = null;
let colorMap = {
2012: 'blue',
2013: 'yellow',
2014: 'red',
2015: 'gray',
2016: 'black',
};
const ForceClusteringLayout = () => {
const container = React.useRef();
useEffect(() => {
if (!graph) {
const graph = new G6.Graph({
container: container.current as string | HTMLElement,
width: 1000,
height: 1000,
layout: {
type: 'force',
clustering: true,
clusterNodeStrength: -10,
clusterEdgeDistance: 200,
clusterNodeSize: 20,
// nodeStrength: -100,
clusterFociStrength: 1.2,
nodeSpacing: 5,
preventOverlap: true,
},
defaultNode: {
size: 15,
color: '#5B8FF9',
style: {
lineWidth: 2,
fill: '#C6E5FF',
},
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
},
modes: {
default: ['drag-canvas'],
},
});
fetch(
'https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json',
)
.then((res) => res.json())
.then((data) => {
graph.data(data);
data.nodes.forEach((i) => {
i.cluster = i.year;
i.style = Object.assign(i.style || {}, {
fill: colorMap[i.year],
});
});
graph.render();
});
}
});
return <div ref={container}></div>;
};
export default ForceClusteringLayout;

View File

@ -10,6 +10,7 @@ import ComboForceLayout from './component/combo-force-layout';
import ForceLayout from './component/force-layout';
import CompactBox from './component/compact-box';
import AutoLayout from './component/auto-layout';
import ForceClusteringLayout from './component/force-clustering-layout';
export default { title: 'Layout' };
@ -24,3 +25,4 @@ storiesOf('Layout', module)
.add('force layout', () => <ForceLayout />)
.add('compactbox layout', () => <CompactBox />)
.add('auto layout', () => <AutoLayout />)
.add('force clustering layout', () => <ForceClusteringLayout />)