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,就这样!下一篇文章再见!

相关推荐
浮华似水17 分钟前
简洁之道 - React Hook Form
前端
正小安2 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch4 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光4 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   4 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   4 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web4 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常4 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇5 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr5 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui