G6 自定义边(Custom Edge)开发指南:自定义路径、额外标签与自定义箭头全解
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):
drawKeyShape:绘制主体路径;drawSourceArrow/drawTargetArrow:绘制起止箭头;drawLabelShape:绘制边标签;drawHaloShape:绘制光晕;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)。
渲染管线可复用
自定义边最常见的两种姿势可以概括为:
- 改路径:继承
BaseEdge或内置边类型,重写getKeyPath,其余(箭头、标签、光晕、徽标、动画帧更新)全部由基类管线接管; - 加内容:重写
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 目录下进一步研读。掌握了这些扩展点,你就能在真实业务中按需定制出符合场景语义的边元素。