vue2使用butterfly绘制有向无环图经验总结

目录

1. 如何使用

1.1 如何配置canvas config 

1.2 如何初始化butterfly-vue组件

1.3 butterfly-vue组件的数据结构

1.4 如何编写自定义节点vue组件

2. 自定义节点的位置

3. dag重绘时,如何恢复画布缩放

4. dag重绘时,如何恢复画布的偏移量

5. 如何控制节点Node显隐

6. 如何实现重新排版


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)
    })
}

 

猜你喜欢

转载自blog.csdn.net/valsedefleurs/article/details/130580071