x6实践
# 入门
# 引入
对于生产环境,我们推荐使用一个明确的版本号,以避免新版本造成的不可预期的破坏:
- https://unpkg.com/@antv/x6@1.1.1/dist/x6.js
- https://cdn.jsdelivr.net/npm/@antv/x6@1.1.1/dist/x6.js
- https://cdnjs.cloudflare.com/ajax/libs/antv-x6/1.1.1/x6.js
<script src="https://unpkg.com/@antv/x6@1.1.1/dist/x6.js"></script>
# helloword
- 创建容器
- 准备数据
- 渲染画布
import React from "react";
import { Graph } from "@antv/x6";
import "./app.css";
const data = {
nodes: [
{
id: "node1",
x: 40,
y: 40,
width: 80,
height: 40,
label: "Hello",
attrs: {
body: {
fill: "#2ECC71",
stroke: "#000",
strokeDasharray: "10,2"
},
label: {
text: "Hello",
fill: "#333",
fontSize: 13
}
}
},
{
id: "node2",
shape: "ellipse", // 使用 ellipse 渲染
x: 160,
y: 180,
width: 80,
height: 40,
label: "World",
attrs: {
body: {
fill: "#F39C12",
stroke: "#000",
rx: 16,
ry: 16
},
label: {
text: "World",
fill: "#333",
fontSize: 18,
fontWeight: "bold",
fontVariant: "small-caps"
}
}
}
],
edges: [
{
source: "node1",
target: "node2",
shape: 'double-edge',
attrs: {
line: {
stroke: 'orange',
},
},
}
]
};
export default class Example extends React.Component {
private container: HTMLDivElement;
componentDidMount() {
const graph = new Graph({
container: this.container,
background: {
color: "#fffbe6" // 设置画布背景颜色
},
grid: {
size: 10, // 网格大小 10px
visible: true // 渲染网格背景
}
});
graph.fromJSON(data);
// graph.zoom(-0.5) // 缩小 50%
graph.translate(80, 40);
}
refContainer = (container: HTMLDivElement) => {
this.container = container;
};
render() {
return (
<div className="app">
<div className="app-content" ref={this.refContainer} />
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 实践x6模型开发
# 初始化项目
mkdir x6-model-demo
npx @umijs/create-umi-app
2
# 安装相关依赖
npm i -S antd @antv/x6 @antv/x6-react-components @antv/x6-react-shape classnames
# package.json
调整
"name": "x6-model-demo",
"version": "0.0.1",
"dependencies": {
"@ant-design/pro-layout": "^6.5.0",
"@antv/g6": "^3.8.2",
"@antv/x6": "^1.12.10",
"@antv/x6-react-shape": "^1.2.3",
"antd": "^4.9.4",
"classnames": "^2.2.6",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"devDependencies": {
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@umijs/test": "^3.3.3",
"@umijs/preset-react": "1.x",
"lint-staged": "^10.0.7",
"prettier": "^2.2.0",
"umi": "^3.3.3",
"typescript": "^4.1.2",
"yorkie": "^2.0.0"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 本地验证
发布之前,可以通过 serve
做本地验证,
$ yarn global add serve
$ serve ./dist
2
# 迁移代码
迁移官方x6-app-er (opens new window), 结合本地demo调整;
# Getting Started
Install dependencies,
$ yarn
Start the dev server,
$ yarn start
# 功能点
# ts封装抽象
# 集成时样式
styles引入
# 数据同步问题
节点拖动数据跟外部的node,edge的数据保持一致;
private registerEvent = () => {
this.graph.on('node:dblclick', ({ e, x, y, node, view }) => {
this.currentCell = node;
this.handleMenuPro();
});
this.graph.on('node:added', ({ node, index, options }) => {
// 添加时,这里面的节点拿不到xy信息;现在通过选中contextmenu时,补充x,y数据;
this.baseGraph.cellController.nodes.push(node);
if (this.props.nodeCb) this.props.nodeCb('added', this.assemblData(node));
});
this.graph.on('node:removed', ({ node, index, options }) => {
// 删除时,这里面的节点可拿到xy信息;已经没有意义了;
this.baseGraph.cellController.removeNode(node);
if (this.props.nodeCb) this.props.nodeCb('removed', node);
});
this.graph.on('edge:added', ({ edge, index, options }) => {
// this.baseGraph.cellController.addEdge()
this.baseGraph.cellController.edges.push(edge);
if (this.props.edgeCb) this.props.edgeCb('added', this.assemblData(edge));
});
this.graph.on('edge:removed', ({ edge, index, options }) => {
this.baseGraph.cellController.removeEdge(edge);
if (this.props.edgeCb) this.props.edgeCb('removed', edge);
});
this.graph.on('cell:change:*', ({ e }) => {
const str = this.graph.toJSON({ diff: true });
console.log(JSON.stringify(str, null, 2));
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 自定义组件
import '@antv/x6-react-shape';
示范
https://codesandbox.io/s/j9qyg?file=/src/app.tsx
# 高度问题
组件内设置了高度,在里面只设置100%,就可以;
也可以在里面固定高度;
# 定义节点及边
# 滚动缩放平移
普通画布(未开启 scroller (opens new window) 模式)通过开启 panning
选项来支持拖拽平移。
const graph = new Graph({
panning: true,
})
// 等同于
const graph = new Graph({
panning: {
enabled: true,
},
})
2
3
4
5
6
7
8
9
10
拖拽可能和其他操作冲突,此时可以设置 modifiers
参数,设置修饰键后需要按下修饰键并点击鼠标才能触发画布拖拽。
const graph = new Graph({
panning: {
enabled: true,
modifiers: 'shift',
},
})
2
3
4
5
6
普通画布(未开启 scroller (opens new window) 模式)通过开启 mousewheel (opens new window) 选项来支持画布缩放。这里说明怎么通过代码来进行画布缩放:
graph.zoom() // 获取缩放级别
graph.zoom(0.2) // 在原来缩放级别上增加 0.2
graph.zoom(-0.2) // 在原来缩放级别上减少 0.2
2
3
常用的就是将画布内容中心与视口中心对齐,使用方式:
graph.centerContent()
更多的居中方法可以查看 Transform (opens new window)
# 右击菜单
# 实现方式一 (opens new window)[推荐]
通过事件冒泡处理;height设置高后,也可以滚动处理;
this.baseGraph.graph.on('cell:contextmenu', ({ e }) => {
const { x, y } = this.baseGraph.graph.clientToLocal(e.clientX, e.clientY);
this.contextMenuAnchor.style.display = 'inline-block';
this.contextMenuAnchor.style.left = `${x}px`;
this.contextMenuAnchor.style.top = `${y}px`;
const clickEvent = document.createEvent('HTMLEvents');
clickEvent.initEvent('click', true, true);
this.contextMenuAnchor.dispatchEvent(clickEvent);
});
this.baseGraph.graph.on('node:mouseenter', ({ e }) => {
const ports = this.graphContainer.querySelectorAll(
'.x6-port-body',
) as NodeListOf<SVGAElement>;
this.showPorts(ports, true);
});
this.baseGraph.graph.on('node:mouseleave', ({ e }) => {
const ports = this.graphContainer.querySelectorAll(
'.x6-port-body',
) as NodeListOf<SVGAElement>;
this.showPorts(ports, false);
});
this.baseGraph.graph.on('cell:change:*', ({ e }) => {
const str = JSON.stringify(
this.baseGraph.graph.toJSON({ diff: true }),
null,
2,
);
console.log(str);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
做相应的优化
居中处理及加入滚轮;只做mousewheel不做scroller处理;
this.baseGraph = new BaseGraph({
container: this.graphContainer,
grid: true,
// scroller: {
// enabled: true,
// pannable: true,
// },
mousewheel: {
enabled: true,
zoomAtMousePosition: false,
factor: 1.1,
},
...this.getContainerSize(),
});
this.graph.batchUpdate('updateGraph', () => {
if (addNodesData && addNodesData.length > 0) {
this.cellController.addNodes(addNodesData);
}
if (addEdgesData && addEdgesData.length > 0) {
this.cellController.addEdges(addEdgesData);
}
this.graph.centerContent();
});
//加入滚轮监听
const p1 = this.graph.pageToLocal(e.clientX, e.clientY);
const p2 = this.graph.localToGraph(p1);
const left = `${p2.x}px`;
const top = `${p2.y}px`;
this.contextMenuAnchor.style.left = left;
this.contextMenuAnchor.style.top = top;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
this.currentCell = cell;
// this.currentCell.x = x;//现在是居中处理,不用再重新复制
// this.currentCell.y = y;
//做了居中处理后,不用在存x,y啦
private assemblData(data) {
const newData = data.getData() || {};
newData.id = data.id || 0;
// newData.x = data.x || 100;
// newData.y = data.y || 100; //x,y 只有对节点有用;
console.log('---assemblData------newData----', newData);
return newData;
}
2
3
4
5
6
7
8
9
10
11
12
# 实现方式二 (opens new window)
通过registerEdgeTool实现;,可以i处理scroller: true的问题;
# 最后实现
通过上面的方式一,再修改相关设置;
设置可以滚动及鼠标放大缩小;
this.baseGraph = new BaseGraph({
container: this.graphContainer,
grid: true,
mousewheel: {
enabled: true,
zoomAtMousePosition: false,
factor: 1.1,
},
scroller: {
enabled: true,
pageVisible: false,
pageBreak: false,
pannable: true,
},
...this.getContainerSize(),
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
修改监听后的偏移;
this.graph.on('cell:contextmenu', ({ e, cell, view, x, y }) => {
this.currentCell = cell;
// const p1 = this.graph.pageToLocal(e.clientX, e.clientY);
// const p2 = this.graph.localToGraph(p1);
// const p1 = {x,y}; // 不滚动时是可以的;
const p2 = { x: e.clientX, y: e.clientY }; // 滚动时的处理
const left = `${p2.x - 320}px`; //去除左边的间距;
const top = `${p2.y - 64 - 150}px`;// 移除上面及下面的间距;
this.contextMenuAnchor.style.display = 'inline-block';
this.contextMenuAnchor.style.left = left;
this.contextMenuAnchor.style.top = top;
const clickEvent = document.createEvent('HTMLEvents');
clickEvent.initEvent('click', true, true);
this.contextMenuAnchor.dispatchEvent(clickEvent);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 最终效果
# 相关链接
https://antv-x6.gitee.io/zh/docs/tutorial/getting-started