Vue2 uses butterfly to draw directed acyclic graph experience summary

Table of contents

1. How to use

1.1 How to configure canvas config 

1.2 How to initialize the butterfly-vue component

1.3 The data structure of the butterfly-vue component

1.4 How to write custom node vue components

2. Customize the position of the node

3. When the dag is redrawn, how to restore the canvas zoom

4. When the dag is redrawn, how to restore the offset of the canvas

5. How to control the visibility of the node Node

6. How to implement rearrangement


1. How to use

The position of the node can be calculated by the canvas.layout method. You only need to define the type of layout and the horizontal spacing between nodes and the vertical spacing between rows

1.1 How to configure 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 How to initialize the butterfly-vue component

Please refer to the template writing method in the index.vue file

<template>
  <div class="canvas-box">
    <ButterflyVue ref="butterflyVue" :canvasData="renderData" :canvasConf="canvasConfig" @onLoaded="handleLoaded" />
  </div>
</template>

1.3 The data structure of the butterfly-vue component

Please refer to the writing method of 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 How to write custom node vue components

Refer to the following node.vue file 

<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. Customize the position of the node

Business background:

The business requires us to save the position of the node in the browser cache after the user drags and drops the position of the mobile node, and automatically calculates the newly added node

There is a problem:

When the canvasData is given, the left and top attributes of the nodes directly defined are not valid. After the dag is rendered, moving the node to the left and top positions does not take effect.

Solution:

first step:

When the position of the node is moved, listen to the canvas event, and write the position and id of all nodes in the current canvas to the local cache of the browser. The code is as follows:

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

Step two:

The code for moving the node position needs to be written in $nextTick to ensure that the dag graph has been loaded and rendered, and after layout with the automatically calculated node position, move to the left and top positions saved in the cache

Please refer to the following code

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. When the dag is redrawn, how to restore the canvas zoom

Business background:

The dag diagram shows the scheduling record of the task running status, because data updates may occur every few seconds. When redrawing the chart, it is necessary to restore the user's zoom on the canvas to optimize the user experience.

Implementation idea:

Here you need to use two methods of butterfly-dag, refer to the official documentation

canvas.getZoom ()

Function : Get the zoom of the canvas

return

  • {float} - the scaling of the canvas (0-1)
getZoom = () => {}

canvas.zoom (scale)

Role : manually set the canvas zoom

parameter

  • {float} scale - scaling value between 0-1
  • {function} callback - Callback after scaling
zoom = (scale) => {}

Save the zoom value of the canvas to the global state this before redrawing. After the redrawing is completed, read the canvas zoom value of the global state this and restore the zoom state of the canvas. You can refer to the following code

this.zoom = this.canvas.getZoom()
this.$refs.butterflyVue.redraw()
this.$nextTick(() => {
    this.canvas.zoom(this.zoom)
})

4. When the dag is redrawn, how to restore the offset of the canvas

 Business background:

The dag diagram shows the scheduling record of the task running status, because data updates may occur every few seconds. When redrawing the chart, it is necessary to restore the offset generated by the user's drag and drop on the canvas to optimize the user experience. This problem can also be extended, that is, we hope to save the offset of the canvas, zoom and the position of the node after moving in the browser cache, then we need to use the knowledge point of the browser's local cache, localStorage.

Implementation idea:

The solution to this problem is very similar to the third point. You need to use two methods of butterfly-dag. Refer to the official documentation .

canvas.move (postion)

Function : manually set the canvas offset

parameter

  • {[x, y]} array - x,y coordinates
move = (postion) => {}

canvas.getOffset ()

Function : Get the offset value of the overall movement of the canvas

return

  • {[x, y]} - the offset value of the canvas
getOffset = () => {}

5. How to control the visibility of the node Node

Business background:

When there are enough nodes in the graph, displaying them all will be redundant, the structure of the graph will become messy, or the nodes will become too small to distinguish. Therefore, it is hoped to be able to control the display/hide of some nodes according to the state of the node

Solution:

After obtaining the node nodes and edge information, immediately filter the nodes that meet the conditions. It should be noted that the edges also need to filter the edges connected to the nodes that meet the conditions, otherwise butterfly-vue will report an error of "the target node or source node does not exist " .

If you need to control the success status of the node, define a global variable this attribute hideSuccess, the user can control its value to change between true/false, after obtaining the data source nodes and edges of the dag graph, filter the nodes according to hideSuccess and node.status, and check whether the target node and source node of the edge exist in the filtered nodes. Then organize it into data and hand it over to butterfly-vue for rendering

Sample code:

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. How to implement rearrangement

Re-layout means removing the user's changes to the zoom and offset of the canvas, as well as the movement of the node position. I wrote the following code intuitively:

initLayout() {
    this.canvas.move([0,0])
    this.canvas.zoom(1)
    // 重新计算节点位置,布局layout
    this.redraw(this.data)
}

question:

The above code does not achieve the effect of scaling the canvas to its initial state.

reason:

After reading the source code of canvas.zoom (scale) in butterfly-dag/src/canvas/baseCanvas.js, I found that this method also accepts a callback function as the second parameter, which means that the method is executed asynchronously. If you call the getZoom() method immediately after calling the zoom method, you will find that the zoom value has not yet returned to the given initial value of 1.

Solution:

Write the code for recalculating the node position layout after canvas.zoom() and canvas.move(), and wrap it into the callback function of $nextTick().

Sample code:

initLayout() {
    this.canvas.move([0,0])
    this.canvas.zoom(1)
    this.$nextTick(() => {
        // 待画布缩放和偏移量设置完,重新计算节点位置,布局layout
        this.redraw(this.data)
    })
}

 

Guess you like

Origin blog.csdn.net/valsedefleurs/article/details/130580071