自定义复杂AntV/G6案例

一、效果图

二、源码

html 复制代码
/**
*
* Author: me
* CreatDate: 2024-08-22
*
* Description: 复杂G6案例
*
*/
<template>
  <div class="moreG6-wapper">
    <div id="graphContainer" ref="graphRef" class="graph-content"></div>
  </div>
</template>

<script>
import G6 from "@antv/g6";
export default {
  name: "moreG6",
  components: {},
  props: {},
  data() {
    return {
      graph: null, // G6 实例
      graphData: {
        nodes: [],
        edges: [],
      },
      dataList: [
        {
          name: "第一阶段",
          aimData: [
            {
              title: "张三",
              aim: "战胜李四",
              type: 1,
            },
            {
              title: "李四",
              aim: "战胜张三",
              type: 2,
            },
          ],
          eventData: [
            {
              time: "1948年11月6日",
              isLeft: true, // 在左边还是在右边
              dataList: {
                title: "发现王五正在收缩",
                type: 1,
                domain: "综合",
                describe: "发现王五正在收缩,当即转入追击",
                children: [
                  {
                    title: "张三攻占a区,没发现李四",
                    type: 1,
                    domain: "战斗",
                    describe: "",
                  },
                  {
                    title: "李四调动李白",
                    type: 1,
                    domain: "战斗",
                    describe: "",
                  },
                ],
              },
            },
            {
              time: "1948年11月7日",
              isLeft: false,
              dataList: {
                title: "张三团守a州,同时向b州东进",
                type: 2,
                domain: "综合",
                describe: "张三团守a州,令小红、小兰两人回b州东进",
                children: [
                  {
                    title: "李四在b官邸",
                    type: 2,
                    domain: "战斗",
                    describe: "",
                  },
                ],
              },
            },
          ],
        },
        {
          name: "第二阶段",
          aimData: [
            {
              title: "小红",
              aim: "战胜王五",
              type: 1,
            },
            {
              title: "小兰",
              aim: "战胜王五",
              type: 1,
            },
            {
              title: "王五",
              aim: "战胜张三",
              type: 2,
            },
          ],
          eventData: [
            {
              time: "1948年11月6日",
              isLeft: true,
              dataList: {
                title: "发现王五正在收缩",
                type: 1,
                domain: "综合",
                describe: "发现王五正在收缩,当即转入追击",
                children: [
                  {
                    title: "张三攻占a区,没发现李四",
                    type: 1,
                    domain: "战斗",
                    describe: "",
                  },
                  {
                    title: "李四调动李白",
                    type: 1,
                    domain: "战斗",
                    describe: "",
                  },
                  {
                    title: "李四造谣张三",
                    type: 1,
                    domain: "舆论",
                    describe: "",
                  },
                ],
              },
            },
            {
              time: "1948年11月7日",
              isLeft: false,
              dataList: {
                title: "张三团守a州",
                type: 2,
                domain: "综合",
                describe: "张三团守a州,令小红、小兰两人回徐州东进",
                children: [
                  {
                    title: "李四在b官邸",
                    type: 2,
                    domain: "前进",
                    describe: "",
                  },
                  {
                    title: "小兰垄断李四的口粮",
                    type: 2,
                    domain: "商战",
                    describe: "",
                  },
                ],
              },
            },
          ],
        },
        {
          name: "第三阶段",
        },
        {
          name: "第四阶段",
        },
      ],
      bgColor: "#0B1E4C",
      stageSize: [110, 40],
      aimSize: [150, 30],
      eventSize: [176, 46],
      eventContentSize: [182, 90],
      eventDataSize: [120, 40],
      stageSpace: 100,
      openSpace: 40,
      aimSpace: 150,
      eventSpace: 220,
      domainW: 28,
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.initData();
      this.initGraph();
      this.drawGraph();
    });
  },
  methods: {
    /**
     * 根据数据生成节点和边数据
     */
    initData() {
      const that = this;
      const dom = this.$refs.graphRef;
      const centerX = dom.offsetWidth / 2;
      this.dataList.forEach((stage, index) => {
        const pl = ((stage.partieList?.length || 0) / 2) * that.aimSize[1];
        let eventLen = 0;
        if (index === 0) {
          stage.y = 40 + pl;
        } else {
          eventLen = this.dataList[index - 1].eventData?.length || 0;
          stage.y =
            this.dataList[index - 1].y +
            eventLen * that.eventSpace +
            that.stageSpace * 2 +
            pl;
        }
        // 1.阶段总线
        const stageNde = {
          id: `stageNode${index}`, // 元素的 id
          label: stage.name, // 标签文字
          x: centerX,
          y: stage.y,
          type: "rect", // 元素的图形
          size: that.stageSize, // 元素的大小
          // 标签配置属性
          labelCfg: {
            positions: "center", // 标签在元素中的位置
            // 包裹标签样式属性的字段 style 与标签其他属性在数据结构上并行
            style: {
              fill: "#fff", // 填充色,这里是文字颜色
              fontSize: 16,
              // fontWeight: "bold",
              fontFamily: "黑体",
            },
          },
          // 包裹样式属性的字段 style 与其他属性在数据结构上并行
          style: {
            lineWidth: 2,
            fill: "#0F3664", // 元素的填充色
            stroke: "#fff", // 元素的描边色
            radius: 20, // 圆角
          },
          // 锚点
          anchorPoints: [
            [1, 0.5], // 右中
            [0.5, 1], // 下中
          ],
        };
        this.graphData.nodes.push(stageNde);
        if (index !== 0) {
          const stageLine = {
            id: `stageLine${index}`,
            source: `stageNode${index - 1}`, // 和上面的保持一致
            target: `stageNode${index}`,
            type: "line", // 边形状
            style: {
              lineWidth: 2,
              color: "#fff",
              opacity: 0.7, // 边透明度
            },
          };
          this.graphData.edges.push(stageLine);
        }
        // 2.阶段-目标数据
        if (stage.aimData && stage.aimData.length) {
          const adl = stage.aimData.length;
          // 目标数据
          stage.aimData.forEach((aim, aimIndex) => {
            // aimNodeY 先大概吧,后面再重新计算优化一下
            const aimNodeY =
              stage.y + (that.aimSize[1] + 10) * aimIndex - pl - adl * 12;
            // 目标标题
            const aimNode = {
              id: `aimNode${index}${aimIndex}`,
              label: aim.title + "目标",
              x: centerX + that.stageSize[0] + that.openSpace * 2.5,
              y: aimNodeY,
              type: "rect",
              size: that.aimSize,
              labelCfg: {
                style: {
                  textAlign: "center",
                  fill: "#fff",
                  fontSize: 14,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 0,
                fill: aim.type === 1 ? "#E16E76" : "#1685F3",
                radius: [0, 15, 15, 0],
              },
              anchorPoints: [
                [0, 0.5],
                [1, 0.5],
              ],
            };
            this.graphData.nodes.push(aimNode);
            // 目标内容
            const aimContentNode = {
              id: `aimContentNode${index}${aimIndex}`,
              label: aim.aim,
              x: centerX + that.stageSize[0] + that.openSpace * 6.5,
              y: aimNodeY,
              type: "rect",
              size: [0, 40],
              labelCfg: {
                positions: "left",
                style: {
                  textAlign: "left",
                  fill: "#fff",
                  fontSize: 14,
                  opacity: 0.7,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 0,
                fill: that.bgColor,
              },
              anchorPoints: [[0, 0.5]],
            };
            this.graphData.nodes.push(aimContentNode);
            // 连线
            const aimLine = {
              id: `aimLine${index}${aimIndex}`,
              source: `stageNode${index}`,
              target: `aimNode${index}${aimIndex}`,
              type: "cubic-horizontal", // 边形状
              style: {
                lineWidth: 1,
                fill: "transparent", // 设置透明背景色
                opacity: 0.7,
              },
            };
            const aimContentLine = {
              id: `aimContentLine${index}${aimIndex}`,
              source: `aimNode${index}${aimIndex}`,
              target: `aimContentNode${index}${aimIndex}`,
              type: "line", // 边形状
              style: {
                lineWidth: 1,
                fill: "#ffffff",
                opacity: 0.7,
              },
            };
            this.graphData.edges.push(aimLine);
            this.graphData.edges.push(aimContentLine);
          });
        }
        // 3.时间事件数据
        if (stage.eventData && stage.eventData.length) {
          stage.eventData.forEach((event, eventIndex) => {
            event.x = event.isLeft
              ? -centerX + that.stageSize[0] + that.openSpace * 4
              : centerX + that.stageSize[0] + that.openSpace * 4;
            event.y =
              stage.y +
              eventIndex * that.eventSpace +
              that.aimSize[1] +
              that.openSpace * 2.5; // 先大概吧,后面再重新计算优化一下
            // 时间
            const timeNode = {
              id: `timeNode${index}${eventIndex}`,
              label: event.time,
              x: event.isLeft ? centerX - 10 : centerX + 10,
              y: event.y - 16,
              type: "rect",
              size: [0, 40],
              labelCfg: {
                positions: event.isLeft ? "right" : "left",
                style: {
                  textAlign: event.isLeft ? "right" : "left",
                  fill: "#fff",
                  fontSize: 14,
                  opacity: 0.8,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 0,
                fill: that.bgColor,
              },
            };
            this.graphData.nodes.push(timeNode);
            // 事件标题
            const eventNode = {
              id: `eventNode${index}${eventIndex}`,
              label: that.fittingString(event.dataList.title, 9),
              labelInit: event.dataList.title,
              x: event.isLeft
                ? -event.x - that.domainW + 12
                : event.x + that.domainW - 12,
              y: event.y,
              type: "rect",
              size: that.eventSize,
              labelCfg: {
                positions: event.isLeft ? "right" : "left",
                style: {
                  textAlign: "center",
                  fill: "#fff",
                  fontSize: 14,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 0,
                fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
                radius: event.isLeft ? [24, 0, 0, 24] : [0, 24, 24, 0],
              },
              anchorPoints: [
                [0, 0.5],
                [1, 0.5],
              ],
            };
            this.graphData.nodes.push(eventNode);
            // 领域
            const realmNode = {
              id: `realmNode${index}${eventIndex}`,
              label: [...event.dataList.domain].join("\n"),
              x: event.isLeft
                ? -event.x + that.openSpace * 2 + 6
                : event.x - that.openSpace * 2 - 6,
              y: event.y,
              type: "rect",
              size: [that.domainW, that.eventSize[1]],
              labelCfg: {
                positions: "center",
                style: {
                  textAlign: "center",
                  fill: "#fff",
                  fontSize: 14,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 1,
                fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
                stroke: event.dataList.type === 1 ? "#EDA9AE" : "#76B7F8", // 元素的描边色
              },
            };
            this.graphData.nodes.push(realmNode);
            // 内容
            const eventContentNode = {
              id: `eventContentNode${index}${eventIndex}`,
              label: event.dataList.describe
                ? that.divideString(event.dataList.describe, 11, 4)
                : "暂无描述",
              labelInit: event.dataList.describe,
              x: event.isLeft ? -event.x + 9 : event.x - 9,
              y: event.y + that.eventSize[1] + 22,
              type: "rect",
              size: that.eventContentSize,
              labelCfg: {
                positions: "center",
                style: {
                  textAlign: "center",
                  fill: "#fff",
                  fontSize: 14,
                  fontFamily: "黑体",
                },
              },
              style: {
                lineWidth: 1,
                fill: "#3C4B70",
                stroke: "#5C6988", // 元素的描边色
                opacity: 0.9,
              },
            };
            this.graphData.nodes.push(eventContentNode);
            // 连线
            const eventLine = {
              id: `eventLine${index}${eventIndex}`,
              source: `stageNode${index}`,
              target: `eventNode${index}${eventIndex}`,
              type: "polyline", // 边形状
              style: {
                lineWidth: 1,
                fill: "transparent",
                opacity: 0.7,
              },
            };
            this.graphData.edges.push(eventLine);
            // 4.时间事件后面的列表
            if (event.dataList.children && event.dataList.children.length) {
              event.dataList.children.forEach((eventData, eventDataIndex) => {
                const x =
                  event.x +
                  that.eventSize[0] +
                  that.openSpace * 3 +
                  eventDataIndex *
                    (that.eventDataSize[0] + that.openSpace * 1.4);
                const y = event.y + that.openSpace;
                // 标题
                const eventDataNode = {
                  id: `eventDataNode${index}${eventIndex}${eventDataIndex}`,
                  label: that.fittingString(eventData.title, 6),
                  labelInit: eventData.title,
                  x: event.isLeft
                    ? -x - that.domainW + 14
                    : x + that.domainW - 14,
                  y: y,
                  type: "rect",
                  size: that.eventDataSize,
                  labelCfg: {
                    positions: event.isLeft ? "right" : "left",
                    style: {
                      textAlign: "center",
                      fill: "#fff",
                      fontSize: 14,
                      fontFamily: "黑体",
                    },
                  },
                  style: {
                    lineWidth: 0,
                    fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
                  },
                  anchorPoints: [[0.5, 0]],
                };
                this.graphData.nodes.push(eventDataNode);
                // 领域
                const realmNode02 = {
                  id: `realmNode${index}${eventIndex}${eventDataIndex}02`,
                  label: [...eventData.domain].join("\n"),
                  x: event.isLeft ? -x + 60 : x - 60,
                  y: y,
                  type: "rect",
                  size: [that.domainW, that.eventDataSize[1]],
                  labelCfg: {
                    positions: "center",
                    style: {
                      textAlign: "center",
                      fill: "#fff",
                      fontSize: 14,
                      fontFamily: "黑体",
                    },
                  },
                  style: {
                    lineWidth: 1,
                    fill: event.dataList.type === 1 ? "#E16E76" : "#1685F3",
                    stroke: event.dataList.type === 1 ? "#EDA9AE" : "#76B7F8", // 元素的描边色
                  },
                };
                this.graphData.nodes.push(realmNode02);
                // 内容
                const eventContentNode02 = {
                  id: `eventContentNode${index}${eventIndex}${eventDataIndex}02`,
                  label: eventData.describe
                    ? that.divideString(eventData.describe, 9, 3)
                    : "暂无描述",
                  labelInit: eventData.describe,
                  x: event.isLeft ? -x : x,
                  y: y + that.eventDataSize[1] + 18,
                  type: "rect",
                  size: [
                    that.eventDataSize[0] + that.domainW,
                    that.eventContentSize[1] * 0.85,
                  ],
                  labelCfg: {
                    positions: "center",
                    style: {
                      textAlign: "center",
                      fill: "#fff",
                      fontSize: 14,
                      fontFamily: "黑体",
                    },
                  },
                  style: {
                    lineWidth: 1,
                    fill: "#3C4B70",
                    stroke: "#5C6988",
                    opacity: 0.9,
                  },
                };
                this.graphData.nodes.push(eventContentNode02);
                // 连线
                const eventLine = {
                  id: `eventLine${index}${eventIndex}${eventDataIndex}`,
                  source: `eventNode${index}${eventIndex}`,
                  target: `eventDataNode${index}${eventIndex}${eventDataIndex}`,
                  type: "polyline", // 边形状
                  style: {
                    lineWidth: 1,
                    fill: "transparent", // 透明色
                    opacity: 0.7,
                  },
                };
                this.graphData.edges.push(eventLine);
              });
            }
          });
        }
      });
    },
    /**
     * 初始化graph实例
     */
    initGraph() {
      // 缩略图
      const minimap = new G6.Minimap({
        size: [window.innerWidth / 6, window.innerHeight / 4],
      });
      // 提示框
      const tooltip = new G6.Tooltip({
        offsetX: 10,
        offsetY: 10,
        size: [100, 100],
        itemTypes: ["node"], // 允许出现 tooltip 的 item 类型
        // 自定义 tooltip 内容
        getContent: (e) => {
          const outDiv = document.createElement("div");
          outDiv.style.width = "fit-content";
          outDiv.innerHTML = `<div style="max-width:200px;">
             ${e.item.getModel().labelInit || e.item.getModel().label}
             </div>`;
          return outDiv;
        },
        // 哪些要设置提示框
        shouldBegin: (e) => {
          const id = e.item.getModel().id;
          const a = id.includes("eventNode");
          const b = id.includes("eventContentNode");
          const c = id.includes("eventDataNode");
          const is = a || b || c;
          return is;
        },
      });
      // 容器节点
      const dom = this.$refs.graphRef;
      // 配置参数
      const options = {
        container: "graphContainer", // 画布的容器id
        with: dom.offsetWidth, // 画布宽度
        height: dom.offsetHeight, // 画布高度
        minZoom: 0.2,
        maxZoom: 5,
        // 图交互模式
        modes: {
          default: ["drag-canvas", "zoom-canvas"], // 允许拖拽画布、缩放画布
        },
        plugins: [tooltip, minimap], // 插件
      };
      // 实例化
      this.graph = new G6.Graph(options);
      // 自适应
      this.resizeView();
      // 监听窗口大小变化
      window.addEventListener("resize", this.resizeView);
    },
    /**
     * 画图
     */
    drawGraph() {
      // 清除画布元素
      this.graph.clear();
      // 数据源
      this.graph.data(this.graphData);
      // 渲染
      this.graph.render();
    },
    /**
     * 自适应
     */
    resizeView() {
      const dom = this.$refs.graphRef;
      if (dom && this.graph) {
        this.graph.changeSize(dom.offsetWidth, dom.offsetHeight); // 修改画布大小
        // this.graph.fitCenter(); // 移至中心
      }
    },
    /**
     * 字符串等长划分-入口事件
     * @param str 字符串
     * @param len 一行的长度
     * @param row 行数
     */
    divideString(str, len, row = 3) {
      str = this.fittingString(str, len * row - 2);
      const result = this.divideHandle(str, len);
      return result.join("\n");
    },
    /**
     * 字符串等长划分
     * @param str 字符串
     * @param len 划分的长度
     */
    divideHandle(str, len) {
      if (str.length <= len) {
        return [str];
      }
      const chunk = str.substring(0, len);
      const arr = [chunk, ...this.divideHandle(str.substring(len), len)];
      return arr;
    },
    /**
     * 截取字符串,超出多少len省略
     * @param str 字符串
     * @param len 长度
     */
    fittingString(str, len) {
      if (str.length <= len) {
        return str;
      }
      const result = str.substring(0, len) + "...";
      return result;
    },
  },
  beforeDestroy() {
    this.graph && this.graph.destroy();
  },
  watch: {},
};
</script>

<style lang='scss' scoped>
.moreG6-wapper {
  height: 100%;
  background: #0b1e4c;
  .graph-content {
    width: 100%;
    height: 96%;
    ::v-deep .g6-tooltip {
      border: 1px solid #e2e2e2;
      border-radius: 4px;
      font-size: 12px;
      color: #545454;
      background-color: rgba(255, 255, 255, 0.9);
      padding: 10px 8px;
      box-shadow: rgb(174, 174, 174) 0px 0px 10px;
    }
    ::v-deep .g6-minimap {
      position: absolute;
      right: 0;
      top: 61px;
      background-color: #0b1e4c;
    }
  }
}
</style>

三、记录说明

  • 当前使用版本:"@antv/g6": "^4.8.21"

  • 官方文档节点形状为shape,但是在不记得哪个版本后, shape 改为 type,当时整了好久,形状就是不生效,原来是字段改变了,这个坑要注意

  • 初步接触就有这么个界面需求,官方文档有点乱,且不全面,新手小白上路属实有点头皮发麻,后来也琢磨出来一些:

    • 官方案例实现不了的,自己画
    • 无非就是两个要点:节点、边
    • 像这案例一样的不能使用居中api的,得自己计算节点的位置
    • 实现不了的不影响使用的小功能,就干掉它,不要了...
  • 期间遇到还没解决的问题:

    • 使用image内置节点,自己的图片显示有问题
    • polyline样式的连线拐点问题,本来定在边角的锚点,但是拐点太多了不齐整,后来就直接锚点在中间了...
    • 本来有个节点收缩/展开的功能,后来没做了,这个后面再研究研究...
相关推荐
heiyay1 年前
antv/g6之交互模式mode
前端·交互·antv/g6·图可视化
heiyay1 年前
antv/g6 交互与事件及自定义Behavior
前端·数据库·交互·antv/g6