news 2026/9/23 22:47:07

G6 自定义 Combo 完全指南:从继承 BaseCombo 到状态响应与生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
G6 自定义 Combo 完全指南:从继承 BaseCombo 到状态响应与生命周期
  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】G6

♾ A Graph Visualization Framework in JavaScript.

项目地址:https://gitcode.com/gh_mirrors/g6/G6
点击查看免费下载

自定义 Combo 是 G6(Graph Visualization Framework in JavaScript)中把业务分组语义(如部门、集群、子图)落地为可视化形态的关键手段。本文以 custom-combo.en.md 为骨架,完整讲解 Combo 的基本组成、两种自定义路线、三步创建第一个自定义 Combo 的完整代码,并深入 G6 源码剖析upsertrendergetShape、生命周期钩子与状态响应等底层原理,帮助读者从"能跑通示例"进阶到"能按业务自由定制"。

Combo 的基本组成与独有特性

在动手之前,先理解 G6 中一个完整 Combo 通常由哪些图形部件构成。文档给出的三部分如下:

  • key:Combo 的主体图形,决定其轮廓形状,例如圆形、矩形,或本文示例中的六边形;
  • label:文本标签,通常用于展示 Combo 的名称或描述;
  • halo:围绕主体图形绘制的光晕图形,用于高亮、选中反馈等场景。

除这三部分外,BaseCombo还支持collapsedMarker(收起标记)与iconbadgeport等通用图形插槽。从 base-combo.ts 的BaseComboStyleProps可以看到,Combo 样式还包含size(展开默认大小)、collapsedSize(收起默认大小,默认 32)、padding(内边距,仅展开时生效)、childrenNodechildrenData等专属属性。

Combo 与普通节点不同,具有以下四类特性:

  1. 包含性(Containment):Combo 可以包含节点和其他 Combo,形成层级结构;
  2. 双状态(Two States):具有展开(Expanded)与收起(Collapsed)两种状态;
  3. 自适应尺寸(Adaptive Size):会根据内部元素自动计算合适的大小;
  4. 拖拽行为(Drag Behavior):支持整体拖拽,也支持把元素拖入/拖出 Combo。

这些特性在 base-combo.ts 中有直接体现:getKeySize会依据childrenNode.length是否为零以及collapsed状态,分别走getEmptyKeySizegetCollapsedKeySizegetExpandedKeySize,其中展开尺寸由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),二者仍有关键差异:

  1. 自适应尺寸:Combo 根据内部元素自动计算尺寸;
  2. 展开/收起状态:Combo 有双显示状态,需要处理状态切换(对应getKeySizecollapsed分支);
  3. 层级结构:Combo 可嵌套,形成层级关系(测试用例 element-combo.ts 中combo-1就嵌在combo-2内);
  4. 内部元素管理:Combo 需要管理其包含的节点与子 Combo(通过childrenNode/childrenData属性)。

此外,BaseCombo还做了两个细节处理:update时调用updateComboPosition把计算出的坐标回写模型(model.syncNodeLikeDatum),收起状态下坐标取后代节点平均中心、展开状态下取内容包围盒中心(见 base-combo.ts)。

原子图形(Atomic Graphics)

G6 的 Combo 使用 G 图形系统提供的原子图形单元绘制。所有图形都可以通过upsert()动态创建或更新,并自动管理图形状态与生命周期。原子图形的详细说明可参考 Element - Shape 概览(若该文件存在)对应的文档章节。

元素基类:两个核心属性

自定义 Combo 前需要了解元素基类中的两个重要属性:

属性类型说明
shapeMapRecord<string, DisplayObject>当前元素下所有图形的映射表
animateMapRecord<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);
参数类型说明
keystring图形键名,对应shapeMap中的键。内置键包括'key''label''halo''icon''port''badge'。键名不要使用特殊符号,会转换为驼峰形式去调用getXxxStyledrawXxxShape方法
Ctor{ new (...args: any[]): DisplayObject }图形类,如'circle''rect''path'
styleRecord<string, any>图形样式
containerDisplayObject图形挂载的容器

例如插入一个固定位置的紫色圆形:

this.upsert( 'element-key', // 元素唯一标识 'circle', // 图形类型 { x: 100, y: 100, fill: '#a975f3' }, // 样式配置对象 container, // 父容器 );

为什么用upsert而不是直接container.appendChild()创建图形?

  1. 性能更好:状态变化或数据更新时智能复用已有图形,而不是删除重建,显著提升渲染性能;
  2. 代码更简洁:无需手动判断元素是否存在;
  3. 管理方便:所有通过upsert创建的图形都会记录在shapeMap中,可用this.getShape(key)轻松访问。

render(attributes, container):Combo 渲染主入口

每个自定义 Combo 类都必须实现render(attributes, container)方法,它定义了 Combo 如何"被绘制",可在其中用各种原子图形组合出想要的形态:

render(style: Record<string, any>, container: Group): void;
参数类型说明
styleRecord<string, any>元素样式
containerGroup容器

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 当前位置,基于状态与子元素计算。
  • 使用getXxxStyledrawXxxShape成对绘制getXxxStyle负责获取图形样式,drawXxxShape负责绘制图形,这样创建的图形支持自动执行动画。其中Xxx是传给upsert的 key 的驼峰形式。例如六边形示例中的getKeyStyle/drawKeyShape对,以及BaseCombo内部的getCollapsedMarkerStyle/drawCollapsedMarkerShape对。

  • 通过this.context访问图上下文context提供graphmodelelementanimation等运行时能力,例如按钮点击回调里通过const { graph } = this.context拿到图实例后调用collapseElement/expandElementBaseElementcontext的定义见 base-element.ts。

生命周期钩子:在关键时刻插入自定义逻辑

ElementHooks接口(见 types/element.ts)定义了以下生命周期钩子,可在自定义 Combo 中覆写:

钩子函数触发时机典型用途
onCreateCombo 创建完成且入场动画结束后绑定交互事件、初始化 Combo 状态、添加外部监听器
onUpdateCombo 更新且过渡动画结束后更新依赖数据、调整相关元素、触发联动效果
onDestroyCombo 销毁且退场动画结束后清理资源、移除外部监听器、执行销毁通知

这些钩子在 runtime/element.ts 中被调用:createElement在入场动画的after回调中触发AFTER_ELEMENT_CREATE事件并调用element.onCreate?.()onUpdateonDestroy的触发时机与之对应(更新/销毁动画完成后)。

状态响应:把"状态"与"绘制逻辑"解耦

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.tsrect.ts的实现,理解两种内置 Combo 如何在drawKeyShape中复用upsert
  • 运行测试用例 element-combo.ts 观察嵌套 Combo、标签与虚线样式的实际效果;
  • 查看 registry/register.ts 与 registry/store.ts 了解扩展注册表的实现,以及ExtensionCategory支持的全部扩展分类。
  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】G6

♾ A Graph Visualization Framework in JavaScript.

项目地址:https://gitcode.com/gh_mirrors/g6/G6
点击查看免费下载

相关推荐

上一篇:Joplin 笔记导入导出完全实战指南:ENEX、Markdown、OneNote 到 JEX 备份
下一篇:ZLMediaKit音视频录制系统:FLV/HLS/MP4格式的存储策略与实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 22:46:47

Multisim 14.0 安装失败原因与可复现解决路径

简介&#xff1a;本资源是一份面向电子类专业学生、电路设计初学者及实验课程教师的Multisim 14.0软件零基础安装实操指南&#xff0c;专为解决Windows环境下该EDA工具部署困难、激活失败、路径配置错误等高频问题而编写。文档以PDF格式呈现&#xff0c;共1个文件&#xff0c;大…

作者头像 李华
网站建设 2026/9/23 22:44:35

12306抢票源码拆解:Java+Python多语言混编的自动化链路设计与实现

简介&#xff1a;面向需要优化12306转移仓库管理效率的Java开发者&#xff0c;这套源码实现完整覆盖系统核心业务链路&#xff0c;从登录认证、余票查询、订单提交到人脸核验等环节均有对应代码模块&#xff0c;适合中高级程序员借鉴其多语言融合的工程化落地方式。资源包共86个…

作者头像 李华
网站建设 2026/9/23 22:44:30

Python多智能体兵棋推演沙盒:轻量级红蓝对抗闭环实现

简介&#xff1a;本资源是一套面向高校本科生的人工智能方向毕业设计实践项目&#xff0c;聚焦多智能体博弈与兵棋推演理论的Python实现与平台验证&#xff0c;适用于人工智能、自动化、电子信息等专业学生开展课程设计、毕设选题或科研入门。压缩包共42个文件&#xff0c;含16…

作者头像 李华
网站建设 2026/9/23 22:43:45

嵌入式开发零基础学习路线:从MCU裸机到Linux应用实战

1. 嵌入式开发到底在做什么&#xff1a;从“点灯”到“造系统”的认知升级很多人第一次听到“嵌入式开发”&#xff0c;脑子里浮现的画面是焊电路板、插杜邦线、对着示波器发呆。这个印象不算错&#xff0c;但只看到了冰山一角。嵌入式开发的本质&#xff0c;是用软件去控制硬件…

作者头像 李华
网站建设 2026/9/23 22:42:54

OpenSpec 实战:用规格驱动开发解决接口契约散落与代码脱节

1. 从“规格散落各处”说起&#xff1a;OpenSpec 到底想解决什么问题如果你参与过稍微有点规模的软件项目&#xff0c;大概率经历过这样的场景&#xff1a;需求文档在某个在线文档里&#xff0c;接口定义在另一个协作平台&#xff0c;数据库字段说明藏在某个人的笔记里&#xf…

作者头像 李华
网站建设 2026/9/23 22:42:41

商标业务一网通办,这几点值得留意

官方门户改版&#xff0c;商标办理入口更集中 国家知识产权局商标局官方网站近期完成升级&#xff0c;网上申请、进度查询、电子送达等功能进一步整合。对企业和申请人来说&#xff0c;最直接的变化是&#xff1a;商标查询、注册申请、异议、评审、转让、续展等高频业务&#x…

作者头像 李华