前言

butterfly官方文档

butterfly官方示例

这篇文章吧是去年(2022年)写的,当时花了挺多时间做了个下面的这个demo,但是去年一直没有使用。今年刚好有一个项目需要用到,就从头开始搞吧,但是下面这个demo有些复杂,所以又相当于重新踩了一边坑,当然也更加加深了印象。
因此打算把这篇文章重新整理一下,会给出一个简单demo,说一下开发的流程以及注意事项。
在这里插入图片描述
删除功能也做了,是鼠标右键弹出菜单的格式,上面没有演示,右键菜单也支持扩展。

开始

demo效果图
在这里插入图片描述

安装

npm install butterfly-dag@4.2.1
npm install jquery@3.6.0

建议安装上面这两个版本

先定义点的样子

我是习惯先写结点
节点类:node.ts

import $ from 'jquery'
import { Node } from 'butterfly-dag'
import './node.scss'

//定义锚点结构
interface baseEndpoint {
    //锚点的唯一标识
    id: string
    //锚点的位置
    orientation: Array<number>
}

// 线条
interface EdgeI {
    //自身节点的id
    sourceNode: string
    //自身节点锚点的起始id
    source: string
    //目标节点的id
    targetNode: string
    //目标节点的锚点结束id
    target: string
    //类型
    type: string
}

// 节点结构
interface NodeI {
    // 节点id
    nodeId: string
    // 节点类型,默认矩形
    nodeType: 'rectangle-node'
    //节点显示的名称
    nodeLabel: string
    //节点对应的值
    nodeValue: string | number
    // 节点位置
    nodeLeft: number
    nodeTop: number
    //锚点
    endpoints: Array<baseEndpoint>
    //子级
    children: Array<NodeI>
    //前置节点
    beforeNode: Array<string>
    //后置节点
    afterNode: Array<string>
    Class: any
    //父级id
    parentId?: string
}

class BaseNode extends Node {
    //节点配置
    options: NodeI
    id: string
    top: number
    left: number
    constructor(opts: NodeI) {
        super(opts)
        this.options = opts
        this.id = opts.nodeId
        this.top = opts.nodeTop
        this.left = opts.nodeLeft
    }

    //创建节点
    draw = () => {
        //如果文本内容过长进行截取
        const desc =
            this.options.nodeLabel?.length > 5
                ? `${this.options.nodeLabel.substring(0, 5)}...`
                : this.options.nodeLabel
        const nodeDom = $(
            `
             <div class="${this.options.nodeType}">
                     ${desc}
             </div>
        `,
        )
            .css('top', this.options.nodeTop)
            .css('left', this.options.nodeLeft)
            .attr('id', this.options.nodeId)
            .attr('value', this.options.nodeValue)
            .addClass(this.options.nodeType)

        if (this.options.nodeLabel?.length > 5) {
            nodeDom.attr('title', this.options.nodeLabel)
        }
        console.log('节点是:', nodeDom[0])
        //返回当前节点的dom对象
        return nodeDom[0]
    }
}

export default BaseNode
export type { EdgeI, NodeI }

我这里是因为实际需要写了几个接口来规范数据结构,这个看个人的实际需求。

注意点:

1、 你可以自定义节点内包含哪些内容,比如上面的 NodeI,但是在你重新的类里,一定要在构造函数里对必填值进行赋值

 //节点配置
 options: NodeI
 id: string
 top: number
 left: number
 constructor(opts: NodeI) {
     super(opts)
     this.options = opts
     this.id = opts.nodeId
     this.top = opts.nodeTop
     this.left = opts.nodeLeft
 }

idtopleft 是必填值,id是唯一表示,topleft都是用于定位节点在什么位置的。具体可以看官方文档,关于节点的部分,只要你满足节点需要的参数就可以,我这里是为了避免属性冲突另外定义的。

2、draw 方法名不要改,这是用来生成节点的。该方法最后会返回要生成的dom元素,你可以在dom上挂载你想要的元素。节点长什么样子,完全看你想让他什么样

3、最好一定要把你这个类导出来,后面很有用

export default BaseNode

节点样式:node.scss

.rectangle-node{
    width: 120px;
    height: 40px;
    border: 1px solid #D7D7D7;
    border-radius: 3px;
    color: #000;
    text-align: center;
    line-height: 40px;
    position: absolute;
    cursor:pointer;
}

这块没什么好说的就是用来修饰你上面的dom元素的,但是要注意节点必须要设置成绝对定位

position: absolute;

在画布上生成节点

<template>
    <div class="flow-edit-chart" id="flow-edit-chart" />
</template>

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
//引入butterfly
import { Canvas } from 'butterfly-dag'
import 'butterfly-dag/dist/index.css'
// 引入节点类、连线类型
import { EdgeI, NodeI } from '../butterfly/node'
import BaseNode from '../butterfly/node'

//画布
const canvas = ref()

const nodeList:Array<NodeI> = [
    {
        nodeId: 'A',
        nodeType: 'rectangle-node',
        nodeLabel: 'A',
        nodeValue: 'A',
        nodeLeft: 100,
        nodeTop: 100,
        endpoints: [
            {
                id: 'right',
                orientation: [1, 0],
            },
            {
                id: 'left',
                orientation: [-1, 0],
            },
        ],
        children: [],
        beforeNode: [],
        afterNode: [],
        Class: BaseNode,
    },
    {
        nodeId: 'B',
        nodeType: 'rectangle-node',
        nodeLabel: 'B',
        nodeValue: 'B',
        nodeLeft: 300,
        nodeTop: 100,
        endpoints: [
            {
                id: 'right',
                orientation: [1, 0],
            },
            {
                id: 'left',
                orientation: [-1, 0],
            },
        ],
        children: [],
        beforeNode: [],
        afterNode: [],
        Class: BaseNode,
    },
    {
        nodeId: 'C',
        nodeType: 'rectangle-node',
        nodeLabel: 'C',
        nodeValue: 'C',
        nodeLeft: 500,
        nodeTop: 100,
        endpoints: [
            {
                id: 'right',
                orientation: [1, 0],
            },
            {
                id: 'left',
                orientation: [-1, 0],
            },
        ],
        children: [],
        beforeNode: [],
        afterNode: [],
        Class: BaseNode,
    },
]

const edgeList:Array<EdgeI> = [
    {
        sourceNode: 'A',
        targetNode: 'B',
        source: 'right',
        target: 'left',
        type: 'endpoint',
    },
    {
        sourceNode: 'B',
        targetNode: 'C',
        source: 'right',
        target: 'left',
        type: 'endpoint',
    },
]

onMounted(() => {
    // 获取绘制容器
    let dom = document.getElementById('flow-edit-chart')
    //生成画布
    canvas.value = new Canvas({
        root: dom, //canvas的根节点(必传)
        zoomable: true, //可缩放(可传)
        moveable: true, //可平移(可传)
        draggable: true, //节点可拖动(可传)
        linkable: true, //节点可连线
        theme: {
            //主题
            edge: {
                shapeType: 'Bezier',
                arrow: true,
            },
        },
    })
    if (canvas.value) {
        console.log('canvas:', canvas.value)
        //单独生成点和线
        // canvas.value.addNodes(nodeList)
        // canvas.value.addEdges(edgeList)
        //,一起生成点和线
        canvas.value.draw({ nodes: nodeList, edges: edgeList })
    }
})

// 新增节点
const addNode = () => {
    const reactangle: NodeI = {
        nodeId: `${new Date().getTime()}`,
        nodeType: 'rectangle-node',
        nodeLabel: `${new Date().getTime()}`,
        nodeValue: `${new Date().getTime()}`,
        nodeLeft: 500,
        nodeTop: 100,
        endpoints: [
            {
                id: 'right',
                orientation: [1, 0],
            },
            {
                id: 'left',
                orientation: [-1, 0],
            },
        ],
        children: [],
        beforeNode: [],
        afterNode: [],
        Class: BaseNode,
    }
    console.log('新增的节点:', reactangle)
    canvas.value.addNode(reactangle)
}
</script>

<style lang="scss" scoped>
.flow-edit-chart {
    width: 100%;
    height: 300px;
    margin-bottom: 10px;
}
</style>

注意点:
1、节点的数据格式,以我的为例。因为我是定义了节点的数据格式的,所以我的数据要与定义的格式一致;如果没有定义,那么要按照官方节点格式来,一般只需要id top left

{
     nodeId: 'A',
     nodeType: 'rectangle-node',
     nodeLabel: 'A',
     nodeValue: 'A',
     nodeLeft: 100,
     nodeTop: 100,
     endpoints: [
         {
             id: 'right',
             orientation: [1, 0],
         },
         {
             id: 'left',
             orientation: [-1, 0],
         },
     ],
     children: [],
     beforeNode: [],
     afterNode: [],
     Class: BaseNode,
 },

2、这点很重要,你的节点数据里必须指明类,比如: Class: BaseNode,这个BaseNode就是我们上面导出的节点类。这句话的意思是,这个节点要按照我定义的这个样式来,要按照我的格式来生成dom元素
3、锚点:endpoints,如下图,一般就是上下左右四个点。在一个节点里,锚点的id不可以重复;在不同的节点里,锚点可以重复。orientation: [1, 0] 表示锚点在右面, orientation: [-1, 0]表示锚点在左边,依次类推
在这里插入图片描述
4、线条,以下面的代码为例,参数代表什么含义上面代码里有备注就不说了。下面代码的作用就是从开始节点Aright锚点开始连线,连接到目标节点Bleft锚点上。

{
     sourceNode: 'A',
     targetNode: 'B',
     source: 'right',
     target: 'left',
     type: 'endpoint',
 },

在这里插入图片描述
5、生成节点和线

// 方式1,可以在点和线都存在的时候用,比如显示流程图
canvas.value.draw({ nodes: nodeList, edges: edgeList })

// 方式2.生成点和线有各自的方法
// 比如以上面为例,我将b节点删除后,我想让a自动连接到c上就可以使用

canvas.value.addNodes(nodeList)
canvas.value.addEdges(edgeList)

常用API学习

这里就简单学习一下实际工作中比较常用的,其他的自行查看官方API

画布(Canvas)

这里只记录常用的属性,其他内容自行查看官方API

root <dom> (必填)
实例容器,一般是一个具有宽高的dom元素, canvas 根节点(必传)

zoomable <Boolean> (选填)
画布是否可缩放;值类型 boolean,默认 false

moveable <Boolean> (选填)
画布是否可移动;值类型 boolean,默认 false

draggable <Boolean> (选填)
画布节点是否可拖动;值类型 boolean,默认 false

linkable <Boolean> (选填)
画布锚点是否可以拖动连线;值类型 boolean,默认 false

disLinkable <Boolean> (选填)
画布锚点是否可以拖动断开线;值类型 boolean,默认 false

layout <Object> (选填)
画布初始化根据设置的布局来自动排版

theme
画布主题配置,默认初始化样式和交互,内容有点多,自行查看官方API

画布API

常用API方法
canvas.draw (data, calllback)
作用:画布的渲染方法, 注意画布渲染是异步渲染

canvas.redraw (data, calllback)
作用:重新渲染方法,会将之前的所有元素删除重新渲染, 注意画布渲染是异步渲染

canvas.getDataMap (data, calllback)
作用:获取画布的所有数据:节点,线段,分组

canvas.setLinkable (boolean)
作用:设置画布所有节点是否可拉线

canvas.setDisLinkable (boolean)
作用:设置画布所有节点是否可断线

canvas.setDraggable (boolean)
作用:设置画布所有节点是否可拖动

canvas.getGroup (string)
作用:根据id获取group

canvas.addGroup (object|Group, nodes, options)
作用:添加分组。若分组不存在,则创建分组并把nodes放进分组内;若分组存在,则会把nodes放进当前分组内。

canvas.removeGroup (string | Group)
作用:删除节点组, 但不会删除里面的节点

canvas.getNode (string)
作用:根据id获取node

canvas.addNode ( object | Node )
作用:添加节点

canvas.addNodes ( array< object | Node > )
作用:批量添加节点

canvas.removeNode (string)
作用:根据id删除节点

canvas.removeNodes (array)
作用:批量删除节点

canvas.addEdge (object|Edge)
作用:添加连线

canvas.addEdges (array<object|Edge>)
作用:批量添加连线

canvas.removeEdge (param)
作用:根据id或者Edge对象来删除线

canvas.removeEdges (param)
作用:根据id或者Edge对象来批量删除线

canvas.getNeighborEdges (string)
作用:根据node id获取相邻的edge

canvas.setEdgeZIndex (edges, zIndex)
作用:设置线段z-index属性

canvas.setZoomable (boolean, boolean)
作用:设置画布缩放

画布事件

let canvas = new Canvas({...});
canvas.on('type key', (data) => {
  //data 数据
});

参数key值:

  • system.canvas.click 点击画布空白处
  • system.canvas.zoom 画布缩放
  • system.nodes.delete 删除节点
  • system.node.move 移动节点
  • system.node.click 点击节点
  • system.nodes.add 批量节点添加
  • system.links.delete 删除连线
  • system.link.connect 连线成功
  • system.link.reconnect 线段重连
  • system.link.click 线段点击事件
  • system.group.add 新增节点组
  • system.group.delete 删除节点组
  • system.group.move 移动节点组
  • system.group.addMembers 节点组添加节点
  • system.group.removeMembers 节点组删除节点
  • system.endpoint.limit 锚点连接数超过上限
  • system.multiple.select 框选结束
  • system.drag.start 拖动开始
  • system.drag.move 拖动
  • system.drag.end 拖动结束

画布辅助事件

canvas.setGridMode (show, options)
作用:设置网格背景


this.canvas.setGridMode(true, {
  isAdsorb: false,         // 是否自动吸附,默认关闭
  theme: {
    shapeType: 'line',     // 展示的类型,支持line & circle
    gap: 23,               // 网格间隙
    adsorbGap: 8,          // 吸附间距
    background: '#fff',     // 网格背景颜色
    lineColor: '#000',     // 网格线条颜色
    lineWidth: 1,          // 网格粗细
    circleRadiu: 1,        // 圆点半径
    circleColor: '#000'    // 圆点颜色
  }
});

canvas.setGuideLine (show, options)
作用:设置辅助线

this.canvas.setGuideLine(true, {
  limit: 1,             // 限制辅助线条数
  adsorp: {
    enable: false       // 开启吸附效果
    gap: 5              // 吸附间隔
  },
  theme: {
    lineColor: 'red',   // 网格线条颜色
    lineWidth: 1,       // 网格粗细
  }
});

canvas.save2img (options)
作用:画布保存为图片

canvas.updateRootResize ()
作用:当root移动或者大小发生变化时需要更新位置

注:
使用了一下网格和辅助线(可能是写的有问题),辅助线没有生效;网格第一次加载会很慢,其次就是缩放时,网格不会缩放。这里我加了一个缩放监听,来动态改变网格的大小,但是网格线会越来越多

如果想要网格背景的话,可以通过css来实现。

节点组(Group)

这个目前用不到,可以自行查看官方文档

节点(Node)

用法

const Node = require('butterfly-dag').Node;

// 当canvas为TreeCanvas时可选TreeNode
// const TreeNode = require('butterfly-dag').TreeNode;
class ANode extends Node {
  draw(obj) {
    // 这里可以根据业务需要,自己生成dom
  }
}

// 初始化画布渲染
canvas.draw({
  nodes: [{
    id: 'xxxx',
    top: 100,
    left: 100,
    Class: ANode //设置基类之后,画布会根据自定义的类来渲染
    // 参考下面属性
    ...
  }]
})

// 动态添加
canvas.addNode({
  id: 'xxx',
  top: 100,
  left: 100,
  Class: ANode
  // 参考下面属性
  ...
});

节点常用属性

id <String> (必填)
节点唯一标识

top <Number> (必填)
y轴坐标: 节点所在画布的坐标;若在节点组中,则是相对于节点组内部的坐标

left <Number> (必填)
x轴坐标: 节点所在画布的坐标;若在节点组中,则是相对于节点组内部的坐标

draggable <Boolean> (选填)
设置该节点是否能拖动:为可覆盖全局的draggable属性

group <String> (选填)
父级group的id: 设置后该节点会添加到节点组中

endpoints <Array> (选填)
系统锚点配置: 当有此配置会加上系统的锚点

Class <Class> (选填)
拓展类:当传入拓展类的时候,该节点则会按拓展类的draw方法进行渲染,拓展类的相关方法也会覆盖父类的方法

scope <Boolean> (选填)
作用域:当scope一致的节点才能拖动进入节点组

自定义属性
可以自定义属性,然后结合拓展类可以自定义节点的样式和内容

import {Node} from 'butterfly-dag';
import $ from 'jquery';
//自定义的节点样式
import './node.scss';

class BaseNode extends Node {
    constructor(opts) {
        super(opts);
        this.id = opts.id;
        this.top = opts.y;
        this.left = opts.x;
        this.options = opts;
    }
  draw = (opts) => {
      let container = $('<div class="fruchterman-node"></div>')
          .css('top', this.top + 'px')
          .css('left', this.left + 'px')
          .attr('id', this.id = opts.id);
      container.text(opts.options.label);

      return container[0];
  }
}

export default BaseNode;

节点外部调用API

node.getWidth ()
作用: 获取节点宽度

node.removeEndpoint(string)
作用:节点中删除锚点

node.getEndpoint (id, type)
作用:获取节点中的锚点

node.moveTo (x, y)
作用: 节点移动坐标的方法

node.remove ()
作用: 节点删除的方法。与canvas.removeNode的方法作用一致。

node.emit (event, data)
作用: 节点发送事件的方法,画布及任何一个元素都可接收。

[树状布局]treeNode.collapseNode (string)
作用: 树状节点的节点收缩功能

[树状布局]treeNode.expandNode (string)
作用: 树状节点的节点展开功能

线(Edge)

let edges = [{
    source: '0',
    target: '1'
}];

线属性

type <String> (选填)
标志线条连接到节点还是连接到锚点。默认值为endpoint

// endpoint类型线段: 锚点连接锚点的线段
{
  type: 'endpoint',
  sourceNode: '', //连接源节点id
  source: '',     //连接源锚点id
  targetNode: '', //连接目标节点id
  target: ''      //连接目标锚点id
}
// node类型线段: 节点连接节点的线段
{
  type: 'node',
  source: '',     //连接源节点id
  target: ''      //连接目标节点id
}

shapeType <String> (选填)
线条的类型: Bezier/Flow/Straight/Manhattan/AdvancedBezier/Bezier2-1/Bezier2-2/Bezier2-3/BrokenLine
在这里插入图片描述

label <String/Dom> (选填)
线条上注释: 可传字符串和dom

labelPosition <Number> (选填)
线条上注释的位置: 取值0-1之间, 0代表代表在线段开始处,1代表在线段结束处。 默认值0.5

arrow <Boolean> (选填)
是否加箭头配置: 默认false

arrowPosition <Number> (选填)
箭头位置: 取值0-1之间, 0代表代表在线段开始处,1代表在线段结束处。 默认值0.5

arrowShapeType <String> (选填)
箭头样式类型: 可使用系统集成的和可使用自己注册的,只需要保证类型对应即可。

// 自行注册的
import {Arrow} from 'butterfly-dag';
Arrow.registerArrow([{
  key: 'yourArrow1',
  type: 'svg',
  width: 10,   // 选填,默认8px
  height: 10,  // 选填,默认8px
  content: require('/your_fold/your_arrow.svg') // 引用外部svg
}, {
  key: 'yourArrow1',
  type: 'pathString',
  content: 'M5 0 L0 -2 Q 1.0 0 0 2 Z' // path的d属性
}]);

线段外部API

edge.redraw ()
作用: 更新线段位置: 线段所在的节点或者锚点位置发生变化后, 需要调用下redraw更新其对应的线

edge.setZIndex (index)
作用: 设置线段的z-index值

edge.updateLabel (label)
作用: 更新线段的注释

edge.remove ()
作用: 线段删除的方法。与canvas.removeEdge的方法作用一致。

edge.emit(event,data)
作用: 线段发送事件的方法,画布及任何一个元素都可接收。

edge.on(event,callback)
作用: 线段接收事件的方法,能接收画布及任何一个元素的事件。

edge.addAnimate (options)
作用: 给该线段加上动画

锚点

用法

// 用法一:
canvas.draw({
  nodes: [{
    ...
    endpoints: [{
      id: 'point_1',
      type: 'target',
      orientation: [-1, 0],
      pos: [0, 0.5]
    }]
  }]
})

// 用法二: 此方法必须在node的mount挂载后才能使用
let node = this.canvas.getNode('xxx');
node.addEndpoint({
  id: 'xxxx',
  type: 'target',
  dom: dom           // 使用此属性用户可以使用任意的一个dom作为一个锚点
});

锚点属性

id <String> (必填)
节点唯一标识

orientation <Array>(选填)
方向: (1) 控制系统锚点方向 (2) 控制线段的出入口方向
下: [0,1]、上: [0,-1]、右: [1,0]、左: [-1,0]

pos <Array> (选填)
位置: 控制系统锚点位置。可配合orientation使用,控制系统锚点
取值: [0-1之间 , 0-1之间],0代表最左/上侧,1代表最右/下侧

type <String> (选填)
锚点类型:

  • source: 来源锚点。线段只出不入
  • target: 目标锚点。线段只入不出
  • undefined: 未定义锚点。线段能入能出,但取决于第一根连线是入还是出
  • onlyConnect: 不能拖动断开线的锚点。线段能入能出,但拖动断开线

scope <String> (选填)
作用域: 锚点之间scope相同才可以连线。

disLinkable <Boolean > (选填)
禁止锚点拖动断开线段

其他内容略,自行查看官方文档

示例

let nodes = [
    {
        id: '0',
        label: 'a',
        x: 100,
        y: 100,
        Class: NodeClass,
        endpoints: [
            {
                id: 'point_0',
                type: 'source',
                orientation: [1,0]
            }
        ]
    },
    {
        id: '1',
        label: 'b',
        x: 200,
        y: 150,
        Class: NodeClass,
        endpoints: [
            {
                id: 'point_1',
                type: 'target',
                orientation: [-1,0]
            }
        ]
    }
];
let edges = [
   {
       type: 'endpoint',
       sourceNode: '0',
       source: 'point_0',
       targetNode: '1',
       target: 'point_1',
       arrow: true,
       arrowPosition: 0.8
 }];

在这里插入图片描述

提示 & 菜单(tooltips & menu)

提示用法

import {Tips} from 'butterfly-dag';
let container = document.getElementById('.you-target-dom');
Tips.createTip({
  className: `butterfly-custom-tips`,
  targetDom: container,
  genTipDom: () => { return $('<div>内容</div>')[0] },
  placement: 'right'
});

菜单用法

import {Tips} from 'butterfly-dag';
let container = document.getElementById('.you-target-dom');
Tips.createMenu({
  className: `butterfly-custom-menu`,
  targetDom: container,
  genTipDom: () => { return $('<div>内容</div>')[0] },
  placement: 'right',
  action: 'click',
  closable: true
});

API
在这里插入图片描述

tip示例

canvas.draw({
    groups: [], // 分组信息
    nodes: nodes, // 节点信息
    edges: edges // 连线信息
},(data) => {
    console.log('渲染完成了:',data);
    let nodes = data.nodes;
    nodes.forEach(item => {
        Tips.createTip({
            targetDom: item.dom,
            genTipDom: () => { return $(`<div>${item.options.label}</div>`)[0]; },
            placement: 'right'
        });
    });
});

在这里插入图片描述

布局(layout)

自行查看官方文档

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐