Vue中使用antV G6技术实现可拖拽拓扑图

10,651 阅读7分钟

1649744709(1).jpg G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。

安装 & 引用

在项目中引入 G6 有以下两种方式:NPM 引入;CDN 引入。

1 在项目中使用 NPM 包引入

Step 1:  使用命令行在项目目录下执行以下命令:

 npm install --save @antv/g6

Step 2:  在需要用的 G6 的 JS 文件中导入:

import G6 from '@antv/g6';

2 在 HTML 中使用  CDN 引入

// version <= 3.2
<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-{$version}/build/g6.js"></script>

// version >= 3.3
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/{$version}/dist/g6.min.js"></script>

// version >= 4.0
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.3.11/dist/g6.min.js"></script>

⚠️ 注意:

  • 在  {$version} 中填写版本号,例如  3.7.1
  • 最新版可以在  NPM  查看最新版本及版本号;
  • 详情参考 Github 分支:github.com/antvis/g6/t…

快速试用

创建一个 G6 的关系图仅需要下面几个步骤:

  1. 创建关系图的 HTML 容器;
  2. 数据准备;
  3. 创建关系图;
  4. 配置数据源,渲染。

Step 1 创建容器

需要在 HTML 中创建一个用于容纳 G6 绘制的图的容器,通常为 div  标签。G6 在绘制时会在该容器下追加 canvas 标签,然后将图绘制在其中。

<div id="mountNode"></div>

Step 2 数据准备

引入 G6 的数据源为 JSON 格式的对象。该对象中需要有节点(nodes)和边(edges)字段,分别用数组表示:

const data = {
  // 点集
  nodes: [
    {
      id: 'node1', // String,该节点存在则必须,节点的唯一标识
      x: 100, // Number,可选,节点位置的 x 值
      y: 200, // Number,可选,节点位置的 y 值
    },
    {
      id: 'node2', // String,该节点存在则必须,节点的唯一标识
      x: 300, // Number,可选,节点位置的 x 值
      y: 200, // Number,可选,节点位置的 y 值
    },
  ],
  // 边集
  edges: [
    {
      source: 'node1', // String,必须,起始点 id
      target: 'node2', // String,必须,目标点 id
    },
  ],
};

注意

  • nodes 数组中包含节点对象。每个节点对象中唯一的、必要的 id 以标识不同的节点,x、 y 指定该节点的位置;
  • edges 数组中包含边对象。source 和 target 是每条边的必要属性,分别代表了该边的起始点 id 与 目标点 id
  • 点和边的其他属性参见链接:内置节点 和 内置边

Step 3 创建关系图

创建关系图(实例化)时,至少需要为图设置容器、宽和高。

以下是vue+typescript下的创建实例

// 创建Graph实例
const graphDom: HTMLDivElement = this.$refs.mountNode as HTMLDivElement;
this.graph = new G6.Graph({
  container: graphDom, // 图的 DOM 容器
  fitView: true, // 是否开启画布自适应。开启后图自动适配画布大小。
  fitViewPadding: 100, // 画布的padding值
  modes: {
    default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点、设置高亮
  }
});
this.graph.read(this.graphData);  // 接收数据,并进行渲染,read 方法的功能相当于 data 和 render 方法的结合。

Step 4 配置数据源,渲染

graph.data(data); // 初始化的图数据,是一个包括 nodes 数组和 edges 数组的对象
graph.render(); // 渲染图

节点总览

G6 的内置节点包括 circle,rect,ellipse,diamond,triangle,star,image,modelRect,donut(v4.2.5 起支持)。这些内置节点的默认样式分别如下图所示。
img img

具体参考官网教程配置节点总览 | G6 (antv.vision),这里不再做阐述

边总览

阅读时间约 15 分钟

G6 提供了 9 种内置边:

  • line:直线,不支持控制点;
  • polyline:折线,支持多个控制点;
  • arc:圆弧线;
  • quadratic:二阶贝塞尔曲线;
  • cubic:三阶贝塞尔曲线;
  • cubic-vertical:垂直方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • cubic-horizontal:水平方向的三阶贝塞尔曲线,不考虑用户从外部传入的控制点;
  • loop:自环。

这些内置边的默认样式分别如下图所示。
img

G6 中的各个内置边类型、内置边的通用属性、配置方法。各类型边详细配置项及配置方法参考官网边总览 | G6 (antv.vision)

特殊交互

项目中需要实现一个节点hover时候弹窗显示该节点详情信息,如下图所示

image.png

代码如下

// 自定义tooltip插件,鼠标悬停显示详情信息
const tooltip = new G6.Tooltip({
  offsetX: -80,
  offsetY: -60,
  getContent(e: any) {
    const outDiv = document.createElement('div');
    outDiv.style.width = '180px';
    outDiv.innerHTML = `<ul>
      <li>${e.item?.getModel().label}</li>
    </ul>`
    return outDiv;
  },
  itemTypes: ['node']
});
// 自定义tooltip插件,鼠标悬停显示详情信息
this.graph = new G6.Graph({
  container: graphDom, // 图的 DOM 容器
  plugins: [tooltip], // 自定义tooltip插件
  layout: {
    type: 'dagre',
    rankdir: 'LR',
    linkDistance: 150,
  },
});

基础事件 Event | G6 (gitee.io)

使用方法示例如下

// 监听鼠标点击节点
this.graph.on('node:click', (e) => {
  const nodeItem: any = e.item;
  console.log('node:click', nodeItem.getModel());
});

事件汇总,如下

  • node:click 鼠标左键单击节点时触发

  • node:dblclick 鼠标双击左键节点时触发,同时会触发两次 node:click

  • node:mouseenter 鼠标移入节点时触发

  • node:mousemove 鼠标在节点内部移到时不断触发

  • node:mouseout 鼠标移出节点后触发

  • node:mouseover 鼠标移入节点上方时触发

  • node:mouseleave 鼠标移出节点时触发

  • node:mousedown 鼠标按钮在节点上按下(左键或者右键)时触发

  • node:mouseup 节点上按下的鼠标按钮被释放弹起时触发

  • node:dragstart 当节点开始被拖拽时触发,此事件作用在被拖拽节点上

  • node:drag 当节点在拖动过程中时触发,此事件作用于被拖拽节点上

  • node:dragend 当拖拽完成后触发,此事件作用在被拖拽节点上

  • node:dragenter 当拖拽节点进入目标元素的时候触发,此事件作用在目标元素上

  • node:dragleave 当拖拽节点离开目标元素的时候触发,此事件作用在目标元素上

  • node:dragover 当拖拽节点在另一目标元素上移动时触发,此事件作用在目标元素上

  • node:drop 被拖拽的节点在目标元素上同时鼠标放开触发,此事件作用在目标元素上

  • node:contextmenu 用户在节点上右击鼠标时触发并打开右键菜单

点击边触发指定边的骚操作

比如要实现下图的效果

image.png

仔细查阅了antV G6的整个API文档说明,终于让我找到了这么一个方法 image.png

1649744803(1).jpg 通过graph.getEdges()方法,拿到所有边实例,然后进行遍历,比对当前点击的边是否同属于同一个路径关系(父级ID)的边,是则选中,否则清除选中效果

    // 监听鼠标点击边
    this.graph.on('edge:click', (e) => {
      const currentItem: any = e.item.getModel();
      // console.log('edge:click', currentItem);
      this.graph.setAutoPaint(false);
      const edges = this.graph.getEdges();
      edges.map(item => {
        const edgeItem = item.getModel();
        const intersect = Common.intersect(edgeItem.pathId, currentItem.pathId);
        if (intersect.length > 0) {
          this.graph.setItemState(item, 'selected', true);
        } else {
          this.graph.setItemState(item, 'selected', false);
        };
      })
      this.graph.paint();
      this.graph.setAutoPaint(true);
    });
      Common.intersect方法
      /**
       * 比对两个数组返回交集
       * @param arr1 数组1
       * @param arr2 数组2
       * @returns 返回两数组的交集
       */
      intersect: (arr1: number[], arr2: number[]) => {
        return arr1.filter(x => new Set(arr2).has(x));
      }

展示所需数据格式如下

private graphData: any = {
    // 点集
    nodes: [
      {
        id: '1', // String,该节点存在则必须,节点的唯一标识
        // x: 100, // Number,可选,节点位置的 x 值
        // y: 200, // Number,可选,节点位置的 y 值
        label: 'S1',
        style: {              // 包裹样式属性的字段 style 与其他属性在数据结构上并行
          fill: '#16e473',       // 样式属性,元素的填充色
          stroke: '#888',     // 样式属性,元素的描边色
          lineWidth: 1, // 节点描边粗细
          // ...              // 其他样式属性
        },
        icon: {
          show: true,
          img: '...',
          // text: '...', 使用 iconfont
          width: 20,
          height: 20,
        },

      },
      {
        id: '8', // String,该节点存在则必须,节点的唯一标识
        label: 'B', // 节点文本
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12,      // 标签的样式属性,文字字体大小
            fill: '#ff578b'
            // ...            // 标签的其他样式属性
          }
        },
      },
      {
        id: '2', // String,该节点存在则必须,节点的唯一标识
        label: 'A',
      },
      {
        id: '3', // String,该节点存在则必须,节点的唯一标识
        label: 'Z1',
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12,      // 标签的样式属性,文字字体大小
            fill: '#00f3f6'
            // ...            // 标签的其他样式属性
          }
        },
      },
      {
        id: '4', // String,该节点存在则必须,节点的唯一标识
        label: 'Z2', // 节点文本
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12,      // 标签的样式属性,文字字体大小
            fill: 'red'
            // ...            // 标签的其他样式属性
          }
        },
      },
      {
        id: '7', // String,该节点存在则必须,节点的唯一标识
        label: 'Z', // 节点文本
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12,      // 标签的样式属性,文字字体大小
            fill: '#ff578b'
            // ...            // 标签的其他样式属性
          }
        },
      },
      {
        id: '5', // String,该节点存在则必须,节点的唯一标识
        label: 'S2', // 节点文本
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12      // 标签的样式属性,文字字体大小
            // ...            // 标签的其他样式属性
          }
        },
      },
      {
        id: '6', // String,该节点存在则必须,节点的唯一标识
        label: 'Y', // 节点文本
        labelCfg: {           // 标签配置属性
          position: 'bottom', // 标签的属性,标签在元素中的位置
          style: {            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            fontSize: 12      // 标签的样式属性,文字字体大小
            // ...            // 标签的其他样式属性
          }
        },
      }
    ],
    // 边集
    edges: [
      {
        pathId: [2],
        source: '1', // String,必须,起始点 id
        target: '2', // String,必须,目标点 id
        label: 'S1-A', // 节点文本
      },
      {
        pathId: [1,3],
        source: '8', // String,必须,起始点 id
        target: '6', // String,必须,目标点 id
      },
      {
        pathId: [2],
        source: '8', // String,必须,起始点 id
        target: '3', // String,必须,目标点 id
        labelCfg: {
          autoRotate: true,   // 使文本随边旋转
          style: {
            stroke: 'white',  // 给文本添加白边和白色背景
            lineWidth: 5,     // 文本白边粗细
            fill: '#722ed1',  // 文本颜色
          }
        },
      },
      {
        pathId: [3],
        source: '3', // String,必须,起始点 id
        target: '4', // String,必须,目标点 id
      },
      {
        pathId: [2,3],
        source: '3', // String,必须,起始点 id
        target: '6', // String,必须,目标点 id
      },
      {
        pathId: [1,2],
        source: '6', // String,必须,起始点 id
        target: '7', // String,必须,目标点 id
      },
      {
        pathId: [1,2,3],
        source: '2', // String,必须,起始点 id
        target: '8', // String,必须,目标点 id
        label: 'A-B', // 节点文本
        // color: '#722ed1',     // 边颜色
        labelCfg: {
          position: 'end',
          refY: -10,
        }
      },
      {
        pathId: [1,3],
        source: '5', // String,必须,起始点 id
        target: '2', // String,必须,目标点 id
        label: 'S2-A', // 节点文本
        labelCfg: {
          refY: -10,
        }
      }
    ],
  };