- 数据可视化
- 前端
- 图表库
【免费下载链接】G6
♾ A Graph Visualization Framework in JavaScript.
自定义 Combo 是 G6(Graph Visualization Framework in JavaScript)中把业务分组语义(如部门、集群、子图)落地为可视化形态的关键手段。本文以 custom-combo.en.md 为骨架,完整讲解 Combo 的基本组成、两种自定义路线、三步创建第一个自定义 Combo 的完整代码,并深入 G6 源码剖析upsert、render、getShape、生命周期钩子与状态响应等底层原理,帮助读者从"能跑通示例"进阶到"能按业务自由定制"。
Combo 的基本组成与独有特性
在动手之前,先理解 G6 中一个完整 Combo 通常由哪些图形部件构成。文档给出的三部分如下:
key:Combo 的主体图形,决定其轮廓形状,例如圆形、矩形,或本文示例中的六边形;label:文本标签,通常用于展示 Combo 的名称或描述;halo:围绕主体图形绘制的光晕图形,用于高亮、选中反馈等场景。
除这三部分外,BaseCombo还支持collapsedMarker(收起标记)与icon、badge、port等通用图形插槽。从 base-combo.ts 的BaseComboStyleProps可以看到,Combo 样式还包含size(展开默认大小)、collapsedSize(收起默认大小,默认 32)、padding(内边距,仅展开时生效)、childrenNode与childrenData等专属属性。
Combo 与普通节点不同,具有以下四类特性:
- 包含性(Containment):Combo 可以包含节点和其他 Combo,形成层级结构;
- 双状态(Two States):具有展开(Expanded)与收起(Collapsed)两种状态;
- 自适应尺寸(Adaptive Size):会根据内部元素自动计算合适的大小;
- 拖拽行为(Drag Behavior):支持整体拖拽,也支持把元素拖入/拖出 Combo。
这些特性在 base-combo.ts 中有直接体现:getKeySize会依据childrenNode.length是否为零以及collapsed状态,分别走getEmptyKeySize、getCollapsedKeySize或getExpandedKeySize,其中展开尺寸由getContentBBox(子元素包围盒 + padding)计算而来。
两种自定义路线:继承优先,从零起步兜底
文档明确指出,创建自定义 Combo 有两条路线,且需要根据场景"选对路"。
路线一:继承已有 Combo 类型(推荐)
这是最常见的做法,可选择继承以下类型之一:
BaseCombo(源码:base-combo.ts):最基础的 Combo 类,提供 Combo 的核心能力,例如自适应尺寸、收起/展开、位置同步;Circle(源码:circle.ts):圆形 Combo,其getExpandedKeySize使用Math.sqrt(width² + height²) / 2让圆始终外接子元素包围盒;Rect(源码:rect.ts):矩形 Combo,主图形为GRect,展开尺寸直接等于子元素包围盒加 padding。
选择继承路线的好处:代码量少(复用已有属性与方法,只聚焦新特性)、开发快(适合绝大多数项目需求)、易维护(继承关系清晰、代码结构明确)。
提示:如果你选择继承路线(绝大多数用户都会这么做),可以直接跳到下文"三步创建第一个自定义 Combo"章节开始动手实践。
路线二:基于 G 图形系统从零开发(高级用法)
当内置类型与继承都无法满足需求时,可以基于 G6 底层的 [G Graphics System] 从零绘制 Combo。这条路线带来最大自由度(完全掌控每个细节)、满足特殊需求(内置类型无法覆盖的高定制场景)、便于定向性能优化。
警告:从零开发需要自行处理图形绘制、事件响应、状态切换、展开/收起逻辑等全部细节,挑战较大。可直接参考 base-combo.ts 的源码实现作为起点。
三步创建第一个自定义 Combo:六边形 Combo 实战
下面以继承BaseCombo实现一个自定义六边形 Combo 为例,走完完整的三个步骤。文档同时提供了可直接运行的完整示例(包含Graph实例化与数据装配),以及分步拆解的 TypeScript 实现,这里先给出分步核心代码。
Step 1:编写自定义 Combo 类
首先定义收起/展开按钮的 SVG 路径生成函数,然后实现HexagonCombo类:
import { BaseCombo } from '@antv/g6'; import type { BaseComboStyleProps } from '@antv/g6'; // 收起状态按钮路径 const collapse = (x, y, r) => { return [ ['M', x - r, y], ['a', r, r, 0, 1, 0, r * 2, 0], ['a', r, r, 0, 1, 0, -r * 2, 0], ['M', x - r + 4, y], ['L', x + r - 4, y], ]; }; // 展开状态按钮路径(比收起状态多一条竖向短线) const expand = (x, y, r) => { return [ ['M', x - r, y], ['a', r, r, 0, 1, 0, r * 2, 0], ['a', r, r, 0, 1, 0, -r * 2, 0], ['M', x - r + 4, y], ['L', x - r + 2 * r - 4, y], ['M', x - r + r, y - r + 4], ['L', x, y + r - 4], ]; }; class HexagonCombo extends BaseCombo { // 计算六边形顶点并构造 SVG path protected getKeyPath(attributes: Required<BaseComboStyleProps>) { const [width, height] = this.getKeySize(attributes); const padding = 10; const size = Math.min(width, height) + padding; const points = []; for (let i = 0; i < 6; i++) { const angle = (Math.PI / 3) * i; const x = (size / 2) * Math.cos(angle); const y = (size / 2) * Math.sin(angle); points.push([x, y]); } const path = [['M', points[0][0], points[0][1]]]; for (let i = 1; i < 6; i++) { path.push(['L', points[i][0], points[i][1]]); } path.push(['Z']); return path; } // 主体图形样式:根据 collapsed 状态切换填充色与透明度 protected getKeyStyle(attributes: Required<BaseComboStyleProps>) { const style = super.getKeyStyle(attributes); return { ...style, d: this.getKeyPath(attributes), fill: attributes.collapsed ? '#FF9900' : '#F04864', fillOpacity: attributes.collapsed ? 0.5 : 0.2, stroke: '#54BECC', lineWidth: 2, }; } // 用 path 类型绘制主体图形 protected drawKeyShape(attributes: Required<BaseComboStyleProps>, container: Group) { return this.upsert('key', 'path', this.getKeyStyle(attributes), container); } // 绘制展开/收起按钮:hit-area 扩大点击区域,button 绘制图标 protected drawCollapseButton(attributes: Required<BaseComboStyleProps>) { const { collapsed } = attributes; const [width] = this.getKeySize(attributes); const btnR = 8; const x = width / 2 + btnR; const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR); const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this); this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea); } // 覆盖 render,在基础渲染之上追加按钮 render(attributes, container) { super.render(attributes, container); this.drawCollapseButton(attributes, container); } // 生命周期钩子:绑定点击事件,切换展开/收起 onCreate() { this.shapeMap['hit-area'].addEventListener('click', () => { const id = this.id; const collapsed = !this.attributes.collapsed; const { graph } = this.context; if (collapsed) graph.collapseElement(id); else graph.expandElement(id); }); } }实现要点:
getKeyPath使用this.getKeySize(attributes)拿到考虑收起状态与子元素的尺寸,再按正六边形角度(每Math.PI / 3一个顶点)生成坐标并拼成 SVG path;getKeyStyle调用super.getKeyStyle继承基础样式,再叠加d(路径数据)、填充、描边等定制样式,并利用attributes.collapsed实现状态相关的视觉差异;drawKeyShape通过upsert('key', 'path', ...)以 path 图形渲染主体;onCreate生命周期钩子中通过this.shapeMap['hit-area']取到按钮图形,绑定 click 事件后调用graph.collapseElement(id)/graph.expandElement(id)完成展开/收起切换。
Step 2:注册自定义 Combo
扩展注册只需一次,即可在项目任意位置使用(内置扩展在项目导入时自动注册,自定义扩展需手动注册):
import { ExtensionCategory } from '@antv/g6'; register(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);从 register.ts 的源码可以看到,register(category, type, Ctor)会把扩展类写入EXTENSION_REGISTRY,若同名扩展已被注册会打印警告并覆盖。ExtensionCategory.COMBO指定注册分类为 Combo,'hexagon-combo'是后续挂载到图上时使用的标识符。
Step 3:在 Graph 中应用自定义 Combo
const graph = new Graph({ // ...其他配置 data: { nodes: [ { id: 'node1', combo: 'combo1', style: { x: 100, y: 100 } }, { id: 'node2', combo: 'combo1', style: { x: 150, y: 150 } }, { id: 'node3', combo: 'combo2', style: { x: 300, y: 100 } }, { id: 'node4', combo: 'combo2', style: { x: 350, y: 150 } }, ], combos: [ { id: 'combo1', data: { label: 'Hexagon 1' } }, { id: 'combo2', data: { label: 'Hexagon 2' }, style: { collapsed: true } }, ], }, combo: { type: 'hexagon-combo', // 使用注册时的名称 style: { padding: 20, showCollapseButton: true, labelText: (d) => d.data?.label, labelPlacement: 'top', }, }, // 展开/收起逻辑已由我们自己实现,这里只需拖拽行为 behaviors: ['drag-element'], }); graph.render();应用时的关键点:
combo.type必须与register时传入的类型名一致;- 节点通过
combo: 'combo1'字段声明归属,Combo 数据用data.label存放标签文本,style.collapsed控制初始收起状态; - 因为展开/收起按钮的交互已在
onCreate中自行实现,所以这里仅配置drag-element行为即可,无需再挂collapse-expand行为。
至此,第一个自定义 Combo 就完成了。
深入原理:Combo 的绘制机制
Combo 与 Node 的差异
尽管 Combo 继承自BaseNode(源码见 base-combo.ts),二者仍有关键差异:
- 自适应尺寸:Combo 根据内部元素自动计算尺寸;
- 展开/收起状态:Combo 有双显示状态,需要处理状态切换(对应
getKeySize中collapsed分支); - 层级结构:Combo 可嵌套,形成层级关系(测试用例 element-combo.ts 中
combo-1就嵌在combo-2内); - 内部元素管理:Combo 需要管理其包含的节点与子 Combo(通过
childrenNode/childrenData属性)。
此外,BaseCombo还做了两个细节处理:update时调用updateComboPosition把计算出的坐标回写模型(model.syncNodeLikeDatum),收起状态下坐标取后代节点平均中心、展开状态下取内容包围盒中心(见 base-combo.ts)。
原子图形(Atomic Graphics)
G6 的 Combo 使用 G 图形系统提供的原子图形单元绘制。所有图形都可以通过upsert()动态创建或更新,并自动管理图形状态与生命周期。原子图形的详细说明可参考 Element - Shape 概览(若该文件存在)对应的文档章节。
元素基类:两个核心属性
自定义 Combo 前需要了解元素基类中的两个重要属性:
| 属性 | 类型 | 说明 |
|---|---|---|
shapeMap | Record<string, DisplayObject> | 当前元素下所有图形的映射表 |
animateMap | Record<string, IAnimation> | 当前元素下所有动画的映射表 |
shapeMap正是上例中this.shapeMap['hit-area']能取到按钮图形的原因——凡是通过upsert创建的图形都会被记录其中。
upsert(name, Ctor, style, container):图形创建/更新
upsert是 "update or insert" 的缩写,负责在元素中新增或更新图形,是自定义 Combo 时使用最频繁的方法:
upsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);| 参数 | 类型 | 说明 |
|---|---|---|
key | string | 图形键名,对应shapeMap中的键。内置键包括'key'、'label'、'halo'、'icon'、'port'、'badge'。键名不要使用特殊符号,会转换为驼峰形式去调用getXxxStyle与drawXxxShape方法 |
Ctor | { new (...args: any[]): DisplayObject } | 图形类,如'circle'、'rect'、'path' |
style | Record<string, any> | 图形样式 |
container | DisplayObject | 图形挂载的容器 |
例如插入一个固定位置的紫色圆形:
this.upsert( 'element-key', // 元素唯一标识 'circle', // 图形类型 { x: 100, y: 100, fill: '#a975f3' }, // 样式配置对象 container, // 父容器 );为什么用upsert而不是直接container.appendChild()创建图形?
- 性能更好:状态变化或数据更新时智能复用已有图形,而不是删除重建,显著提升渲染性能;
- 代码更简洁:无需手动判断元素是否存在;
- 管理方便:所有通过
upsert创建的图形都会记录在shapeMap中,可用this.getShape(key)轻松访问。
render(attributes, container):Combo 渲染主入口
每个自定义 Combo 类都必须实现render(attributes, container)方法,它定义了 Combo 如何"被绘制",可在其中用各种原子图形组合出想要的形态:
render(style: Record<string, any>, container: Group): void;| 参数 | 类型 | 说明 |
|---|---|---|
style | Record<string, any> | 元素样式 |
container | Group | 容器 |
BaseCombo.render先调用super.render(attributes, container)(绘制 key、label、halo 等基础图形),再绘制 collapsed marker(见 base-combo.ts);自定义类在其后追加额外图形即可。
getShape(name):获取已创建的图形
创建之后可能需要修改子图形属性或让子图形之间产生交互,此时可用getShape访问之前通过upsert创建的任意图形:
this.getShape('key');注意:图形创建顺序很重要。如果图形 B 依赖图形 A 的位置,务必确保 A 先创建。
元素约定(Element Conventions)
使用约定属性:Combo 中的约定属性包括——
this.getKeySize(attributes):获取 Combo 尺寸,已考虑收起状态与子元素;this.getContentBBox(attributes):获取内容区域包围盒;this.getComboPosition(attributes):获取 Combo 当前位置,基于状态与子元素计算。
使用
getXxxStyle与drawXxxShape成对绘制:getXxxStyle负责获取图形样式,drawXxxShape负责绘制图形,这样创建的图形支持自动执行动画。其中Xxx是传给upsert的 key 的驼峰形式。例如六边形示例中的getKeyStyle/drawKeyShape对,以及BaseCombo内部的getCollapsedMarkerStyle/drawCollapsedMarkerShape对。通过
this.context访问图上下文:context提供graph、model、element、animation等运行时能力,例如按钮点击回调里通过const { graph } = this.context拿到图实例后调用collapseElement/expandElement。BaseElement中context的定义见 base-element.ts。
生命周期钩子:在关键时刻插入自定义逻辑
ElementHooks接口(见 types/element.ts)定义了以下生命周期钩子,可在自定义 Combo 中覆写:
| 钩子函数 | 触发时机 | 典型用途 |
|---|---|---|
onCreate | Combo 创建完成且入场动画结束后 | 绑定交互事件、初始化 Combo 状态、添加外部监听器 |
onUpdate | Combo 更新且过渡动画结束后 | 更新依赖数据、调整相关元素、触发联动效果 |
onDestroy | Combo 销毁且退场动画结束后 | 清理资源、移除外部监听器、执行销毁通知 |
这些钩子在 runtime/element.ts 中被调用:createElement在入场动画的after回调中触发AFTER_ELEMENT_CREATE事件并调用element.onCreate?.();onUpdate、onDestroy的触发时机与之对应(更新/销毁动画完成后)。
状态响应:把"状态"与"绘制逻辑"解耦
G6 元素设计最强大的能力之一,是把状态响应与绘制逻辑分离。你可以在 Combo 配置中为每个状态定义样式:
combo: { type: 'custom-combo', style: { fill: '#f0f2f5', stroke: '#d9d9d9' }, state: { selected: { stroke: '#1890ff', lineWidth: 2, shadowColor: 'rgba(24,144,255,0.2)', shadowBlur: 15, }, hover: { fill: '#e6f7ff', }, }, }切换状态的方法:
graph.setElementState(comboId, ['selected']);该状态会传入render()方法的attributes,并经内部系统合并后自动应用到图形上。
也可以在自定义类中根据状态编写渲染逻辑:
protected getKeyStyle(attributes: Required<BaseComboStyleProps>) { const style = super.getKeyStyle(attributes); // 根据状态调整样式 if (attributes.states?.includes('selected')) { return { ...style, stroke: '#1890ff', lineWidth: 2, shadowColor: 'rgba(24,144,255,0.2)', shadowBlur: 15, }; } return style; }两种方式可结合使用:配置化的state适合声明式、无需写代码的常见反馈;在getKeyStyle中读取attributes.states适合需要联动多个子图形或做复杂分支逻辑的场景。
进一步探索
- 阅读 Combo 元素基础文档(若该文件存在),了解内置圆形/矩形 Combo 的完整配置;
- 在源码 packages/g6/src/elements/combos 下对比
circle.ts与rect.ts的实现,理解两种内置 Combo 如何在drawKeyShape中复用upsert; - 运行测试用例 element-combo.ts 观察嵌套 Combo、标签与虚线样式的实际效果;
- 查看 registry/register.ts 与 registry/store.ts 了解扩展注册表的实现,以及
ExtensionCategory支持的全部扩展分类。
- 数据可视化
- 前端
- 图表库
【免费下载链接】G6
♾ A Graph Visualization Framework in JavaScript.
相关推荐
G6 自定义 Combo 完整实战指南:从继承 BaseCombo 到掌握绘制原理
G6 自定义 Combo 完整实战指南:从继承 BaseCombo 到掌握绘制原理 本篇指南聚焦于 G6( GitHub 加速计划 / g6 / G6 http
数据可视化前端图表库Alpine.js 生命周期完全指南:从元素初始化到状态响应与 Alpine 启动钩子
Alpine.js 生命周期完全指南:从元素初始化到状态响应与 Alpine 启动钩子 生命周期钩子是 Alpine.js 中"在正确的时机做正确的事"的关键机
前端G6 Combo(组合)元素完全指南:从数据建模到交互与自定义
G6 Combo(组合)元素完全指南:从数据建模到交互与自定义 G6 中的 Combo(组合)是一种特殊的图元素,用于将节点与子 Combo 组织成层级容器,常
数据可视化前端图表库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考