G6 自定义边(Custom Edge)开发指南:自定义路径、额外标签与自定义箭头全解

原创2026-09-23 15:05:121,891 阅读
文章标签:数据可视化前端图表库

G6 自定义边(Custom Edge)开发指南:自定义路径、额外标签与自定义箭头全解

导读

在 G6 图可视化框架中,边的形态并非一成不变:除了内置的 line、polyline、cubic、quadratic 等边类型,开发者经常需要实现业务专属的连接线——例如折线式路径、带起止点额外文案的标签边、或完全自定义形状的箭头。本指南以仓库中 packages/site/examples/element/custom-edge 示例页的三个 Demo(自定义路径、额外标签、自定义箭头)为主线,结合 G6 边元素源码,系统讲解自定义边的两条技术路线:继承边基类重写绘制逻辑,以及通过配置直接定制箭头。读完本文,你将掌握 BaseEdge 的 getKeyPath / render 扩展点、扩展注册机制 register、子样式提取工具 subStyleProps 以及箭头样式属性的完整用法,能够独立实现贴合业务场景的边元素。

自定义边概述:三种典型场景

G6 的边扩展能力覆盖从“几何路径”到“视觉装饰”的多个层次。示例页 custom-edge 目录 下的三个 Demo 恰好对应三个递进的定制粒度:

Demo 文件 定制内容 实现方式
custom-path.js 自定义折线路径 继承 BaseEdge,重写 getKeyPath
extra-label.js 在边两端追加额外标签 继承 Line,重写 render 并使用 upsert 追加图形
custom-arrow.js 自定义箭头形状、偏移与图片箭头 纯样式配置(endArrowD / endArrowSrc 等)

其中前两个 Demo 属于“代码级扩展”(注册新的边类型),第三个属于“配置级定制”(复用内置边类型)。两者相辅相成,覆盖了绝大多数自定义边需求。

前置知识:扩展注册机制

在开始写自定义边之前,需要理解 G6 的扩展注册 API。任何非内置扩展都必须先注册再使用,相关实现在 packages/g6/src/registry/register.ts:

export function register<T extends ExtensionCategory>(
  category: Loosen<T>,
  type: string,
  Ctor: ExtensionRegistry[T][string],
) {
  const ext = EXTENSION_REGISTRY[category][type];
  if (ext) {
    print.warn(`The extension ${type} of ${category} has been registered before, and will be overridden.`);
  }
  Object.assign(EXTENSION_REGISTRY[category]!, { [type]: Ctor });
}

要点说明:

  • category 声明扩展所属分类。边的分类常量是 ExtensionCategory.EDGE(定义于 packages/g6/src/constants/registry.ts),取值为 'edge';
  • type 是扩展标识,注册后在 Graph 配置的 edge.type 中直接使用;
  • Ctor 是扩展类,图实例化时按需创建实例;
  • 内置扩展在导入项目时会自动注册;自定义扩展只需注册一次,即可在整个项目中任意使用;
  • 若同名类型重复注册,会打印警告并覆盖旧扩展,因此命名时建议使用带业务前缀的类型名(如示例中的 custom-polyline、extra-label-edge)。

三个 Demo 均使用了标准注册写法,例如:

register(ExtensionCategory.EDGE, 'custom-polyline', PolylineEdge);

注册完成后,在 Graph 配置中通过 edge: { type: 'custom-polyline' } 即可启用该边类型。

路线一:继承 BaseEdge 自定义路径

custom-path.js 演示了最核心的边扩展方式:继承 BaseEdge 基类并重写 getKeyPath,将一条直线边改造成带中间折点的折线边。

完整示例代码

import { BaseEdge, ExtensionCategory, Graph, register } from '@antv/g6';

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

    return [
      ['M', sourcePoint[0], 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], targetPoint[1]],
    ];
  }
}

register(ExtensionCategory.EDGE, 'custom-polyline', PolylineEdge);

const graph = new Graph({
  container: 'container',
  data: {
    nodes: [
      { id: 'node-0', style: { x: 100, y: 100, ports: [{ key: 'right', placement: [1, 0.5] }] } },
      { id: 'node-1', style: { x: 250, y: 200, ports: [{ key: 'left', placement: [0, 0.5] }] } },
    ],
    edges: [{ source: 'node-0', target: 'node-1' }],
  },
  edge: {
    type: 'custom-polyline',
    style: {
      startArrow: true,
      endArrow: true,
      stroke: '#F6BD16',
    },
  },
  behaviors: ['drag-element'],
});

graph.render();

getKeyPath:边的几何灵魂

getKeyPath 是 BaseEdge 声明的抽象方法(见 packages/g6/src/elements/edges/base-edge.ts),子类必须实现。它接收解析后的样式属性 attributes,返回一段 SVG Path 指令数组,用于描述边的主体路径。

示例中的折线路径构造逻辑:

  • 先通过 this.getEndpoints(attributes) 获取边在起点、终点处的连接坐标(已考虑节点锚点与 port);
  • 返回的指令依次为:M 移动到起点 → 水平 L 到中点横坐标(纵坐标保持起点 y)→ 垂直 L 到终点 y(横坐标保持中点 x)→ 水平 L 到终点,形成“先横后竖再横”的折线;
  • 折点横坐标取起终点 x 的中点 targetPoint[0] / 2 + sourcePoint[0] / 2,即 50% 处,可依业务自由调整比例。

getEndpoints 与 Port 协同

getEndpoints 由基类提供(base-edge.ts),内部会通过 findPorts 查找起终点节点上配置的 port,再计算连接点。Demo 中在节点上声明了端口:

ports: [{ key: 'right', placement: [1, 0.5] }]  // 节点右侧中点
ports: [{ key: 'left', placement: [0, 0.5] }]   // 节点左侧中点

placement: [1, 0.5] 表示端口位于节点包围盒右边缘纵向 50% 处(相对坐标 0~1),[0, 0.5] 表示左边缘中点。这样边就从源节点的右侧中点连到目标节点的左侧中点,配合 getEndpoints 返回的精确连接点,折线路径不会“穿模”到节点内部。

边样式的默认行为

Demo 中显式设置了 startArrow: true 与 endArrow: true。之所以需要显式开启,是因为 BaseEdge.defaultStyleProps 中两个箭头的默认值均为 false(见 base-edge.ts)。同段默认配置还包含:

  • label: true、labelPlacement: 'center':边标签默认显示在路径中点;
  • halo: false:默认不显示光晕;
  • badge: true:徽标默认开启;
  • loop: true:自环边路径默认启用。

路线二:继承 Line 追加额外标签

extra-label.js 展示了一种更灵活的扩展方式:不重写路径,而是继承内置直线边 Line,在基类渲染管线的基础上追加自定义图形(边两端的额外标签)。

完整示例代码

import { Text } from '@antv/g';
import { Renderer } from '@antv/g-svg';
import { ExtensionCategory, Graph, Line, register, subStyleProps } from '@antv/g6';

class LabelEdge extends Line {
  render(attributes, container) {
    super.render(attributes);
    this.drawEndLabel(attributes, container, 'start');
    this.drawEndLabel(attributes, container, 'end');
  }

  drawEndLabel(attributes, container, type) {
    const key = type === 'start' ? 'startLabel' : 'endLabel';
    const [x, y] = this.getEndpoints(attributes)[type === 'start' ? 0 : 1];

    const fontStyle = {
      x,
      y,
      dx: type === 'start' ? 15 : -15,
      fontSize: 16,
      fill: 'gray',
      textBaseline: 'middle',
      textAlign: type,
    };
    const style = subStyleProps(attributes, key);
    const text = style.text;
    this.upsert(`label-${type}`, Text, text ? { ...fontStyle, ...style } : false, container);
  }
}

register(ExtensionCategory.EDGE, 'extra-label-edge', LabelEdge);

const graph = new Graph({
  container: 'container',
  renderer: () => new Renderer(),
  data: {
    nodes: [
      { id: 'node-0', style: { x: 100, y: 100 } },
      { id: 'node-1', style: { x: 300, y: 100 } },
    ],
    edges: [{ source: 'node-0', target: 'node-1' }],
  },
  edge: {
    type: 'extra-label-edge',
    style: {
      startArrow: true,
      endArrow: true,
      stroke: '#F6BD16',
      startLabelText: 'start',
      endLabelText: 'end',
    },
  },
  behaviors: ['drag-element'],
});

graph.render();

基于 Line 而非 BaseEdge 扩展

Line 是内置直线边(实现见 packages/g6/src/elements/edges/line.ts),其 getKeyPath 仅生成 M → L 两段直线指令。继承 Line 可以复用其全部路径计算与内置图形,只在 render 阶段叠加自定义内容,是最小侵入的扩展方式。所有内置边类型均从 packages/g6/src/elements/edges/index.ts 导出,可供直接继承。

render 生命周期与超集绘制

BaseEdge.render 定义了固定的绘制顺序(base-edge.ts):

  1. drawKeyShape:绘制主体路径;
  2. drawSourceArrow / drawTargetArrow:绘制起止箭头;
  3. drawLabelShape:绘制边标签;
  4. drawHaloShape:绘制光晕;
  5. drawBadgeShape:绘制徽标。

子类重写 render 时先调用 super.render(attributes) 完成上述管线,再追加自定义逻辑,即可在不破坏原有绘制的前提下扩展边元素。示例在基类渲染后,分别在边的起点和终点各绘制一个 Text 文本图形。

upsert:声明式的图形增删改

drawEndLabel 使用 this.upsert(key, Ctor, style, container) 管理子图形:

this.upsert(`label-${type}`, Text, text ? { ...fontStyle, ...style } : false, container);

upsert 是 G6 元素基类提供的声明式图形管理 API:根据 key 查找已有图形,存在则更新样式,不存在则创建,传入 false 则销毁。这意味着当边的属性更新(如拖动节点导致端点移动)时,基类会自动协调标签文本的创建、更新与销毁,无需手工维护图形引用。

subStyleProps:提取前缀子样式

示例中用 subStyleProps(attributes, key) 从完整样式对象中按前缀提取子样式,其实现位于 packages/g6/src/utils/prefix.ts:

export function subStyleProps<T extends Record<string, any>>(style: object, prefix: string) {
  // 遍历 style,收集以 prefix 开头的键,并剥离前缀
  // 同时向下传递 opacity 透明度
}

当 key = 'startLabel' 时,配置中的 startLabelText: 'start' 会被转换为 { text: 'start' },再与默认的 fontStyle 合并,实现“以 startLabelText / endLabelText 作为额外标签文案入口”的效果。这是 G6 子样式(label、halo、badge、arrow 等)统一的解耦方式:主样式与子样式共用一份扁平配置,通过前缀划分归属。

SVG 渲染器说明

示例中显式配置了 renderer: () => new Renderer()(Renderer 来自 @antv/g-svg)。G6 基于 @antv/g 图形体系,支持 Canvas / SVG 等渲染器,这里选用 SVG 渲染器表明边上的文本与图形同样依赖底层渲染能力,你可按场景自由切换。

路线三:纯配置自定义箭头

custom-arrow.js 无需编写任何扩展类,仅靠边样式配置即可实现三种风格迥异的箭头:矢量路径箭头、实心锥形箭头、图片箭头。它演示了 EdgeArrowStyleProps 的完整用法。

完整示例代码

import { Graph } from '@antv/g6';

const data = {
  nodes: new Array(6).fill(0).map((_, i) => ({ id: `node${i + 1}` })),
  edges: [
    {
      id: 'custom-arrow-1',
      source: 'node1',
      target: 'node2',
      style: {
        endArrowD: 'M-14,0 L-4,-4 L0,-14 L4,-4 L14,0 L4,4 L0,14 L-4,4 Z',
        endArrowOffset: 14,
      },
    },
    {
      id: 'custom-arrow-2',
      source: 'node3',
      target: 'node4',
      style: {
        endArrowD: 'M 3,-5 L 3,5 L 15,10 L 15,-10 Z',
        endArrowOffset: 10,
      },
    },
    {
      id: 'image-arrow',
      source: 'node5',
      target: 'node6',
      style: {
        endArrowSrc: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',
        endArrowSize: 28,
        endArrowTransform: [['rotate', 90]],
      },
    },
  ],
};

const graph = new Graph({
  data,
  edge: {
    style: {
      stroke: '#F6BD16',
      labelText: (d) => d.id,
      labelBackground: true,
      endArrow: true,
    },
  },
  layout: { type: 'grid', cols: 2 },
});

graph.render();

箭头样式属性的取值体系

箭头子样式接口 EdgeArrowStyleProps 定义于 packages/g6/src/types/edge.ts,除继承 PathStyleProps 与 ImageStyleProps 外,核心字段如下:

属性 说明 默认值
endArrow / startArrow 是否显示箭头(基类默认 false) false
endArrowSize / startArrowSize 箭头大小,支持数值或 [width, height] 8
endArrowD / startArrowD 箭头路径的 d 指令,自定义矢量形状 内置形状
endArrowOffset / startArrowOffset 箭头相对边端点的偏移量,避免箭头被节点遮挡 由线宽与大小推导
endArrowSrc / startArrowSrc 图片 URL,以图片作为箭头 无
endArrowTransform / startArrowTransform 对箭头施加的变换,如 [['rotate', 90]] 无
endArrowType / startArrowType 内置箭头形状 'vee'(基类默认)

type 字段支持的内置形状(见 edge.ts)包括:triangle、circle、diamond、vee、rect、triangleRect、simple,也支持传入 (width, height) => PathArray 函数动态生成路径。

自定义 d 路径与偏移量

endArrowD 直接提供 SVG 路径指令即可替换默认箭头形状:

  • 第一个箭头(custom-arrow-1)的 d 描述了八芒星/花型轮廓,endArrowOffset: 14 让箭头向边内侧回退 14 像素,保证箭头根部不被节点覆盖;
  • 第二个箭头(custom-arrow-2)的 d 是梯形锥体,配合 endArrowOffset: 10 使用。

从源码看,箭头最终作为 markerStart / markerEnd 标记挂在主路径上(base-edge.ts):当提供 src 时使用 Image 图形,否则使用 Path 图形;偏移量默认取 width / 2 + lineWidth,显式设置的 endArrowOffset 会覆盖该推导值。

图片箭头与变换

第三个箭头通过 endArrowSrc 引入外部图片作为箭头素材,并用 endArrowSize: 28 控制尺寸、endArrowTransform: [['rotate', 90]] 将图片旋转 90°。endArrowTransform 的写法与 @antv/g 的变换语法一致,可组合多个变换(如先缩放再旋转)。

配套的边级样式

Demo 同时展示了边标签的便捷配置:

  • labelText: (d) => d.id:标签文本支持回调函数,按边数据动态生成(此处显示边的 id);
  • labelBackground: true:为标签添加背景色块,提升可读性;
  • 图实例未配置 container,并使用了 layout: { type: 'grid', cols: 2 } 网格布局,六条边在双列网格上展示,便于观察三种箭头的效果差异。

源码视角:BaseEdge 的默认配置与箭头绘制细节

基类默认样式速查

BaseEdge.defaultStyleProps(base-edge.ts)是理解边元素行为的关键,常用默认值如下:

label: true,              // 显示标签
labelPlacement: 'center', // 标签位于路径中心
labelOffsetX: 4,          // 标签横向偏移
labelAutoRotate: true,    // 标签随路径自动旋转
halo: false,              // 不显示光晕
badge: true,              // 显示徽标
loop: true,               // 自环边开启
startArrow: false,        // 起点箭头默认关闭
endArrow: false,          // 终点箭头默认关闭
startArrowType: 'vee',    // 起点箭头默认 vee 形
endArrowType: 'vee',      // 终点箭头默认 vee 形
loopPlacement: 'top',     // 自环默认位于节点上方
loopClockwise: true,      // 自环默认顺时针

箭头绘制链路

箭头绘制的核心逻辑在 drawArrow(base-edge.ts):当 startArrow / endArrow 为真时,依据 src 是否存在选择 Image 或 Path 构造箭头图形并挂载为路径的 markerStart / markerEnd;当开关关闭时销毁已有箭头。getArrowStyle 则负责将 startArrowXxx / endArrowXxx 前缀样式归一为箭头图形样式,其中 type 为函数时直接调用函数生成路径,否则从内置符号表取形状,兜底为三角形(Symbol.triangle)。

渲染管线可复用

自定义边最常见的两种姿势可以概括为:

  1. 改路径:继承 BaseEdge 或内置边类型,重写 getKeyPath,其余(箭头、标签、光晕、徽标、动画帧更新)全部由基类管线接管;
  2. 加内容:重写 render,先 super.render() 再 upsert 追加自定义图形,充分利用基类的属性响应与更新机制。

小结

本文以 custom-edge 示例页 的三个 Demo 为骨架,覆盖了 G6 自定义边的完整能力矩阵:

  • 自定义路径:继承 BaseEdge 重写 getKeyPath,配合 getEndpoints 与 Port 锚点实现任意几何路径;
  • 额外标签:继承 Line 扩展 render 生命周期,借助 upsert 与 subStyleProps 追加与管理自定义文本图形;
  • 自定义箭头:通过 endArrowD / endArrowOffset / endArrowSrc / endArrowSize / endArrowTransform 等样式属性,零代码实现矢量、图片等多种箭头形态。

三者共同依赖 G6 的扩展注册机制(register + ExtensionCategory.EDGE)与 BaseEdge 基类渲染管线,相关源码可在 packages/g6/src/elements/edges 目录下进一步研读。掌握了这些扩展点,你就能在真实业务中按需定制出符合场景语义的边元素。

登录后查看全文
G6