Bootstrap

使用antv/G6在vue项目中开发较复杂样式流程图


前言

设计师提供了一版样式较复杂的流程图,我搜了一些常用的vue-super-flow和vue-x6-flow等都只支持简单的样式。之前自己写过纯展示流程图不涉及太多交互,感觉还是找一个成熟的插件开发更适合,也方便其他同事参考,所以最后选择了用antv/G6自己个性化开发,总结了使用antv/G6在vue项目中开发常见流程图的过程。

示例如下:

在这里插入图片描述


一、什么是 G6?

G6官方文档链接 https://g6.antv.vision/zh/docs/manual/introduction
G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。

基于 G6,用户可以快速搭建自己的 图分析 或 图编辑 应用。

G6 作为一款专业的图可视化引擎,具有以下特性:
优秀的性能:支持大规模图数据的交互与探索;
丰富的元素:内置丰富的节点与边元素,自由配置,支持自定义;
可控的交互:内置 10+ 交互行为,支持自定义交互;
强大的布局:内置了 10+ 常用的图布局,支持自定义布局;
便捷的组件:优化内置组件功能及性能;
友好的体验:根据用户需求分层梳理文档,支持 TypeScript 类型推断。
除了默认好用、配置自由的内置功能,元素、交互、布局均具有高可扩展的自定义机制。

二、使用步骤

1.安装依赖并导入

npm install --save @antv/g6

import G6 from '@antv/g6';

2.初始化数据

常用的主要是两大类,一般图和树图。

一般图的数据结构是:

data:{
	//	节点
	nodes:[
	{
		id: 'XXX',
        label: 'XXX',
        key: 'XXX',
        text: 'XXX',
        img: require('XXX'),
	},
	...],
	// 连接线
	edges:[
	{
        source: '起始id',
       	target: '到达id',
	},
	...]
}

树图的数据结构是:

data:{
	id:'XXX',
	label: 'XXX',
  	children: [
    {
      	id: 'XXX',
      	label: 'XXX',
      	children: [
      	{
      		id: 'XXX',
      		label: 'XXX',
      		children: []
    	},
    	...]
    },
    ...],
}

我一开始选定了用树图实现,实现了样式以后发现没有办法满足产品要求的能够隐藏/显示不同层级并且重新渲染层级关系的功能,最后又更换成了一般图的布局方式。

3.树图实现

(1)首先配置自定义节点

 G6.registerNode(
          'icon-node', {
            options: {
              size: [90, 50], // 设置节点大小
              stroke: '#91d5ff', // 设置节点边框颜色
              fill: '#91d5ff', // 设置节点背景颜色
            },
            draw(cfg, group) {
              const styles = this.getShapeStyle(cfg);
              const {
                labelCfg = {}
              } = cfg;

              const w = styles.width;
              const h = styles.height;
              // 我的项目里需要区分多个节点显示不同的样式,我用key来区分
              if (cfg.key != "ACTION") {
                const keyShape = group.addShape('rect', {
                  attrs: {
                    ...styles,
                    x: -w / 2,
                    y: -h / 2,
                  },
                });
                /**
                 * leftIcon 格式如下:
                 *  {
                 *    style: ShapeStyle;
                 *    img: ''
                 *  }
                 */
                if (cfg.leftIcon) {
                  const {
                    style,
                    img
                  } = cfg.leftIcon;
                  // addShape是添加图形,可以添加不同的属性,具体配置可以看官方文档
                    group.addShape('rect', {
                      attrs: {
                        x: 1 - w / 2,
                        y: 1 - h / 2,
                        width: 38,
                        height: styles.height - 2,
                        fill: '#8c8c8c',
                        ...style,
                      },
                    });
                  group.addShape('image', {
                    attrs: {
                      x: 8 - w / 2,
                      y: 8 - h / 2,
                      width: 24,
                      height: 24,
                      img: img ||
                        'https://g.alicdn.com/cm-design/arms-trace/1.0.155/styles/armsTrace/images/TAIR.png',
                    },
                    name: 'image-shape',
                  });
                }
                if (cfg.label) {
                  group.addShape('text', {
                    attrs: {
                      ...labelCfg.style,
                      text: cfg.label,
                      x: 50 - w / 2,
                      y: 28 - h / 2,
                    },
                  });
                }

                return keyShape;
              } else {
                var keyShape = group.addShape('image', {
                  attrs: {
                    x: 8 - w / 2,
                    y: 8 - h / 2,
                    width: 24,
                    height: 24,
                    img: 'https://g.alicdn.com/cm-design/arms-trace/1.0.155/styles/armsTrace/images/TAIR.png',
                  },
                  name: 'image-shape',
                });
                group.addShape('text', {
                  attrs: {
                    fill: '#2f6bff',
                    fontSize: 24,
                    text: cfg.label,
                    x: 8 - w / 2,
                    y: 8 - h / 2,
                  },
                });
                group.addShape('text', {
                  attrs: {
                    fill: '#333333',
                    fontSize: 14,
                    text: '项目',
                    x: 8 - w / 2,
                    y: h - 4,
                  },
                });
                group.addShape('image', {
                  attrs: {
                    x: 40 - w / 2,
                    y: h - 20,
                    width: 14,
                    height: 14,
                    img: 'https://g.alicdn.com/cm-design/arms-trace/1.0.155/styles/armsTrace/images/TAIR.png',
                  },
                });
                return keyShape;

              }


            },
            update: undefined,
            // 自定义锚点,方便连线
            getAnchorPoints: () => {
              return [
                [0, 0.5], // 左侧中间
                [1, 0.5], // 右侧中间
              ];
            },
          },
          'single-shape',
        );

(2)自定义带箭头的贝塞尔曲线(连接线)

   G6.registerEdge('flow-line', {
          draw(cfg, group) {
            const startPoint = cfg.startPoint;
            const endPoint = cfg.endPoint;

            const {
              style
            } = cfg;
            const shape = group.addShape('path', {
              attrs: {
                stroke: style.stroke,
                // endArrow: style.endArrow,  // 需要箭头可以自己配置箭头 有开始箭头和结束箭头
                path: [  // 用path路径配置带折点的连接线 M表示开始的坐标,L表示拐点的坐标
                  ['M', startPoint.x, startPoint.y],
                  ['L', (startPoint.x + endPoint.x) / 2, (startPoint.y)],
                  ['L', (startPoint.x + endPoint.x) / 2, endPoint.y],
                  ['L', endPoint.x, endPoint.y],
                ],
              },
            });
            return shape;
          },
          afterDraw(cfg, group) {
            const shape = group.get('children')[0];
            const startPoint = shape.getPoint(0);
            //创建节点之间的圆圈,并为每一个设置样式
            const circle = group.addShape('circle', {
              attrs: {
                x: startPoint.x + 92,
                y: startPoint.y,
                fill: '#cbdaff',
                r: 4, //圆圈大小
              },
              name: 'circle-shape',
            });
            // 实现动态效果,从线的开始滑到结束
            circle.animate(
              ratio => {
                const tmpPoint = shape.getPoint(ratio);
                return {
                  x: tmpPoint.x,
                  y: tmpPoint.y,

                };
              }, {
                repeat: 1, //动画是否重复
                duration: 3000, //一次动画持续时长
              },
            );
          },
        }, );

(3)设置默认样式

        // 设置默认开启状态样式
 		const defaultStateStyles = {
          hover: {
            stroke: '#2f6bff',
          },
        };
		// 设置默认节点样式
        const defaultNodeStyle = {
          fill: '#fff',
          stroke: '#cbdaff',
          radius: 5,
          shadowColor: '#cbdaff', // 阴影颜色
          shadowBlur: 10, // 阴影范围
          shadowOffsetX: 0, // 阴影偏移位置
          shadowOffsetY: 5,
          cursor: 'pointer', // 鼠标移动到节点上的样式
        };
		// 设置默认链接线样式
        const defaultEdgeStyle = {
          stroke: '#cbdaff',
          // 结尾箭头样式
          endArrow: {
            path: 'M 0,0 L 12, 6 L 9,0 L 12, -6 Z',
            fill: '#91d5ff',
            d: -60, // x轴上的偏移量
          },
        };
        // 设置默认节点名称样式
        const defaultLabelCfg = {
          style: {
            fill: '#333',
            fontSize: 14,
          },
        };

(4)配置树图并渲染

const graph = new G6.TreeGraph({
  container: 'html元素的id',
  width: XXX, // 渲染的宽度
  height: XXX, // 渲染的高度
  linkCenter: true,
  plugins: [minimap], // 配置插件
  modes: {  
    default: ['drag-canvas', 'zoom-canvas'],  // 常用的拖拽和缩放
  },
  defaultNode: {
    type: 'icon-node',  // 自定义节点名称,记得保持一致
    size: [120, 40],  //设置节点大小
    style: defaultNodeStyle, // 配置过的默认样式,直接写在这里也可以
    labelCfg: defaultLabelCfg, // 配置过的默认样式,直接写在这里也可以
  },
  defaultEdge: {
    type: 'flow-line', // 自定义连接线名称,记得保持一致
    style: defaultEdgeStyle, // 配置过的默认样式,直接写在这里也可以
  },
  nodeStateStyles: defaultStateStyles, // 配置过的默认样式,直接写在这里也可以
  edgeStateStyles: defaultStateStyles, // 配置过的默认样式,直接写在这里也可以
  // 若数据中不存在节点位置,则默认为随机布局。配置布局类型及其参数。
  layout: {
  // 类型  总共三种:径向:radial   有向分层:dagre     力导:force
  type: 'dagre',
  rankdir: 'LR', // 可选值:'TB' | 'BT' | 'LR' | 'RL',默认为图的中心 TB T,B,L,R 代表top,bottom,left,right
  },
});

graph.data(data); // 配置数据
graph.render();   // 渲染流程图
graph.fitView();  // 让画布内容适应视口,可选参数padding, rules, animate, animateCfg

(6)小地图插件

		// 设置出现一个缩略地图
		const minimap = new G6.Minimap({
          size: [150, 100],
        });

4.一般图实现

(1)自定义节点

一般图中获取节点属性获取不到节点的宽高,所以自己设置一个w、h,然后再刚刚的基础上加入一些细节

   G6.registerNode(
          // 该新节点类型名称
          'node',
          // 该新节点类型的定义
          // 当有 extendedTypeName (第三个参数)时,没被复写的函数将会继承 extendedTypeName 的定义
          {
            // cfg  节点身上所有的配置:包括label,size,x,y坐标等 

            // 这个方法,每渲染一个节点,执行一次
            drawShape(cfg, group) {
              const styles = this.getShapeStyle(cfg);
              // ctg上的id key label 都可以决定当前节点的类型
              
              // 可以根据标题长度设置节点的大小
              // cfg.size = (cfg.label.length / 10) * 130; 
              // if (cfg.label.length / 10 < 1) {
              //   widthX = 120;
              // } else {
              //   widthX = cfg.size;
              // }
              
              let w = 180,
                  h = 50;
              // 根据设计图第一个节点主要是展示图片和个数,配置对应的形状
              if (cfg.key == "0") {
                w = 100;
                h = 155;
                var keyShape = group.addShape('image', {
                  attrs: {
                    x: -w / 2,
                    y: -h / 2,
                    width: w,
                    height: h,
                    img: cfg.img,
                  },
                  name: 'image-shape',
                });
                group.addShape('circle', {
                  attrs: {
                    x: w / 2,
                    y: 0,
                    r: 2.5,
                    fill: '#fff',
                    stroke: '#cbdaff',
                    lineWidth: 2,
                  },
                });
                group.addShape('text', {
                  attrs: {
                    fill: '#2f6bff',
                    fontSize: 24,
                    text: cfg.label,
                    textAlign: 'center',
                    x: 0,
                    y: -45,
                  },
                });
                group.addShape('text', {
                  attrs: {
                    fill: '#333333',
                    fontSize: 14,
                    text: '项目',
                    textAlign: 'center',
                    x: 0,
                    y: h / 2 - 30,
                  },
                });
                return keyShape;
              } else {
              // 根据标题的长度判断节点的高度
                var rowNum = 0
                for (let i = 0; i < cfg.label.length; i++) {
                  var j = i + rowNum
                  if (i % 8 == 0 && i > 0 && j != cfg.label.length) {
                    var newStr = cfg.label.slice(0, j) + "\n" + cfg.label.slice(j);
                    cfg.label = newStr
                    rowNum++;
                  }
                }
                if (rowNum > 1) {
                  h += (rowNum - 1) * 15
                }
                cfg.size = [w, h]
                const rect = group.addShape('rect', {
                  attrs: {
                    x: -w / 2,
                    y: -h / 2,
                    width: w,
                    height: h,
                    fill: '#fff',
                    stroke: '#a0d4ff',
                    radius: 9,
                    shadowColor: '#a0d4ff', // 阴影颜色
                    shadowBlur: 10, // 阴影范围
                    shadowOffsetX: 0, // 阴影偏移位置
                    shadowOffsetY: 5,
                    cursor: 'pointer', // 鼠标移动 到节点上的样式
                  },
                });
                group.addShape('circle', {
                  attrs: {
                    x: 0 - w / 2,
                    y: 0,
                    r: 2.5,
                    fill: '#fff',
                    stroke: '#a0d4ff',
                    lineWidth: 2,
                  },
                });
                group.addShape('image', {
                  attrs: {
                    x: cfg.key != '1' ? 17 - w / 2 : 12 - w / 2,
                    y: cfg.key != '1' ? -15 : -19,
                    width: cfg.key != '1' ? 30 : 38,
                    height: cfg.key != '1' ? 30 : 38,
                    img: cfg.img,
                  },
                });

                group.addShape('text', {
                  attrs: {
                    x: 60 - w / 2,
                    y: 0,
                    fill: '#333333',
                    fontSize: 14,
                    text: cfg.label,
                    textBaseline: 'middle',
                  },
                });
                return rect;
              }

            },
            /**
             * 获取锚点(相关边的连入点)
             * @param  {Object} cfg 节点的配置项
             * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点
             */
            // 自定义锚点
            getAnchorPoints: () => {
              return [
                [0, 0.5], // 左侧中间
                [1, 0.5], // 右侧中间
              ];
            },
          },

          // 被继承的节点类型,可以是内置节点类型名,也可以是其他自定义节点的类型名。
          // extendedTypeName 未指定时代表不继承其他类型的节点;
          // 例如基类 'single-node',或 'circle', 'rect' 等
          'single-shape'
        );

连接线跟上面树图的配置一样即可,如果想要曲线可以自己修改

(2)配置图属性并渲染

 const graph = new G6.Graph({
          // 常用配置项 
          // 类型:Boolean;默认:'false'。图是否自适应画布。
          fitView: true,
          // 类型:Number | Array;默认:0。图自适应画布时的四周留白像素值。fitView 为 true 时生效。
          // fitViewPadding : 0
          // 类型:Boolean;默认:'false'。是否平移图使其中心对齐到画布中心。v3.5.1 后支持。
          fitCenter: true,
          // bloodView:流程图容器id
          container: 'projectView',
          width:'XXX',
          height:'XXX',
          //modes 交互行为相关
          // 配置多种交互模式及其包含的交互事件的。
          modes: {
            default: [
              'drag-canvas',
              'zoom-canvas',
              // 如果没有特殊的样式可以直接用内置的tooltip
              //       {
              //         type: 'tooltip',
              //         formatText(model) {
              //           console.log(model)
              //           const cfg = model.text;
              //           const text = model.text;
              //           // cfg.forEach((row) => {
              //           //   text.push(row.label + ':' + row.value + '<br>');
              //           // });
              //           return text;
              //         },
              //         offsetX: 70,
              // 		 offsetY: 20,
              //       },
            ],
          },
          plugins: [tooltip], // 这次项目悬浮框用了插件
          zoom: 1,
          // 若数据中不存在节点位置,则默认为随机布局。配置布局类型及其参数。
          layout: {
            // 类型  总共三种:径向:radial   有向分层:dagre     力导:force
            type: 'dagre',
            // 'LR':从左至右布局;
            rankdir: 'LR', // 可选可选值:'TB' | 'BT' | 'LR' | 'RL',默认为图的中心 TB
            nodesep: 20,  //节点之间的距离,水平布局时指垂直距离
            ranksep: 40,  //层级之间的距离,水平布局时指垂直距离
          },
          // defaultNode类型:Object。默认情况下全局节点的配置项,包括样式属性和其他属性
          // G6 的内置节点包括 
          // circle圆形,rect长方形,ellipse椭圆,diamond菱形,triangle三角形,
          // star五角星,image图片,modelRect卡片,donut圆形(v4.2.5 起支持)。
          defaultNode: {
            type: 'node', // 这里的type指向自定义节点
            // size:300,
            size: [180, 50],
            style: defaultNodeStyle,
            labelCfg: defaultLabelCfg,
          },
          // defaultEdge  类型:Object。默认情况下全局边的配置项,包括样式属性和其他属性
          defaultEdge: {
            type: 'flow-line',
            style: {
              endArrow: true,
              lineWidth: 2,
              stroke: '#cbdaff',
            },
          },
          nodeStateStyles: defaultStateStyles,
          edgeStateStyles: defaultStateStyles,
        });
       	// 动态配置完层级后需要销毁后重新渲染,否则页面上会显示多个渲染的流程图
         if (this.graph != null) {
            this.graph.destroy();
          }
          this.graph = graph
          graph.data(data);
           // 渲染
          graph.render();
        // this.graph.zoomTo(1);  // 缩放 参数为缩放的倍数,后面也可跟x,y坐标,将以传入坐标为中心
        // this.graph.moveTo(0,height/2); // 采用绝对位移将画布移动到指定坐标

(3)自定义tooltip

需求中的悬浮框需要展示该节点下面的所有项目及带有查看详情按钮,所以我使用了插件没有用内置的。

需要注意的一点是在getContent()里的代码一定要严格遵守编写规范,尤其注意后面一定要带';',否则会一直报错而且你找不到原因!!!

 const tooltip = new G6.Tooltip({
          offsetX: -400,  // 偏移距离
          offsetY: -850,
          trigger: 'click', // 触发方式 可选 mouseenter,
          getContent(e) {
            const outDiv = document.createElement('div');
            outDiv.style.width = '270px';
            outDiv.style.padding = '0 10px';
            let ulText = ``;
            if (typeof (e.item.getModel().text) == 'object') {
              (e.item.getModel().text).map((item, index) => {
                ulText +=
                  `<li style='padding:5px 60px 5px 0;
                position: relative;word-break: break-all;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;font-size: 12px;'>项目名称:<span style="color:#333">${item.label}</span><span type="text" style="color: #2f6bff;border:none;background:transparent;cursor:pointer;  position: absolute;top: 50%;right: 0;transform: translateY(-50%);">查看详情</span></li>`;
              });
              outDiv.innerHTML =
                `<div style="font-size:14px;color:#333;font-weight:bold;">${e.item.getModel().label}</div><ul style="margin:10px 0 0;padding:0;font-size:12px">` +
                ulText +
                `</ul>`;
            } else {
              outDiv.innerHTML =
                `<div style="font-size:14px;color:#333;font-weight:bold;">${e.item.getModel().label}</div>`;
            }

            return outDiv
          },
          itemTypes: ['node']
        });

5.插件

G6 提供了一些可插拔的组件,包括:
Legend (v4.3.0 起支持)
SnapLine (v4.3.0 起支持)
Grid
Minimap
ImageMinimap
Edge Bundling
Menu
ToolBar
TimeBar
Tooltip
Fisheye
EdgeFilterLens

三、注意事项

如何让 IE 支持 G6

对于这类问题,我们在项目中只需要引入 babel-polyfill 即可,具体使用方法如下:

在主入门文件中引入 babel-polyfill ;
在 bable-loader 中加入如下代码:

{
    test: /\.js$/,
    loader: 'babel-loader',
    include: [resolve('src'), resolve('node_modules/@antv/g6')]
}

include 表示哪些目录中的 .js 文件需要进行 babel-loader;exclude 表示哪些目录中的 .js 文件不要进行 babel-loader。

总结

以上就是今天要讲的内容,本文仅仅简单介绍了antv/G6的使用,更多的示例及API请移步官方文档。

;