Antv g6实现拓扑图-VUE版本

3,952 阅读8分钟

背景

在项目中要实现拓扑图,翻看尝试了一些,比如使用echarts来实现拓扑图,但是好像结果不尽人意,然后就去用g6了,下面是一些使用的分享。

image.png 官网 g6.antv.antgroup.com/manual/intr…

安装&使用

项目用的技术栈是vue,还是先npm install 再 import,官网有线上引入的地址。后面会有基础的使用,前面先分享一下解决的案例,希望能帮到大家!

npm install --save @antv/g6
// 组件中引入
import G6 from "@antv/g6";

遇到的问题和解决分享

需求:要求边之间变虚线,并且要能运动起来,还要有目标箭头的指向,查阅官网,有边动画的效果:g6.antv.antgroup.com/examples/sc…

但是这个比较尴尬的是,如果你设置了箭头,那么这个箭头也会有动画,不太符合我们的效果图,如下:

image.png

那么我们就要考虑怎么去重新设置这个箭头,一开始想着网上可能会有类似的解决方案,然后各种找之后发现好像确实没有,ok,那就发散一下,g6提供了自定义边的效果,那我们是不是可以自定义一个箭头,然后让箭头的指向指向目标node,ok,尝试一下。

这是官网提供的虚线动画的效果:

  //这是官网提供的虚线动画的效果
  const lineDash = [12, 24]; // 12 为实线段长度,24 为虚线段长度
  G6.registerEdge(
    "line-dash",
    {
      afterDraw(cfg, group) {
        // get the first shape in the group, it is the edge's path here
        const shape = group.get("children")[0];
        let index = 0;
        // Define the animation
        shape.animate(
          () => {
            index++;
            const res = {
              lineDash,
              lineDashOffset: -index * 1, // 根据 index 动态调整 lineDashOffset
            };
            // returns the modified configurations here, lineDash and lineDashOffset here
            return res;
          },
          {
            repeat: true, // whether executes the animation repeatly
            duration: 3000, // the duration for executing once
          }
        );
      },
    },
    "line" // extend the built-in edge 'line'
  );

我们自己去绘制一个箭头,然后画一个三角形,指向我们的目标节点:

function drawArrow(edge) {
  const group = edge.getContainer(); // 获取边的容器组,用于添加或删除图形元素
 
  const keyShape = edge.getKeyShape(); // 获取边的关键图形,即边的路径。
  const endPoint = keyShape.getPoint(1); // 获取边的终点坐标。
  const startPoint = keyShape.getPoint(0); // 获取边的起点坐标。
  const isWarnEdge = edge.getModel().isSpecialEdge; // 检查边是否为特殊边(例如告警边)。
  
  const angle = Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x); // 计算边的角度,用于确定箭头方向。
  
  const arrowLength = 10; // 箭头的长度。
  const arrowWidth = 6; // 箭头的宽度。

  // 定义箭头的路径,形成一个三角形。
  const arrowPath = [
    ["M", endPoint.x, endPoint.y], // 移动到箭头的起点(边的终点)。
    [
      "L",
      endPoint.x - arrowLength * Math.cos(angle) + arrowWidth * Math.sin(angle),
      endPoint.y - arrowLength * Math.sin(angle) - arrowWidth * Math.cos(angle),
    ], // 绘制箭头的第一条边。
    [
      "L",
      endPoint.x - arrowLength * Math.cos(angle) - arrowWidth * Math.sin(angle),
      endPoint.y - arrowLength * Math.sin(angle) + arrowWidth * Math.cos(angle),
    ], // 绘制箭头的第二条边。
    ["Z"], // 关闭路径,形成一个三角形。
  ];

  // 添加箭头图形到边的容器组中。
  group.addShape("path", {
    attrs: {
      path: arrowPath, // 设置箭头的路径。
      stroke: isWarnEdge ? "rgb(247, 59, 10)" : "#a0a0a0", // 设置箭头的边框颜色,根据是否为特殊边来决定颜色。
      fill: isWarnEdge ? "rgb(247, 59, 10)" : "#a0a0a0", // 设置箭头的填充颜色,根据是否为特殊边来决定颜色。
    },
    name: "arrow", // 设置图形的名称为 "arrow"。
  });
}

上面我们画了自定义箭头,使其可以正确的指向目标节点,那让其什么时候渲染呢,那就是等我们init初始化完成之后,再将我们的箭头给画上去,g6提供了对应的方法(afterrender),这里加个loading,因为两者的渲染时间是不一样的,等整个完整的图全部画出来之后,再展示出来更合理

  // 等图渲染完成之后,展示动画和画三角形,最后将图展示出来
  graph.on("afterrender", () => {
    const edges = graph.getEdges();
    edges.forEach((edge) => {
      // drawArrow(edge);
    });
    loading.value = false;
  });

OK,遇到了下一个问题,让画出来的箭头,要跟随着我们拖动拓扑图上的节点移动而移动,看看官网,有对node节点拖动的监听函数,那就好办了,对拖动的节点,找到其对应的边,重新绘制箭头就好了,查到了两个函数,怎样去选择使用,根据自己的需求来定就好了,我选了drag,感觉这样更符合视觉效果一些

// 拖动过程中持续触发,频率高。会跟随拖动逐步改变
graph.on("node:drag", (evt) => {
    const node = evt.item;
    const edges = graph.getEdges();
    edges.forEach((edge) => {
      const model = edge.getModel();
      if (model.source === node.getID() || model.target === node.getID()) {
        drawArrow(edge);
      }
    });
  });
  
// 拖动结束时触发,仅触发一次。只会在拖动结束后更新一次
// graph.on("node:dragend", () => {
//   graph.getEdges().forEach((edge) => {
//     drawArrow(edge);
//   }

这样其实就可以实现那个需求了,一些解决这个需求的思路,我也是第一次接触g6,所以记录一下。

认识一下g6的基本数据结构

看下准备的node,edge简单数据解释,具体还有别的属性,大家有需要查阅的移步官网吧,小建议,多看几遍文档,温故知新。

const data = {
  // 点集
  nodes: [
    {
      id: 'node1', // 节点的唯一标识
      shape: "image", // 节点类型是图片
      size: [30, 30], // 图片节点的尺寸
      label: "名称", // 图片节点的描述
      style: {...} // 图片节点的样式
      x: 100, // 节点位置的 x 值
      y: 200, // 可选,节点位置的 y 值
    },
    {
      id: 'node2', // 节点的唯一标识
      shape: "image", // 节点类型是图片 
      size: [30, 30], // 图片节点的尺寸 
      label: "名称", // 图片节点的描述 
      style: {...} // 图片节点的样式
      x: 300, // 节点位置的 x 值
      y: 200, // 节点位置的 y 值
    },
  ],
  // 边集
  edges: [
    {
      source: 'node1', // 起始点 id
      target: 'node2', // 目标点 id
      label: 'test' // 边上的文案
      labelStyle: { // 边的样式
          cursor: "pointer",
       },
      style: {
          stroke: "red", // 填充边的颜色
      },
    },
  ],
};

然后就是初始化整个图:

const graph = new G6.Graph({
    container: container.value, // ref图的实例
    width: width, 
    height: height,
    plugins: [toolbar], // 内部提供的toobar,就是放大镜,放大类似功能的插件,可以自己配置,
    toolbar: { // 对 toolbar的数据进行自定义配置/改写
      menus: {
        undo: false, // 隐藏撤销按钮
        redo: false, // 隐藏重做按钮
        // zoomOut: null, //
        // zoomIn: null, //
        // 其他工具保持默认配置或自定义配置
      },
    },
    layout: {
      type: "force", // 布局类型,
      preventOverlap: true, //防止点重叠
      // 防碰撞必须设置nodeSize或size,否则不生效,由于节点的size设置了40,虽然节点不碰撞了,但是节点之间的距离很近,label几乎都挤在一起,所以又重新设置了大一点的nodeSize,这样效果会好很多
      nodeSize: 50,
      linkDistance: 220,
      // nodeSpacing: 10, // 节点间距
    },
    // 默认节点的属性合集
    defaultNode: {
      type: "image",
    },
    // 默认边的属性合集
    defaultEdge: {
      type: "line-dash",
      style: {
        lineWidth: 2, // 线宽
        stroke: "#a0a0a0", // 填充色
        cursor: "pointer",
        lineDash: [24, 24], // 虚线实线 线段比
        // endArrow: true, // 末尾添加箭头
        // endArrow: {
        //   // d--> distance 离终点的距离
        //   path: G6.Arrow.triangle(10, 10, 15), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
        //   d: 15,
        // },
        // startArrow: {
        //   path: G6.Arrow.vee(0, 0, 15), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
        //   d: 15,
        // },
      },
      labelCfg: {
        // autoRotate: true, // 边上的标签文本根据边的方向旋转
        style: {
          cursor: "pointer",
        },
      },
    },
    modes: {
      default: ["drag-canvas", "drag-node"], // 允许拖拽画布、放缩画布、拖拽节点
    },
    // fitCenter: true, //是否居中展示
    // fitView: true, //是否自适应屏幕大小
  });
  
  // 填充数据
  graph.data(data);
  // 渲染
  graph.render();

下面是一些自定义节点/事件/插件

G6.registerNode 自定义节点,
G6.registerEdge 自定义边
const tooltip = new G6.Tooltip 可以自定义tooltip
const toolbar = new G6.ToolBar 可以自定义toolbar
.....

节点事件汇总,如下
-   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 用户在节点上右击鼠标时触发并打开右键菜单
参考博主的地址:https://juejin.cn/post/7063248501441822728?searchId=20240625193716533EA8A403A294DFA767 

边事件其实也差不多类似,就不详细赘述了,有需求的移步就行。

总结

其实对于提供的布局类型,感觉也要花费一些时间,因为有些布局并不能很好地对大家所需要的关系图进行展示。

从初步认识g6到实现效果,难点主要集中在自定义边和点上,没有很详细对官方文档的东西做注释,主要分享一个案例给大家,希望大家看完能有帮助吧!