Antv X6 动态连线
antv x6的使用
这是我实际开发项目中,利用 X6 开发的一个关系图。具备连线功能。这里我尽可能全的记录整个开发思路和部分编码,如果你也用了 X6 希望对你有帮助。
补充下我当时使用的版本(现在官网已经到 2.x 了,建议用新版,实现思路都是一样的):
"@antv/x6": "^1.30.0",
"@antv/layout": "^0.1.31"
创建画布
代码有删减,以下展示的代码全都有删减
index.vue
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
export default {
name: 'index',
data () {
return {
graph: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
}
},
mounted () {
this.init()
}
}
</script>
GraphCells.js
class GraphCells {
constructor () {
this.graph = null
this.cells = new Map()
this.edges = []
}
clear () {
this.graph = null
this.cells.clear()
this.edges = []
}
graphOptions (options) {
return {
container: document.getElementById('dag-view'),
autoResize: true,
// 是否可以拖动
panning: false,
grid: {
size: 10,
type: 'dot', // 'dot' | 'fixedDot' | 'mesh'
visible: true,
args: {
color: '#a0a0a0', // 网格线/点颜色
thickness: 1 // 网格线宽度/网格点大小
}
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
attrs: {
stroke: '#47C769'
}
}
},
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#fff',
stroke: '#31d0c6'
}
}
}
},
mousewheel: {
enabled: true,
modifiers: 'ctrl',
factor: 1.1,
maxScale: 1.5,
minScale: 0.5
},
scroller: {
enabled: true,
pageVisible: false,
pageBreak: false,
pannable: true
},
connecting: {
// 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
snap: true,
// 是否允许连接到画布空白位置的点
allowBlank: false,
// 是否允许创建循环连线
allowLoop: false,
// 拖动边时,是否高亮显示所有可用的连接桩或节点
highlight: true
},
...options
}
}
...
}
export {
GraphCells
}
创建节点
DagNode.js
import { Node, ObjectExt, Dom } from '@antv/x6'
class DagNode extends Node {
constructor (options) {
super(options)
this.options = options
}
}
DagNode.config({
zIndex: 2,
width: 100,
height: 28,
markup: [
{
tagName: 'rect',
selector: 'body'
},
{
// 使用 foreignObject 渲染 HTML 片段
tagName: 'foreignObject',
attrs: {
},
children: [
{
// 当 tagName 指定的标签是 HTML 元素时,需要使用 HTML 元素的命名空间
ns: Dom.ns.xhtml,
tagName: 'body',
attrs: {
xmlns: Dom.ns.xhtml
},
style: {
display: 'table-cell',
// 设置上左和下左边框radius。在svg元素中很难做到一侧radius,所以这里选择html元素
borderTopLeftRadius: '4px',
borderBottomLeftRadius: '4px',
// 背景颜色(下面介绍怎么动态设置)
backgroundColor: '',
textAlign: 'center',
verticalAlign: 'middle',
height: 28,
padding: '0 5px'
},
children: [
{
tagName: 'i',
attrs: {
// 设置图标字体class
class: 'iconfont iconzirenwu'
},
style: {
fontSize: '16px',
// 图标颜色(下面介绍怎么动态设置)
color: ''
}
}
]
}
]
},
{
tagName: 'text',
selector: 'label'
}
],
attrs: {
body: {
refWidth: '100%',
refHeight: '100%',
strokeWidth: 0,
fill: '#ffffff',
// stroke: '#5F95FF',
rx: 4,
ry: 4,
filter: {
name: 'highlight',
args: {
color: '#BFBFBF',
width: 2,
blur: 2,
opacity: 0.5
}
}
},
label: {
textWrap: {
// text: 'lorem ipsum dolor lorem ipsum dolor lorem ipsum dolor',
ellipsis: true,
breakWord: true,
width: -35
},
textAnchor: 'middle',
textVerticalAnchor: 'middle',
refX: 60,
refY: '50%',
refWidth: '80%',
fontSize: 12,
fill: '#333'
}
},
// 定义连接桩
ports: {
groups: {
right: {
position: { name: 'right' },
zIndex: 2,
attrs: {
portBody: {
magnet: true,
r: 3
}
}
},
in: {
position: { name: 'left' },
zIndex: 2,
attrs: {
portBody: {
magnet: false,
r: 0
}
}
},
out: {
position: { name: 'right' },
zIndex: 2,
attrs: {
portBody: {
magnet: false,
r: 0
}
}
}
}
},
portMarkup: {
tagName: 'circle',
selector: 'portBody',
attrs: {
fill: '#fff',
stroke: '#F08BB4',
strokeWidth: 1
}
},
propHooks (metadata) {
const { label, ...others } = metadata
if (label) {
ObjectExt.setByPath(others, 'attrs/label/textWrap/text', label)
}
return others
}
})
Node.registry.register('dag-node', DagNode, true)
export default DagNode
在 index.vue 中引,并添加两个节点到画布中
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
export default {
name: 'index',
data () {
return {
graph: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
// 添加节点
this.graph.addNode({
shape: 'dag-node',
x: 100,
y: 100,
label: 'foofoo' // 显示文本。这样写归功于自定义节点中的propHooks
})
this.graph.addNode({
shape: 'dag-node',
x: 300,
y: 100,
label: 'barbar'
})
}
},
mounted () {
this.init()
}
}
</script>
显示效果:
创建连线
DagEdge.js
import { Shape, Edge } from '@antv/x6'
/* 连线1 */
class CommonEdge extends Shape.Edge {
// ...
}
CommonEdge.config({
zIndex: 1,
router: {
name: 'er',
args: {
offset: 24,
direction: 'H'
}
},
connector: 'rounded',
connectionPoint: 'boundary',
attrs: {
line: {
stroke: '#BFBFBF',
strokeWidth: 1,
targetMarker: null
}
}
})
Edge.registry.register('common-edge', CommonEdge, true)
export {
CommonEdge
}
在 index.vue 中引入 DagEdge,并添加连线
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线
import './DagEdge'
export default {
name: 'index',
data () {
return {
graph: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
// 添加节点
this.graph.addNode({
id: 'node1',
shape: 'dag-node',
x: 100,
y: 100,
label: 'foofoo',
// 创建连接桩
ports: {
items: [
{
id: 'node1_out',
group: 'out'
}
]
}
})
this.graph.addNode({
id: 'node2',
shape: 'dag-node',
x: 300,
y: 100,
label: 'barbar',
ports: {
items: [
{
id: 'node2_in',
group: 'in'
}
]
}
})
this.graph.addEdge({
shape: 'common-edge',
source: { cell: 'node1', port: 'node1_out' },
target: { cell: 'node2', port: 'node2_in' }
})
}
},
mounted () {
this.init()
}
}
</script>
此时显示效果:
自定义连接
如果再添加一个节点 bazbaz,此时我想手动在 barbar 和 bazbaz 之间连接一条线,这条线和已有的线还不一样,该怎么做呢?
在 DagEdge 中添加另一条自定义连线:
import { Shape, Edge, Graph } from '@antv/x6'
/* 连线1 */
class CommonEdge extends Shape.Edge {
// ...
}
CommonEdge.config({
zIndex: 1,
router: {
name: 'er',
args: {
offset: 24,
direction: 'H'
}
},
connector: 'rounded',
connectionPoint: 'boundary',
attrs: {
line: {
stroke: '#BFBFBF',
strokeWidth: 1,
targetMarker: null
}
}
})
Edge.registry.register('common-edge', CommonEdge, true)
/* 连线2 */
class RelationEdge extends Shape.Edge {
// hover加粗线
hoverLine () {
this.attr('line', {
strokeWidth: 8
})
}
// 清除hover
clearHover () {
this.attr('line', {
strokeWidth: 1
})
}
// 清除箭头和虚线
clearMarker () {
this.attr('line', {
strokeDasharray: '',
targetMarker: ''
})
}
}
RelationEdge.config({
zIndex: 1,
connector: 'rounded',
connectionPoint: 'boundary',
router: {
name: 'oneSide',
args: { side: 'right' }
},
attrs: {
line: {
stroke: '#F08BB4',
strokeWidth: 1,
strokeDasharray: 5, // 控制虚线间隔
targetMarker: {
name: 'classic',
size: 5
}
}
}
})
Edge.registry.register('relation-edge', RelationEdge, true)
export {
CommonEdge,
RelationEdge
}
在 GraphCells.js 中添加
import { RelationEdge } from './DagEdge'
graphOptions (options) {
return {
...
connecting: {
...
// 连接的过程中创建新的边
createEdge () {
return new RelationEdge()
},
// 在移动边的时候判断连接是否有效,如果返回 false,当鼠标放开的时候,不会连接到当前元素,否则会连接到当前元素
validateConnection ({ sourceView, targetView, targetMagnet }) {
if (!targetMagnet) {
return false
}
if (targetMagnet.getAttribute('port-group') !== 'right') {
return false
}
return true
}
}
}
}
在 index.vue 中添加 bazbaz 节点
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线
import './DagEdge'
export default {
name: 'index',
data () {
return {
graph: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
// 添加节点
this.graph.addNode({
id: 'node1',
shape: 'dag-node',
x: 100,
y: 100,
label: 'foofoo',
ports: {
items: [
{
id: 'node1_out',
group: 'out'
}
]
}
})
this.graph.addNode({
id: 'node2',
shape: 'dag-node',
x: 300,
y: 100,
label: 'barbar',
ports: {
items: [
{
id: 'node2_in',
group: 'in'
},
{
id: 'node3_right',
group: 'right'
}
]
}
})
this.graph.addNode({
id: 'node3',
shape: 'dag-node',
x: 300,
y: 200,
label: 'bazbaz',
ports: {
items: [
{
id: 'node3_right',
group: 'right'
}
]
}
})
this.graph.addEdge({
shape: 'common-edge',
source: { cell: 'node1', port: 'node1_out' },
target: { cell: 'node2', port: 'node2_in' }
})
}
},
mounted () {
this.init()
}
}
</script>
显示效果:
添加事件
当鼠标移到连线上时,线加粗,并且显示删除按钮。
index.vue
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
// 注册自定义节点
import './DagNode'
// 引入自定义连线
import {RelationEdge} from './DagEdge'
export default {
name: 'index',
data () {
return {
graph: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
// 添加节点
this.graph.addNode({
id: 'node1',
shape: 'dag-node',
x: 100,
y: 100,
label: 'foofoo',
ports: {
items: [
{
id: 'node1_out',
group: 'out'
}
]
}
})
this.graph.addNode({
id: 'node2',
shape: 'dag-node',
x: 300,
y: 100,
label: 'barbar',
ports: {
items: [
{
id: 'node2_in',
group: 'in'
},
{
id: 'node3_right',
group: 'right'
}
]
}
})
this.graph.addNode({
id: 'node3',
shape: 'dag-node',
x: 300,
y: 200,
label: 'bazbaz',
ports: {
items: [
{
id: 'node3_right',
group: 'right'
}
]
}
})
this.graph.addEdge({
shape: 'common-edge',
source: { cell: 'node1', port: 'node1_out' },
target: { cell: 'node2', port: 'node2_in' }
})
/* 添加事件 */
this.graph.on('edge:mouseenter', ({ edge }) => {
if (edge instanceof RelationEdge) {
// 在 RelationEdge 中已定义
edge.hoverLine()
// X6 提供的小工具 https://x6.antv.vision/zh/docs/api/registry/edge-tool
edge.addTools([
{
name: 'button-remove',
args: {
distance: '50%',
offset: 0,
// 删除回调
// onClick ({ cell }) {
// }
}
}
])
}
})
this.graph.on('edge:mouseleave', ({ edge }) => {
if (edge instanceof RelationEdge) {
edge.clearHover()
edge.removeTools()
}
})
this.graph.on('edge:connected', ({ isNew, edge }) => {
if (isNew) {
edge.clearMarker()
}
})
}
},
mounted () {
this.init()
}
}
</script>
此时效果:
使用布局
使用布局使节点按照一定形式排列
index.vue
<template>
<div id="dag-view"></div>
</template>
<script>
import { Graph } from '@antv/x6'
import { GraphCells } from './GraphCells'
import { DagreLayout } from '@antv/layout'
// 注册自定义节点
import './DagNode'
// 引入自定义连线
import {RelationEdge} from './DagEdge'
export default {
name: 'index',
data () {
return {
graph: null,
dagreLayout: null,
graphCells: new GraphCells()
}
},
methods: {
init () {
this.graphCells.clear()
this.graph = new Graph(this.graphCells.graphOptions({}))
this.graphCells.graph = this.graph
/* 添加事件 */
this.graph.on('edge:mouseenter', ({ edge }) => {
if (edge instanceof RelationEdge) {
// 在 RelationEdge 中已定义
edge.hoverLine()
// X6 提供的小工具 https://x6.antv.vision/zh/docs/api/registry/edge-tool
edge.addTools([
{
name: 'button-remove',
args: {
distance: '50%',
offset: 0,
// 删除回调
// onClick ({ cell }) {
// }
}
}
])
}
})
this.graph.on('edge:mouseleave', ({ edge }) => {
if (edge instanceof RelationEdge) {
edge.clearHover()
edge.removeTools()
}
})
this.graph.on('edge:connected', ({ isNew, edge }) => {
if (isNew) {
edge.clearMarker()
}
})
this.dagreLayout = new DagreLayout({
type: 'dagre',
rankdir: 'LR',
align: undefined,
ranksep: 50,
nodesep: 5,
controlPoints: true,
begin: [50, 80]
})
},
// 布局渲染
graphLayout () {
const newModel = this.dagreLayout.layout(this.graphCells.getModel())
this.graph.fromJSON(newModel)
this.graph.centerContent()
},
getData () {
// 接口获取节点和连线信息。添加到 graphCells 中
// this.graphCells.setCell(cell)
// this.graphCells.setEdge(edge)
this.$nextTick(() => {
this.graph && this.graphLayout()
})
}
},
created () {
this.getData()
},
mounted () {
this.init()
}
}
</script>
GraphCell.js
import { Dom, Shape } from '@antv/x6'
import { RelationEdge } from './DagEdge'
class GraphCells {
constructor () {
this.graph = null
this.cells = new Map()
this.edges = []
}
clear () {
this.graph = null
this.cells.clear()
this.edges = []
}
graphOptions (options) {
return {
container: document.getElementById('dag-view'),
autoResize: true,
// 是否可以拖动
panning: false,
grid: {
size: 10,
type: 'dot', // 'dot' | 'fixedDot' | 'mesh'
visible: true,
args: {
color: '#a0a0a0', // 网格线/点颜色
thickness: 1 // 网格线宽度/网格点大小
}
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
attrs: {
stroke: '#47C769'
}
}
},
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#fff',
stroke: '#31d0c6'
}
}
}
},
mousewheel: {
enabled: true,
modifiers: 'ctrl',
factor: 1.1,
maxScale: 1.5,
minScale: 0.5
},
scroller: {
enabled: true,
pageVisible: false,
pageBreak: false,
pannable: true
},
connecting: {
// 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
snap: true,
// 是否允许连接到画布空白位置的点
allowBlank: false,
// 是否允许创建循环连线
allowLoop: false,
// 拖动边时,是否高亮显示所有可用的连接桩或节点
highlight: true,
createEdge () {
return new RelationEdge()
},
validateConnection ({ sourceView, targetView, targetMagnet }) {
if (!targetMagnet) {
return false
}
if (targetMagnet.getAttribute('port-group') !== 'right') {
return false
}
return true
}
},
...options
}
}
// 根据节点类型生成连接桩
getPortItems (id) {
const items = []
const inPort = {
id: `port_${id}_in`,
group: 'in'
}
const outPort = {
id: `port_${id}_out`,
group: 'out'
}
const rightPort = {
id: `port_${id}_right`,
group: 'right'
}
return items
}
/**
* @function 添加节点
* */
setCell (options) {
const id = options.id
const cell = {
shape: 'dag-node',
// 注意添加节点数据时传入的makup会覆盖DagNode.config中的markup。
// 所以DagNode中的markup可以删除,在这动态定义,就可以根据节点类型不同改变色值和图标
markup: [
{
tagName: 'rect',
selector: 'body'
},
{
tagName: 'foreignObject',
attrs: {
},
children: [
{
ns: Dom.ns.xhtml,
tagName: 'body',
attrs: {
xmlns: Dom.ns.xhtml
},
style: {
display: 'table-cell',
borderTopLeftRadius: '4px',
borderBottomLeftRadius: '4px',
// 动态传入颜色
backgroundColor: '',
textAlign: 'center',
verticalAlign: 'middle',
height: 28,
padding: '0 5px'
},
children: [
{
tagName: 'i',
attrs: {
// 动态传入图标class
class: ''
},
style: {
fontSize: '16px',
// 动态传入颜色
color: ''
}
}
]
}
]
},
{
tagName: 'text',
selector: 'label'
}
],
ports: {
items: this.getPortItems(id)
},
...options
}
this.cells.set(id, cell)
}
/**
* @function 添加连线
* @param edge {object} 连线配置
* */
setEdge (edge) {
const { source, target } = edge
if (this.cells.has(source.cell) && this.cells.has(target.cell)) {
this.edges.push({
// 默认普通连线
shape: 'common-edge',
...edge
})
}
}
/* graph model */
getModel () {
return {
nodes: [...this.cells.values()],
edges: this.edges
}
}
}
export {
GraphCells
}
Tooltip
当节点 label 文本过长会显示 … ,鼠标移入显示 tooltip 显示全名。在 X6 中没有找到合适的小工具,我自己写了一个 Tooltip 组件。这里只是配合我的代码结构使用,并非封装完美的 Tooltip 组件。仅供大家参考。
Tooltip.vue
<template>
<div id="lz_tooltip_container" class="lz_tooltip_container">
<div class="tooltip">
<span class="arrow"></span>
<div class="cont"></div>
</div>
</div>
</template>
<script>
export default {
name: 'Tooltip'
}
</script>
<style lang="scss">
.lz_tooltip_container {
position: fixed;
left: -1000px;
top: -1000px;
z-index: 999;
}
.lz_tooltip_container > .tooltip {
position: absolute;
background: black;
color: white;
top: -40px;
font-size: 14px;
padding: 8px 16px;
border-radius: 5px;
}
.lz_tooltip_container > .tooltip > .cont {
white-space: nowrap
}
.lz_tooltip_container > .tooltip > .arrow {
position: absolute;
width: 0;
height: 0;
bottom: -6px;
left: 24px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #000000;
filter: drop-shadow(0 2px 12px rgba(0,0,0, .03));
}
.lz_tooltip_container > .tooltip_hidden {
display: none;
}
</style>
我使用的是相对于屏幕的固定定位 fixed。因为在画布会上下滚动,左右平移,所以要根据节点相对画布的位置,转换成相对屏幕的位置然后动态设置 Tooltip 的 left top 属性。
在 index.vue 中 添加事件
<template>
<div id="dag-view"></div>
</template>
<script>
import Tooltip from './Tooltip'
export default {
name: 'index',
components: {
Tooltip
},
methods: {
init () {
...
this.graph.on('node:mouseenter', ({ e, node, view }) => {
// 节点相对画布位置
const pos = node.position({ relative: true })
// 转为相对屏幕位置
const { x, y } = this.graph.localToClient(pos.x, pos.y)
const tooltip = document.getElementById('lz_tooltip_container')
if (!tooltip) return
const cont = tooltip.querySelector('.cont')
if (!cont) return
cont.innerText = node.options.label
tooltip.style.left = `${x + 20}px`
tooltip.style.top = `${y - 5}px`
})
this.graph.on('node:mouseleave', ({ node }) => {
const tooltip = document.getElementById('lz_tooltip_container')
if (!tooltip) return
tooltip.style.left = '-1000px'
tooltip.style.top = '-1000px'
})
}
}
}
</script>
效果展示:
更多推荐
所有评论(0)