目录
1. 如何使用
节点的位置可以交给canvas.layout方法去计算,只需要定义layout的类型和节点之间的横向间距和行间纵向间距
1.1 如何配置canvas config
canvasConfig: {
disLinkable: false, // 可删除连线
linkable: false, // 可连线
draggable: true, // 可拖动
zoomable: true, // 可放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: 'AdvancedBezier',
isExpandWidth: false,
},
},
layout: {
type: 'dagreLayout',
options: {
rankdir: 'TB',
nodesep: 70,
ranksep: 40,
controlPoints: false,
},
},
}
1.2 如何初始化butterfly-vue组件
请参考index.vue文件中template写法
<template>
<div class="canvas-box">
<ButterflyVue ref="butterflyVue" :canvasData="renderData" :canvasConf="canvasConfig" @onLoaded="handleLoaded" />
</div>
</template>
1.3 butterfly-vue组件的数据结构
请参考mockData的写法
import Node from '../Node.vue'
const renderData = {
"nodes": [
{
"id": "200606",
"label": "节点 1",
"iconType": "icon-code",
"status": "success",
"render": Node
},
{
"id": "200599",
"label": "节点 2",
"iconType": "icon-code",
"status": "failed",
"render": Node
},
{
"id": "200609",
"label": "节点 3",
"iconType": "icon-code",
"status": "init",
"render": Node
}
],
"edges": [
{
"id": "0",
"source": "200609",
"target": "200606",
"arrow": true,
"type": "node"
},
{
"id": "1",
"source": "200609",
"target": "200599",
"arrow": true,
"type": "node"
},
{
"id": "2",
"source": "200599",
"target": "200606",
"arrow": true,
"type": "node"
}
]
}
export default {
renderData
}
1.4 如何编写自定义节点vue组件
参考以下node.vue文件
<template>
<div
:class="['dagre-node', hovered && 'hover']"
:id="itemData.id"
@mouseenter="onFocus"
@mouseleave="onBlur"
>
<div :class="`icon-box ${itemData.className}`">
<a-icon v-if="itemData.status === 'running'" class="iconfont" type="loading" />
<i v-else :class="`iconfont ${itemData.iconType}`"></i>
</div>
<span class="text-box" :title="itemData.label">{{ itemData.label }}</span>
</div>
</template>
<script>
export default {
props: {
itemData: {
type: Object,
default: () => {},
},
},
data() {
return {
hovered: false,
}
},
mounted() {},
methods: {
onFocus() {
this.hovered = true
},
onBlur() {
this.hovered = false
},
},
}
</script>
<style lang="less" scoped>
.dagre-node {
width: fit-content;
.icon-box {
width: 46px;
height: 46px;
display: flex;
justify-content: center;
align-items: center;
line-height: 46px;
box-sizing: content-box;
margin: 0 auto;
text-align: center;
background: #438efd;
border-radius: 50%;
.iconfont {
font-size: 28px;
color: #fff;
}
}
.text-box {
height: 30px;
width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
padding: 0 10px;
line-height: 30px;
text-align: center;
font-size: 14px;
}
}
</style>
2. 自定义节点的位置
业务背景:
业务需要我们在用户拖拽移动节点位置之后,在浏览器缓存中保存节点的位置,对于新添加的节点自动计算得出
存在问题:
在canvasData给出时直接定义的节点的left, top属性不生效,在dag渲染之后,再去移动节点到left,top的位置也没有生效
解决方法:
第一步:
在节点位置移动时,监听canvas事件,将当前画布中所有节点的位置和id写入浏览器本地缓存,代码如下:
canvas.on('system.drag.end', (data) => {
if(data.dragType === 'node:drag') {
const graphData = _.cloneDeep(this.canvas.getDataMap().nodes.map((node) => {
return {
id: node.id,
left: node.left,
top: node.top
}
}))
this.setLocalDagData(graphData)
}
})
第二步:
需要将移动节点位置的代码写在$nextTick里,能够保证dag图已经加载并渲染,且用自动计算得出的节点位置进行布局之后,再去移动到缓存里保存的left,top位置
请参考如下代码
watch: {
data(newValue) {
// 通过watch组件的props属性data的变化,实现重绘
// 第一步,重新计算dag图的nodes和edges属性
const _nodes = newValue.nodes.map((item) => {
return {
id: item.id.toString(),
label: item.name,
iconType: 'icon-code', // 或者 'icon-yinzi'
factor_id: item.factor_id,
schedule_id: item.schedule_id,
status: item.status,
render: item.render,
}
})
const _edges = newValue.edges.map((item, index) => {
return {
id: index.toString(),
source: item.source.toString(),
target: item.dest.toString(),
arrow: true,
type: 'node',
}
})
// 深拷贝nodes和edges,重新复制给renderData也就是butterfly-vue的数据canvasData属性
// 只有这样,vue组件才能监听到canvasData属性的数据变化,引发重绘
this.renderData = {
nodes: _.cloneDeep(_nodes),
edges: _.cloneDeep(_edges),
}
// 引用两次vue组件的redraw()方法,保证及时重绘更新
this.$refs.butterflyVue.redraw()
this.$nextTick(() => {
this.$refs.butterflyVue.redraw()
this.$nextTick(() => {
// 在第二重绘redraw()完成之后,再移动节点到缓存里的位置
this.canvas.nodes.forEach((node, index) => {
const graphData = this.getLocalDagData()
const target = graphData ? graphData.find((item) => { return item.id === node.id }) : null
if(target) {
this.canvas.nodes[index].moveTo(target.left, target.top)
}
})
})
})
},
},
methods: {
getLocalDagData() {
const dashboard_id = this.$route.params.id
if(dashboard_id && localStorage.getItem(`dagData-${dashboard_id}`)) {
return JSON.parse(localStorage.getItem(`dagData-${dashboard_id}`))
}
return null
},
}
3. dag重绘时,如何恢复画布缩放
业务背景:
dag图展示的是任务运行状态的调度记录,因为每隔几秒的时间就会有可能发生数据更新,重绘图表时,需要恢复用户对画布的缩放,优化使用体验。
实现思路:
这里需要用到butterfly-dag的两个方法,参考官方文档说明
canvas.getZoom ()
作用:获取画布的缩放
返回
{float}
- 画布的缩放(0-1)
getZoom = () => {}
canvas.zoom (scale)
作用:手动设置画布缩放
参数
{float} scale
- 0-1之间的缩放值{function} callback
- 缩放后的回调
zoom = (scale) => {}
在重绘之前保存画布的缩放值到全局状态this中,在重绘完成之后,读取全局状态this的画布缩放值,恢复画布的缩放状态,可以参考以下代码
this.zoom = this.canvas.getZoom()
this.$refs.butterflyVue.redraw()
this.$nextTick(() => {
this.canvas.zoom(this.zoom)
})
4. dag重绘时,如何恢复画布的偏移量
业务背景:
dag图展示的是任务运行状态的调度记录,因为每隔几秒的时间就会有可能发生数据更新,重绘图表时,需要恢复用户对画布的拖拽产生的偏移量,优化使用体验。这个问题还可以有个延申,就是希望能在浏览器缓存里保存画布的偏移量,缩放和节点移动后的位置,那我们需要用到浏览器本地缓存的知识点,localStorage。
实现思路:
这个问题的解决思路和第三点很类似,需要用到butterfly-dag的两个方法,参考官方文档说明
canvas.move (postion)
作用:手动设置画布偏移
参数
{[x, y]} array
- x,y坐标
move = (postion) => {}
canvas.getOffset ()
作用:获取画布整体移动的偏移值
返回
{[x, y]}
- 画布的偏移值
getOffset = () => {}
5. 如何控制节点Node显隐
业务背景:
当图里的节点足够多时,展示全部会显得冗余,图的结构会变的乱糟糟,或者节点会变得很小难以分辨。因此希望能够根据节点的状态,控制显示/隐藏部分节点
解决方法:
需在获取节点nodes和边edges信息之后,立即过滤筛选满足条件的节点,需注意edges也需要筛选与满足条件节点相连的边,否则butterfly-vue会报“目标节点或源节点不存在”的错误
如需要控制成功状态的节点,则定义一个全局变量this属性hideSuccess,用户可以控制其值在true/false之间变化,获取到dag图的数据源nodes和edges后,根据hideSuccess和node.status筛选nodes,根据edge的目标节点和源节点是否同时存在在筛选后的nodes。再将其整理成data的形式交给butterfly-vue渲染
示例代码:
getSchedule(this.schedule_id).then((res) => {
this.task_history = res
const _nodes = this.task_history.nodes.filter((node) => {
return !this.hideSuccess || node.status !== 'success'
})
const _edges = this.task_history.edges.filter((edge) => {
const sourceNodeExist = !!_nodes.find((node) => { return node.id === edge.source })
const destNodeExist = !!_nodes.find((node) => { return node.id === edge.dest })
return sourceNodeExist && destNodeExist
})
this.dagData = {
nodes: _nodes.map((node) => {
node.render = Node
node.factor_id = node.id
node.schedule_id = this.task_history.id
return node
}),
edges: _edges
}
})
6. 如何实现重新排版
重新排版意味着去除用户对画布缩放和偏移量的变动,以及对节点位置的移动,凭着直觉写了如下代码:
initLayout() {
this.canvas.move([0,0])
this.canvas.zoom(1)
// 重新计算节点位置,布局layout
this.redraw(this.data)
}
问题:
以上代码没有实现将画布缩放到初始的状态的效果。
原因:
经过阅读butterfly-dag/src/canvas/baseCanvas.js关于canvas.zoom (scale)源码之后发现,该方法还接受第二个参数是一个回调函数,意味着该方法是异步执行的,如果在调用了zoom方法之后立即去调用getZoom()方法的话,会发现zoom值还没有恢复到给定的初始值1。
解决办法:
将重新计算节点位置布局的代码写在canvas.zoom()和canvas.move()之后,且包进$nextTick()的回调函数里。
示例代码:
initLayout() {
this.canvas.move([0,0])
this.canvas.zoom(1)
this.$nextTick(() => {
// 待画布缩放和偏移量设置完,重新计算节点位置,布局layout
this.redraw(this.data)
})
}