AntV G6在Vue中的实际应用案例分享(上)

此文章用于回顾之前使用AntV G6实现需求的过程:了解、熟悉、运用、落地;

分为上下两部分,下一篇文章分享下一个案例。

需求

下图部分就是此次需要使用AntV G6实现的部分,下图是整个页面的左侧部分,能够收起和展开;其中的图形部分具有以下功能:

  • 支持整体缩放、拖动
  • 图中节点图片由后端数据决定
  • 图中右侧节点下方的文本、中间节点的上下文本、右侧节点右边的文本由后端数据动态渲染
  • 节点连接关系由后端数据决定
  • 点击绿色图标有tooltip效果,内容由数据渲染,再次点击tooltip的内容能够触发事件作用于外部

以下内容着重描述图形部分实现,面板收缩功能不在此次讨论范围内。

ps:示意图中数据为模拟数据。

选型

在接到这个需求的时候,在当时团队的技术栈中,ECharts看似能解决这个需求。但在对ECharts调研之后,发现它并不能处理这种自定义程度比较高的任务。团队中也没有相关的技术能处理这个需求,这时就需要接入一个新的技术。

这里我就省去查找技术的过程,直接说答案,在这里我选择了使用AntV G6来处理这个需求,原因有以下几点:

  • 大厂出品
  • 持续维护
  • 社区良好
  • 文档完善

其中主要是「文档完善」这一点在我这里比重很大,因为当时这个需求的时间不充裕,完善的文档能提高开发效率。

接入AntV G6过程

熟悉文档

接入一个新技术,当然首先要了解并熟悉。在官方文档中,需要先掌握以下内容:

在阅读完上述文档之后,就能够对G6有一个基础的认识:

  • 节点
  • 布局
  • 交互
    • 管理
    • 行为
    • 状态
  • 插件
  • 动画

注意事项: 这里要特别指出,可以优先阅读FAQ部分,这里面总结了不同技术栈在使用G6时遇到的问题,阅读此部分能够判断当前技术栈是否能引入该技术。

编写基础案例

经过熟悉之后,我就先把技术引入到项目中,并且按照需求图的布局方式尝试写一个小案例,以达到熟悉的目的:

ps: 基础案例背景色我习惯用pink,是因为我的启蒙老师是pink老师

html 复制代码
<template>
  <div class="container">
    <div class="g6" id="g6"></div>
  </div>
</template>

<script setup>
import G6 from '@antv/g6';
import {onMounted} from 'vue';
const initData = {
  // 点集
  nodes: [
    {
      id: 'node1', // 节点的唯一标识
      label: '1', // 节点文本
    },
    {id: 'node2', label: '2'},
    {id: 'node3', label: '3'},
    {id: 'node4', label: '4'},
    {id: 'node5', label: '5'},
    {id: 'node6', label: '6'},
    {id: 'node7', label: '7'},
  ],
  // 边集
  edges: [
    {
      source: 'node1', // 起始点 id
      target: 'node2', // 目标点 id
    },
    {source: 'node2', target: 'node3'},
    {source: 'node1', target: 'node4'},
    {source: 'node4', target: 'node5'},
    {source: 'node1', target: 'node6'},
    {source: 'node6', target: 'node7'},
  ],
};

onMounted(() => {
  const graph = new G6.Graph({
    container: 'g6', // 指定挂载容器
    width: 500, // 图的宽度
    height: 500, // 图的高度
    layout: {
      type: 'dagre',
      // 布局方向
      rankdir: 'LR',
      // 节点对齐方式
      align: 'DL',
      // 是否保留布局连线的控制点
      controlPoints: true,
      // nodesep 竖直间距
      nodesepFunc: () => 20,
      // ranksep 水平方向层间距
      ranksepFunc: () => 34,
    },
  });
  graph.data(initData); // 加载数据
  graph.render(); // 渲染
});
</script>

<style lang="less" scoped>
.container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  .g6 {
    width: 500px;
    height: 500px;
    background-color: pink;
  }
}
</style>

剩余问题

目前撰写的案例和需求的要求相比,还有以下几个点需要处理:

  • 自定义节点
  • 自定义边
  • 实现tooltip
  • 定义数据结构
  • 封装

这些比较深入的部分就需要阅读「核心概念部分」之后才能实现,核心概念总览如下:

在这里需要重点阅读以下内容:

  1. 图(基础定义)
  2. 图形(基础定义)
  3. 图布局(实现图布局)
  4. 交互与事件(实现交互)
  5. 插件(实现tooltip)
  6. 节点(实现自定义节点)
  7. 边(实现自定义边)

自定义节点

三种节点的样式都较为相近,都是以图片为核心,周围加上文字修饰,所以我就准备在内置的image节点基础上来绘制节点:

  • 注册节点
  • 绘制节点

主要是利用了内置的textimage节点,设置好位置之后,返回image关键图形;其中根据节点数据中的nodeType字段来判断添加的文本内容以及渲染相应的图片

js 复制代码
G6.registerNode('dom-node-1', {
    /**
     * 绘制节点,包含文本
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
     * 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape
     */
    draw: (cfg, group) => {
      // nodeType自定义数据,用于区分节点
      switch (cfg.nodeType) {
        case 'bbu':
          // 添加下方文本
          group.addShape('text', {
            attrs: {
              x: -10,
              y: 80,
              fontSize: 12,
              fontWeight: 'bold',
              fill: '#292d33',
              text: cfg[cfg.nodeType].enodebId ?? '--',
            },
          });
          break;
        case 'rru':
          // 添加上方文本
          group.addShape('text', {
            attrs: {
              x: -10,
              y: 80,
              fontSize: 12,
              fontWeight: 'bold',
              fill: '#292d33',
              text: cfg[cfg.nodeType].rruCode ?? '--',
            },
          });
          // 添加下方文本
          group.addShape('text', {
            attrs: {
              x: 20,
              y: -7,
              fontSize: 14,
              fontWeight: 'bold',
              fill: '#1a988e',
              text: cfg[cfg.nodeType].vendor ?? '--',
            },
          });
          break;
        case 'antenna':
          // 添加右侧DOM结构
          group.addShape('dom', {
            attrs: {
              x: 68,
              y: 13,
              width: 73,
              height: 50,
              html: `
                  <div style="width: 73px;height: 50px;display: grid;grid-template-columns: repeat(2, 1fr);grid-template-rows: repeat(2, 1fr);font-weight: bold;">
                    <div>e:${cfg.antenna.electronDowndip ?? '--'}</div>
                    <div style="text-align: right;">m:${cfg.antenna.mechanicalDowndip ?? '--'}</div>
                    <div>a:${cfg.antenna.azimuth ?? '--'}</div>
                    <div style="text-align: right;">h:${cfg.antenna.antennaHeight ?? '--'}</div>
                  </div>
                    `,
            },
            draggable: true,
          });
          break;
        default:
          break;
      }
      // 返回关键图形「image」
      return group.addShape('image', {
        attrs: {
          x: 0,
          y: 0,
          width: 75,
          height: 65,
          // imgData为封装的图片数据
          img: imgData[cfg.nodeType],
        },
        name: 'image-shape',
      });
    },
    // 调整锚点 anchorPoint,确定节点与边的相交的位置
    getAnchorPoints() {
      return [
        [0, 0.5], // 左侧中间
        [1, 0.5], // 右侧中间
      ];
    },
  });

自定义边

由于给出的需求图中的边与G6默认边差距较大,所以这里也需要自定义边(包含边、箭头)。

实现这部分需要先阅读:

第一个自定义边以及自定义箭头:

js 复制代码
G6.registerEdge('lk-line-one', {
    /**
     * 绘制节点,包含文本
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape
     */
    draw(cfg, group) {
      // 开始节点
      const startPoint = cfg.startPoint;
      // 结束节点
      const endPoint = cfg.endPoint;
      // 图形
      const shape = group.addShape('path', {
        attrs: {
          // 描边颜色
          stroke: '#f7b551',
          // path是svg里面的path,M是移动画笔,moveto的缩写,L是lineto的缩写,线移动
          // 大写代表绝对定位,小写代表相对定位
          path: [
            ['M', endPoint.x / 4 + (2 / 3) * startPoint.x, startPoint.y],
            ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y],
            ['L', endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y],
            ['L', endPoint.x, endPoint.y],
          ],
          // 描边虚线,Number[] 类型中数组元素分别代表实、虚长度
          lineDash: [5],
          // 描边宽度
          lineWidth: 2,
          // 开始箭头
          startArrow: {
            // 填充色
            fill: '#000',
            // 透明度
            opacity: 0.5,
            // path是svg里面的path,M是移动画笔,moveto的缩写,L是lineto的缩写,线移动。a是弧形
            // 从坐标0,0开始画两个弧形凑成一个空心圆
            path: 'M 0,0 a 2 2,0,1,1, 2 2 a 2 2, 0,1,1, 2, -2',
          },
          // 结束箭头
          endArrow: {
            // 用线构建一个箭头
            path: 'M 0,0 l -3,3 l 0,0 l 3,-3 l -3,-3 l 0,0 l 3,3 Z',
          },
        },
        // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
        name: 'edge-shape',
      });
      return shape;
    },
  });

第二个自定义边以及自定义箭头:

js 复制代码
G6.registerEdge('lk-line-two', {
    /**
     * 绘制节点,包含文本
     * @param  {Object} cfg 节点的配置项
     * @param  {G.Group} group 图形分组,节点中图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape
     */
    draw(cfg, group) {
      // 开始节点
      const startPoint = cfg.startPoint;
      // 结束节点
      const endPoint = cfg.endPoint;
      // 图形
      const shape = group.addShape('path', {
        attrs: {
          // 描边
          stroke: '#eb6877',
          // path是svg里面的path,M是移动画笔,moveto的缩写,L是lineto的缩写,线移动
          // 大写代表绝对定位,小写代表相对定位
          path: [
            ['M', endPoint.x / 4 + (2 / 3) * startPoint.x, startPoint.y],
            ['L', endPoint.x / 4 + (2 / 3) * startPoint.x, startPoint.y],
            ['L', endPoint.x / 4 + (2 / 3) * startPoint.x, endPoint.y],
            ['L', endPoint.x, endPoint.y],
          ],
          // 描边虚线,Number[] 类型中数组元素分别代表实、虚长度
          lineDash: [5],
          // 描边宽度
          lineWidth: 2,
          // 结束箭头
          endArrow: {
            // 用线构建一个箭头
            path: 'M 0,0 l -3,3 l 0,0 l 3,-3 l -3,-3 l 0,0 l 3,3 Z',
          },
        },
        // 在 G6 3.3 及之后的版本中,必须指定 name,可以是任意字符串,但需要在同一个自定义元素类型中保持唯一性
        name: 'path-shape',
      });
      return shape;
    },
  });

实现tooltip

到这一个步骤的时候,其实整体样式已经实现了,最后还差一个tooltip的功能,具体使用方式在这里G6插件的使用方式类似于Webpack里面的插件,也是需要实例化之后传到plugins数组中。

js 复制代码
const tooltip = new G6.Tooltip({
    // tooltip 的 x 方向偏移值,需要考虑父级容器的 padding
    offsetX: 10,
    // tooltip 的 y 方向偏移值,需要考虑父级容器的 padding
    offsetY: 20,
    // 取出nodeType,控制是否显示tooltip
    shouldBegin(e) {
      const {
        item: {
          _cfg: {
            model: {nodeType},
          },
        },
      } = e;
      const judgeAction = {
        bbu: false,
        rru: true,
        antenna: false,
      };
      return judgeAction[nodeType];
    },
    // tooltip 内容,支持 DOM 元素或字符串
    getContent(e) {
      const {
        item: {
          _cfg: {
            model: {nodeType},
          },
        },
      } = e;
      if (nodeType === 'rru') {
        const {
          item: {
            _cfg: {
              model: {
                rru: {cellList},
              },
            },
          },
        } = e;
        const cellHtmlStr = cellList.map((item) => {
          return `<div class="rru-item">${item.cellkey}</div>`;
        });
        const outDiv = document.createElement('div');
        outDiv.className = 'rru-container';
        outDiv.innerHTML = cellHtmlStr.join('');

        setTimeout(() => {
          const cell = document.getElementsByClassName('rru-item');
          Array.from(cell, (item, ind) => {
            item.onclick = (e) => {
              // fn是外部传进来的,响应外部事件
              fn(cellList[ind].cellName, cellList[ind].cellkey);
            };
          });
        }, 100);
        return outDiv;
      }
      return '';
    },
    trigger: 'click',
    itemTypes: ['node'],
  });

最终图布局配置:

js 复制代码
new G6.Graph({
    // 传入的渲染节点id
    container: id,
    // 传入的DOM ref
    width: ref.clientWidth,
    height: ref.clientHeight,
    // 自适应画布
    fitView: true,
    // 自适应画布时四周留白像素值,fitView为true时生效
    fitViewPadding: [0, 20, 20, 10],
    layout: {
      // 布局类型
      type: 'dagre',
      /* 
      说明:布局的方向。T:top(上);B:bottom(下);L:left(左);R:right(右)。
      'TB':从上至下布局;
      'BT':从下至上布局;
      'LR':从左至右布局;
      'RL':从右至左布局。
      */
      rankdir: 'LR',
      /* 
      说明:节点对齐方式。U:upper(上);D:down(下);L:left(左);R:right(右)
      'UL':对齐到左上角;
      'UR':对齐到右上角;
      'DL':对齐到左下角;
      'DR':对齐到右下角;
      undefined:默认,中间对齐。
      */
      align: 'DL',
      // 是否保留布局连线的控制点
      controlPoints: true,
      // nodesep 竖直间距
      nodesepFunc: () => 20,
      // ranksep 水平方向层间距
      ranksepFunc: () => 34,
    },
    // 交互管理,一个mode是多种行为Behavior的组合
    modes: {
      // 缩放画布、拖拽画布
      default: ['zoom-canvas', 'drag-canvas'],
    },
    plugins: [tooltip],
    renderer: 'svg', // 使用 Dom node 的时候需要使用 svg 的渲染形势
    defaultNode: {
      type: 'dom-node-1',
    },
    fitCenter: true,
  });

定义数据结构

整体数据结构是一个对象,有两个数组:nodesedges,代表节点数据和边数据;其中未标明自定义字段 的都是必要字段

js 复制代码
const imitateData = {
  // 点集
  nodes: [
    {
      // 节点唯一标识,类型为String
      id: 'node1',
      // 节点类型,默认为circle
      type: 'dom-node-1',
      // 自定义字段
      nodeType: 'bbu',
      // 自定义字段
      bbu: {enodebId: 'XX-679021'},
    },
    {
      // 节点唯一标识,类型为String
      id: 'node2',
      // 节点类型,默认为circle
      type: 'dom-node-1',
      // 自定义字段
      nodeType: 'rru',
      // 自定义字段
      rru: {rruCode: '210108434518', vendor: 'L1.8G', cellList: [{cellkey: '111'}, {cellkey: '222'}]},
    },
    {
      // 节点唯一标识,类型为String
      id: 'node3',
      // 节点类型,默认为circle
      type: 'dom-node-1',
      // 自定义字段
      nodeType: 'antenna',
      // 自定义字段
      antenna: {electronDowndip: '4', mechanicalDowndip: '3', azimuth: '85', antennaHeight: '23'},
    },
    ......
  ],
  // 边集
  edges: [
    {
      // 自定义字段
      id: 'edge1',
      // 边类型
      type: 'lk-line-one',
      // 开始节点,对应节点id
      source: 'node1',
      // 结束节点,对应结束id
      target: 'node2',
    },
    {
      // 自定义字段
      id: 'edge2',
      // 边类型
      type: 'lk-line-two',
      // 开始节点,对应节点id
      source: 'node2',
      // 结束节点,对应结束id
      target: 'node3',
    },
    ......
  ],
};

整合封装

到这一步,使用G6做图的任务已经完成了,剩下的任务就是将其整合封装为一个函数并导出:

代码

受限于篇幅问题,我将此次案例的代码,放在了github gist(打不开则需要科学上网)。目录结构如下:

以上代码不包含图片文件,如果需要完整实例,请访问github Demo(打不开则需要科学上网)。

总结

以上就是此次实现G6的全部过程,其中的难点主要集中在自定义节点和边 的部分,除了熟悉文档相关内容,还需要对SVG path掌握一定的知识。朋友们在初次使用这个技术来实现需求的时候,可以借鉴我的实现路线,我把需要掌握的文档中的内容都用链接🔗的形式标注出来了。

总体来说是一个比较愉快的过程。

再啰嗦一句,「文档写的好,使用者用起来很轻松👻」

ok,就这样!下一篇文章再见!

相关推荐
gnip13 分钟前
包管理工具的发展
前端
前端工作日常1 小时前
H5 实时摄像头 + 麦克风:完整可运行 Demo 与深度拆解
前端·javascript
韩沛晓1 小时前
uniapp跨域怎么解决
前端·javascript·uni-app
前端工作日常1 小时前
以 Vue 项目为例串联eslint整个流程
前端·eslint
程序员鱼皮1 小时前
太香了!我连夜给项目加上了这套 Java 监控系统
java·前端·程序员
Rubin932 小时前
TS 相关
javascript
该用户已不存在2 小时前
这几款Rust工具,开发体验直线上升
前端·后端·rust
前端雾辰2 小时前
Uniapp APP 端实现 TCP Socket 通信(ZPL 打印实战)
前端
无羡仙2 小时前
虚拟列表:怎么显示大量数据不卡
前端·react.js
云水边2 小时前
前端网络性能优化
前端