AntV/G6: 实现产业图谱

一. 前言

一直使用echarts做产业图谱, 奈何它依然不够完美: 1. 节点的自定义样式不够灵活;2. 节点的激活状态不够多样化;3.节点的多重组合类型缺失; 导致产业链图谱的多重节点和节点的多重组合效果很难实现;故而转战阿里的AntV/G6技术栈

二.先看最终实现效果

三.Antv/G6

我用的是最新的 v5.0.47版本, 这真是个小坑, 搜边全网没有几个v5版本的案例, 普遍都是v4版本, v4升级到v5变更又特别多,只能先把文档从头到尾熟读几遍, 这里只说重点:节点Node, 边Edge, 组合Combo, 状态State, 交互 Behavior, 这几个核心模块文档至少看三遍

四.实现思路

  1. 如何把一二三级的所有节点平均分布在画布上?

①把画布分成三等份; ②每一级产业链平均分布在各自等份中并且居中; 代码如下:

js 复制代码
// 设置node坐标点
function setNodeXY(arr, elementId) {
  arr.forEach((item, index) => {
    const nodeHeight = 50  - 10 * (item.level-1);
    const totalHeight = arr.length * nodeHeight;
    let x = 200 + ((elementId.offsetWidth / 3) * 2)*(item.level-1);
    let y = (elementId.offsetHeight - totalHeight) / 2; 
    item.xy = [x, y + index * nodeHeight];
  });
}
  1. 如何把每一级的节点进行分组,方便同组区分?

①把树形结构数据按照层级拆成三级, level01List, level02List,level03List (G6的nodes: [...level01List,......level02List,......level03List])

②把每一级的数据根据parentId相同的进行分组, (G6的combos: [])

js 复制代码
// 设置node的组合combos
function setCombos(arr) {
  let result = Object.values(
    arr.reduce((acc, item) => {
      const key = item.parentId;
      if (!acc[key]) {
        acc[key] = {
          id: `level${item.level}-${key}`,
          data: { label: item.name },
        };
      }
      return acc;
    }, {})
  );
  return result;
}
  1. 现在有了节点(node), 有了组合(combo), 怎么把两者连接起来, 需要连线边(edge) ?

①首先要知道combo与combo之间是没有edge的, 只有node之间有

②如果所有节点都用边相连,会导致一个父级有四条边连向子级, 我想要相同的子级只连一条边到父级

③把边的数据处理成看着像组合与组合相连的样子,如上效果图中,本来有18条连线边, 处理成只有4条连线边 (G6的edges: [])

js 复制代码
// 设置node的连线边edges
function setEdges(startArr, endArr) {
  startArr.forEach((startItem, startIndex) => {
    let tempArr = [];
    endArr.forEach((endItem, endIndex) => {
      if (startItem.id === endItem.parentId) {
        tempArr.push(endObj);
      }
    });
    if (tempArr && tempArr.length) {
      let tempEdge = {
        id: `${startItem.id}-${tempArr[Math.floor(tempArr.length / 2)].id}`,
        source: startObj.id,
        target:
          tempArr &&
          tempArr.length &&
          tempArr[Math.floor(tempArr.length / 2)].id
      };
      edgesList.value.push(tempEdge);
    }
  });
}

五.交互效果: 鼠标移入节点时, 高亮当前节点, 高亮边, 高亮子级组合

  1. G6的交互效果需要配置behaviors, 只有移入node节点时才需要交互, 所以enable参数处理了edge和combo的移入,代码如下:
js 复制代码
    behaviors: [
      {
        type: "hover-activate",
        key: "hover-activate-1",
        enable: (e) => {
          if (e.targetType === "node") {
            return true;
          }
          return false;
        },
      }
    ]
  1. node节点的默认配置和激活高亮配置, 代码如下:
js 复制代码
    node: {
      type: "html",
      style: {
        innerHTML: (d) => {
          return `
            <div class="g6-node-html">
              <span>${d.data.name}</span>
            </div>
          `;
        },
        zIndex: 2,
      },
      state: {
        active: {
          innerHTML: (d) => {
            return `
                <div class="g6-node-html-active">
                  <span>${d.data.name}</span>
                </div>
              `;
          },
        }
      }
    }
  1. edge边的默认配置和激活高亮配置, 代码如下:
js 复制代码
    edge: {
      type: "polyline",
      style: {
        stroke: "rgba(46, 53, 66, 1)",
        controlPoints: (d) => [
          [d.data.start.xy[0] , d.data.start.xy[1] + 20], // 起
          [
            d.data.start.xy[0] + d.data.middle * 20 + 100,
            d.data.start.xy[1] + 20,
          ], // 拐
          [
            d.data.start.xy[0] + d.data.middle * 20 + 100,
            d.data.end.xy[1],
          ], // 拐
          [d.data.end.xy[0], d.data.end.xy[1]], // 终
        ],

        radius: 10,
        lineJoin: "round",
        lineWidth: 10,
        zIndex: 1,
      },
      state: {
        active: {
          stroke: (d) => {
            return "linear-gradient(0deg, #6fd4df00 0%, #7DBDF2 50%,#7DBDF2 100%)";
          },
          lineWidth: 8,
        },
      }
    }
  1. combo组合的默认配置和激活高亮配置,代码如下:
js 复制代码
    combo: {
      type: "rect",
      style: {
        padding: 10,
        radius: 6,
        fill: "linear-gradient(90deg,rgba(75, 75, 103, 0.3) 0%, rgba(75, 75, 103, 0.08) 100%)",
        fillOpacity: 1,
        stroke: "rgba(255, 255, 255, 0.1)",
        zIndex: 3,
      },
      state: {
        active: {
          stroke: "rgba(184, 243, 255, 1)",
        },
      }
    }

六. 最最最麻烦的交互: 鼠标移入节点时, 连线的边有个小球在运动

怎么说呢, 文档没有详细api, 没有具体案例, 网上也没有教程, 怎么办? 又把文档啃了几遍,发现有个Shape图形,shape有几个模棱两可的说明, 尝试了几十次, 具体思路已经完全靠猜靠经验靠尝试, 实现了, 代码如下:

js 复制代码
// 自定义边 路径 运动小球
let runMarkerEdge = null;
class RunMarkerEdge extends Polyline {
  getMarkerStyle(attributes) {
    if (attributes.stroke.includes("linear-gradient")) {
      return {
        ...subStyleProps(attributes, "marker"),
        size: 6,
        fill: "#fff",
        visibility: "visible",
        offsetPath: this.shapeMap.key,
      };
    } else {
      return {
        ...subStyleProps(attributes, "marker"),
        size: 6,
        fill: "#7DBDF2",
        visibility: "hidden",
        offsetPath: this.shapeMap.key,
      };
    }
  }

  getKeyPath(attributes) {
    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);

    return [
      ["M", sourcePoint[0] + 15, sourcePoint[1]],
      ["L", targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],
      ["L", targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],
      ["L", targetPoint[0] - 15, targetPoint[1]],
    ];
  }

  onCreate() {
    runMarkerEdge = this.upsert(
      "marker",
      Circle,
      this.getMarkerStyle(this.attributes),
      this
    );
    runMarkerEdge.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
      duration: 2000,
      iterations: Infinity,
    });
  }
  onUpdate() {
    runMarkerEdge = this.upsert(
      "marker",
      Circle,
      this.getMarkerStyle(this.attributes),
      this
    );
    runMarkerEdge.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
      duration: 2000,
      iterations: Infinity,
    });
  }
}

register(ExtensionCategory.EDGE, "run-mark-edge", RunMarkerEdge);

七. 束语

作为一个有自尊心的开发。 当产品说,这个交互别人能实现,你能实现一下吗? 本能是拒绝的, 但不能说实现不了, 头发掉光也要实现它。

相关推荐
teeeeeeemo3 分钟前
CSS place-items: center; 详解与用法
前端·css·笔记
未来之窗软件服务7 分钟前
html读取身份证【成都鱼住未来身份证】:CyberWinApp-SAAS 本地化及未来之窗行业应用跨平台架构
前端·html·身份证读取
木木jio12 分钟前
🧹 前端日志查询组件的重构实践:从 1600 行巨型组件到模块化 hooks
前端·react.js
WAKEUP36930 分钟前
TypeScript 类型系统简述:构建更健壮的代码基础
前端·typescript
難釋懷33 分钟前
Vue-github 用户搜索案例
前端·vue.js
red润35 分钟前
被转义字符麻痹的一天:理解转义字符串
前端·javascript·正则表达式
ladymorgana37 分钟前
【 FastJSON 】解析多层嵌套
java·前端·fastjson
晚风30840 分钟前
组件传参方式
前端·vue.js