G6 Tooltip 插件从配置到源码:交互式提示框的完整实现指南
G6 Tooltip 插件从配置到源码:交互式提示框的完整实现指南
本文以 G6 官方 API 演示文档(
packages/site/common/api/plugins/tooltip.md)为核心骨架,结合 Tooltip 插件源码、官方手册与仓库内测试用例,系统讲解 G6(@antv/g6)中 Tooltip 插件的全部配置项、运行期 API、源码原理与进阶实战。读完本文,你将能独立完成 Tooltip 的接入、样式定制、条件启用、异步内容加载、双 Tooltip 协同,以及基于updatePlugin的交互式动态配置。
一、Tooltip 插件是什么
Tooltip 是 G6 内置插件之一(见 packages/g6/src/plugins/index.ts 的导出),它会在用户将鼠标**悬停(hover)或点击(click)**图中的节点、边、Combo 时,在对应位置弹出一个信息气泡,展示元素的额外数据。其典型使用场景包括:
- 详细信息展示:节点/边上承载的数据有限,用 Tooltip 展示完整字段(名称、状态、指标等);
- 数据可视化辅助:在监控、拓扑、关系分析类图中,悬停即可查看数据点详情;
- 交互反馈:为鼠标操作提供即时视觉反馈,提升图应用的使用体验。
它继承自 BasePlugin(base-plugin.ts),底层气泡渲染复用 @antv/component 的 Tooltip 组件,因此既保持与 G6 生态的统一,又拥有完整的样式定制能力。
二、快速上手:最小配置
在 官方手册 中,最小可用配置如下:
import { Graph } from '@antv/g6';
const graph = new Graph({
// 其他配置...
plugins: [
{
type: 'tooltip',
},
],
});
只要在 plugins 数组中声明 { type: 'tooltip' },G6 即会在初始化时通过 PluginController 注册该插件。此时采用全部默认行为:悬停触发、显示在元素右上角、默认展示元素 ID。
一个带完整数据与布局的示例(来自 API 演示文档的图形结构):
const graph = new Graph(
{
data: {
nodes: [
{ id: 'node-0' },
{ id: 'node-1' },
{ id: 'node-2' },
{ id: 'node-3' },
{ id: 'node-4' },
{ id: 'node-5' },
],
edges: [
{ source: 'node-0', target: 'node-1' },
{ source: 'node-0', target: 'node-2' },
{ source: 'node-0', target: 'node-3' },
{ source: 'node-0', target: 'node-4' },
{ source: 'node-1', target: 'node-0' },
{ source: 'node-2', target: 'node-0' },
{ source: 'node-3', target: 'node-0' },
{ source: 'node-4', target: 'node-0' },
{ source: 'node-5', target: 'node-0' },
],
},
node: { style: { fill: '#7e3feb' } },
edge: { style: { stroke: '#8b9baf' } },
layout: { type: 'grid' },
behaviors: ['drag-canvas'],
plugins: ['grid-line', { type: 'tooltip', key: 'tooltip' }],
},
{ width: 600, height: 300 },
);
注意这里 plugins 数组同时混用了字符串形式('grid-line')与对象形式({ type: 'tooltip', key: 'tooltip' })。PluginOptions 类型即 (string | CustomPluginOption | ((this: Graph) => CustomPluginOption))<a href="https://link.gitcode.com/i/6fffe02c55009d0ac5d2087aae499434" target="_blank">],见 [packages/g6/src/spec/plugin.ts。为 Tooltip 显式指定 key,是后续通过 updatePlugin 动态更新配置的前提。
三、配置项总览
Tooltip 的完整配置项定义于 packages/g6/src/plugins/tooltip.ts 的 TooltipOptions 接口,结合官方手册整理如下:
| 属性 | 描述 | 类型 | 默认值 | 必选 |
|---|---|---|---|---|
| type | 插件类型 | string | tooltip |
✓ |
| key | 插件唯一标识,用于后续操作 | string | - | |
| trigger | 触发行为 | hover | click |
hover |
|
| enable | 是否启用 | boolean | ((event, items) => boolean) | true |
|
| getContent | 自定义气泡内容 | (event, items) => Promise<HTMLElement | string> | - | |
| position | 气泡相对鼠标的显示位置 | top | bottom | left | right | top-left | top-right | bottom-left | bottom-right |
top-right |
|
| offset | 相对鼠标位置的偏移 | [number, number] | [10, 10] |
|
| enterable | 鼠标指针是否可进入气泡 | boolean | false |
|
| container | 自定义渲染容器 | string | HTMLElement | - | |
| title | 气泡标题 | string | - | |
| style | 样式对象(作用于 .tooltip 等选择器) |
Record<string, any> | { '.tooltip': { visibility: 'hidden' } } |
|
| onOpenChange | 显示/隐藏回调 | (open: boolean) => void | - |
这些默认值在源码的 Tooltip.defaultOptions 中一一对应(tooltip.ts):
static defaultOptions: Partial<TooltipOptions> = {
trigger: 'hover',
position: 'top-right',
enterable: false,
enable: true,
offset: [10, 10],
style: {
'.tooltip': {
visibility: 'hidden',
},
},
};
style 默认让气泡初始 visibility: hidden,在展示时再切换为 visible——这是 show 方法内部通过 tooltipElement.update({ ...style: { '.tooltip': { visibility: 'visible' } } }) 实现的(tooltip.ts)。
四、核心配置详解
4.1 trigger:触发方式
hover(默认):鼠标移入节点/边/Combo 时立即展示,移出或移到画布空白处时隐藏;click:鼠标点击元素时展示,再次点击同一元素则隐藏(源码onClick中判断currentTarget === id实现 toggle,见 tooltip.ts)。
const graph = new Graph({
// ...
plugins: [
{
type: 'tooltip',
trigger: 'click',
getContent: (e, items) => {
return `
<div>
<div style="color: #0369a1; font-weight: bold; margin-bottom: 4px;">点击触发</div>
<div style="color: #0c4a6e;">
元素ID: ${items[0].id}<br/>
名称: ${items[0].data?.name || '未命名'}
</div>
</div>
`;
},
},
],
});
从源码可以确认,两种触发方式绑定的是完全不同的底层事件集(getEvents):
click模式监听node:click/edge:click/combo:click,并在canvas:click、contextmenu、drag时隐藏;hover模式监听node/edge/combo的pointerover与pointermove,并在canvas:pointermove、contextmenu、node:drag时隐藏。
因此两者是互斥的触发策略,切换 trigger 时插件会先 unbindEvents 再重新绑定(见 update 方法,tooltip.ts)。
4.2 enable:条件启用
enable 接受布尔值或函数。函数签名为 (event: IElementEvent, items: ElementDatum[]) => boolean,其中 event.targetType 可取 'node' | 'edge' | 'combo',items 是命中的元素数据数组。
只对节点启用 Tooltip:
const graph = new Graph({
// ...
plugins: [
{
type: 'tooltip',
enable: (e) => e.targetType === 'node',
getContent: (e, items) => {
return `<div>节点: ${items[0].data.name}</div>`;
},
},
],
});
基于数据字段精细控制(来自仓库测试用例 plugin-tooltip-enable.ts):
plugins: [
{
key: 'tooltip',
type: 'tooltip',
trigger: 'click',
enable: (e, items) => {
// 只有 data.type === 'test1' 的节点才显示
return items[0].data?.type === 'test1';
},
getContent: (evt, items) => {
return items[0].data?.desc || '';
},
},
],
结合 show 方法源码 可以看到:当元素不满足 enable 条件时,插件会直接调用 hide 并中止渲染,因此 getContent 不会被触发,能有效节省开销。
4.3 getContent:自定义内容
getContent 接收 (event, items) 两个参数,返回 HTMLElement | string 或对应 Promise(支持异步)。items<a href="https://link.gitcode.com/i/1c802265a716c7178cea04b0d25ffb1f" target="_blank">0] 为当前目标元素的数据;边的数据通过 model.getEdgeData([id]) 获取,其 id 可能不存在,可回退用 source -> target 表示(源码见 [getElementData)。
节点与边差异化渲染:
plugins: [
{
type: 'tooltip',
getContent: (e, items) => {
const item = items[0];
if (e.targetType === 'node') {
return `
<div>
<h4 style="margin: 0 0 8px 0; color: #333; border-bottom: 1px solid #eee; padding-bottom: 4px;">
${item.data.name}
</h4>
<div style="margin: 4px 0; color: #666;">
<strong>类型:</strong> ${item.data.type}
</div>
<div style="margin: 4px 0; color: #666;">
<strong>状态:</strong>
<span style="color: ${item.data.status === '运行中' ? '#52c41a' : '#ff4d4f'}">
${item.data.status}
</span>
</div>
</div>
`;
} else if (e.targetType === 'edge') {
return `
<div>
<h4 style="margin: 0 0 8px 0; color: #333;">连接信息</h4>
<div style="margin: 4px 0; color: #666;"><strong>带宽:</strong> ${item.data.bandwidth}</div>
<div style="margin: 4px 0; color: #666;"><strong>延迟:</strong> ${item.data.latency}</div>
</div>
`;
}
return '暂无信息';
},
},
],
异步加载:getContent 可以返回 Promise,例如从远端接口拉取详情(见 plugin-tooltip-async.ts):
getContent: (evt, items) => {
return new Promise((resolve) => {
setTimeout(() => resolve(items[0].data?.desc || ''), 300);
});
},
源码在 show 中会 await getContent(event, items),若返回内容为空则直接中断展示(tooltip.ts)。
未配置 getContent 时的默认内容:插件会自动组装一个列表卡片——title(未设置时为元素类型 node/edge/combo),每条数据一行 ID(边显示 source -> target),并附带元素渲染颜色(节点取 fill、边取 stroke),相关实现见 tooltip.ts。
4.4 position:显示位置
气泡相对鼠标位置的方位,可选 8 个值:top、bottom、left、right、top-left、top-right(默认)、bottom-left、bottom-right。
不同元素可以配置不同位置的 Tooltip 实例(每个实例用 key 区分、用 enable 圈定目标元素),例如让 4 个节点分别展示上/下/左/右方位的气泡,并在 style 中配以不同配色(完整示例见 官方手册 position 小节)。
4.5 offset:偏移量
以鼠标位置为基点,向 x/y 方向偏移。默认 [10, 10],即气泡出现在鼠标右下方 10px 处:
plugins: [
{
key: 'tooltip-default',
type: 'tooltip',
enable: (e, items) => items[0].id === 'node1',
getContent: () => '默认偏移 [10,10]',
},
{
key: 'tooltip-custom',
type: 'tooltip',
offset: [30, -10], // 向右 30px,向上 10px
enable: (e, items) => items[0].id === 'node2',
getContent: () => '自定义偏移 [30,-10]',
},
],
4.6 enterable:鼠标可进入气泡
默认 false,鼠标一旦移出元素气泡即消失;当气泡内需要放置按钮、链接等可交互内容时,设置 enterable: true,鼠标即可移入气泡进行后续操作:
plugins: [
{
type: 'tooltip',
enterable: true,
position: 'right',
getContent: (e, items) => {
const item = items[0];
return `
<div>
<h4 style="margin: 0 0 12px 0; color: #333;">用户操作</h4>
<div style="margin-bottom: 8px; color: #666;"><strong>姓名:</strong> ${item.data.name}</div>
<div style="margin-bottom: 12px; color: #666;"><strong>邮箱:</strong> ${item.data.email}</div>
<button onclick="alert('发送消息给 ${item.data.name}')"
style="padding: 4px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
发消息
</button>
<button onclick="alert('查看 ${item.data.name} 的详情')"
style="padding: 4px 12px; background: #52c41a; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
详情
</button>
</div>
`;
},
style: {
'.tooltip': {
background: '#fff',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
minWidth: '200px',
},
},
},
],
4.7 style:样式自定义
通过 style 字段按 CSS 选择器覆盖气泡样式,最常用的是 .tooltip 类选择器。深色主题示例:
plugins: [
{
key: 'tooltip-dark',
type: 'tooltip',
style: {
'.tooltip': {
background: '#1f1f1f',
color: '#fff',
border: '1px solid #333',
borderRadius: '8px',
fontSize: '14px',
boxShadow: '0 4px 20px rgba(0,0,0,0.3)',
},
},
getContent: () => `<div>深色主题内容</div>`,
},
],
值得一提的是仓库中的回归用例 bug-tooltip-resize.ts 展示了通过 style: { '.tooltip': { transition: 'none' } } 关闭过渡动画,避免画布 resize 后气泡位置出现异常抖动的处理方式,供你在布局频繁变化的场景参考。
五、运行期动态配置:updatePlugin / setPlugins / getPluginInstance
API 演示文档(tooltip.md)的核心玩法,是借助 dat.GUI 控制面板运行期动态修改 Tooltip 配置。其模式为:面板变更后,通过 graph.updatePlugin 只更新指定 key 的插件,再调用 graph.render() 重绘:
const options = {
type: 'tooltip',
trigger: 'hover',
enable: 'always',
position: 'top-left',
enterable: false,
};
const optionFolder = gui.addFolder('Tooltip Options');
optionFolder.add(options, 'type').disable(true);
optionFolder.add(options, 'trigger', ['click', 'hover']);
optionFolder.add(options, 'enable', ['always', 'node', 'edge']);
optionFolder.add(options, 'position', [
'top', 'bottom', 'left', 'right',
'top-left', 'top-right', 'bottom-left', 'bottom-right',
]);
optionFolder.add(options, 'enterable');
optionFolder.onChange((e) => {
const { enable, ...rest } = e.object;
let enableFn = () => true;
if ((enable === 'node') | (enable === 'edge')) {
enableFn = (e) => e.targetType === enable;
}
graph.updatePlugin({
key: 'tooltip',
enable: enableFn,
...rest,
});
graph.render();
});
这段代码覆盖了 Tooltip 的 5 个关键开关:type、trigger、enable、position、enterable,并把字符串化的 enable 翻译为 e.targetType === enable 的判定函数——正是上一节 enable 函数式用法的实践体现。
updatePlugin 的定义位于 packages/g6/src/runtime/graph.ts,其核心逻辑是按 key 匹配并浅合并:
public updatePlugin(plugin: UpdatePluginOption): void {
this.setPlugins((plugins) =>
plugins.map((_plugin) => {
if (typeof _plugin === 'object' && _plugin.key === plugin.key) {
return { ..._plugin, ...plugin };
}
return _plugin;
}),
);
}
因此更新前必须给插件配置 key,否则无法定位目标插件。相关的运行期 API 还有:
graph.setPlugins(plugins | (prev) => next):全量替换插件列表,可传入函数做增量修改,如(plugins) => <a href="https://link.gitcode.com/i/7fb2b28304edc685bdf5830e4b9c554c" target="_blank">...plugins, { key: 'grid-line' }]([graph.ts);graph.getPlugins():读取当前插件配置(graph.ts);graph.getPluginInstance<T>(key):获取插件实例以调用其公开方法(graph.ts),其内部实现会先在extensionMap中精确查找,找不到再按type模糊查找并给出告警(plugin.ts)。
六、源码原理:Tooltip 的内部工作流
理解了配置,再看 tooltip.ts 的完整生命周期,会更清楚每个参数如何生效。
1. 构造与挂载:构造函数执行 super(context, Object.assign({}, Tooltip.defaultOptions, options)) 完成配置合并,随后 render() 获取画布容器并初始化底层气泡组件 TooltipComponent,最后 bindEvents() 按 trigger 绑定事件(tooltip.ts)。
2. show 流程(show 方法):
- 先判断目标是否即将销毁(
isToBeDestroyed),再通过graph.getElementType(id)推断元素类型; - 调用
getElementData从 model 中取数据,经isEnable过滤后决定是否继续; - 有
getContent则await自定义内容;否则用默认逻辑组装 ID 列表; - 以事件
client坐标(无事件时退化为元素 style 坐标)计算气泡位置; - 触发
onOpenChange?.(true),调用tooltipElement.update将.tooltip置为visible。
3. hide 流程(hide 方法):外部直接调用 hide()(不传事件)或事件驱动均会触发 onOpenChange?.(false),并通过 tooltipElement.hide(x, y) 隐藏气泡;重复调用会被 currentTarget 判空短路,避免多余开销。
4. 定位逻辑(tooltipStyleProps):气泡的 container 默认取画布容器左上角坐标的负值({ x: -left, y: -top }),使定位基准与画布一致;bounding 为画布尺寸,用于防止气泡溢出画布边界。
5. 销毁:destroy() 依次解绑事件、移除气泡 DOM、调用父类销毁(tooltip.ts)。
七、插件实例 API:showById / show / hide
通过 graph.getPluginInstance('tooltip') 拿到实例后,可直接调用公开方法:
const tooltip = graph.getPluginInstance('tooltip');
// 通过元素 ID 直接展示(不依赖鼠标事件,可用于程序化触发)
await tooltip.showById('node-1');
// 展示/隐藏
tooltip.hide();
showById 的实现是把 { target: { id } } 包装成伪事件再走 show 流程(tooltip.ts),因此同样受 enable 与 getContent 约束——仓库单测正是用 showById 验证配置是否生效(见下节)。
八、进阶实战
8.1 双 Tooltip:点击与悬停显示不同内容
同一个图上可以同时挂载两个 Tooltip,用 key 区分、用 onOpenChange 协同互斥(见 plugin-tooltip-dual.ts):点击工具条展示"点击详情"时隐藏悬停工具条,反之亦然。
plugins: [
function () {
return {
key: 'tooltip-click',
type: 'tooltip',
trigger: 'click',
getContent: (evt, items) => `<div>click ${items[0].id}</div>`,
onOpenChange: (open) => {
const tooltip = this.getPluginInstance('tooltip-hover');
if (tooltip && open) tooltip.hide();
},
};
},
function () {
return {
key: 'tooltip-hover',
type: 'tooltip',
trigger: 'hover',
getContent: (evt, items) => `<div>hover ${items[0].id}</div>`,
onOpenChange: (open) => {
const tooltip = this.getPluginInstance('tooltip-click');
if (tooltip && open) tooltip.hide();
},
};
},
],
注意插件配置支持函数形式,函数内 this 指向 Graph 实例,可借此访问其他插件实例。
8.2 对自定义节点内部图形精准触发
当节点内包含多个子图形(如自绘的"+"按钮)时,可用 enable 结合 e.originalTarget.className 只对特定图形触发(见 plugin-tooltip-with-custom-node.ts):
plugins: [
{
type: 'tooltip',
trigger: 'hover',
enable: (e) => e.originalTarget.className === 'custom-shape',
},
],
8.3 测试验证
仓库单测 packages/g6/tests/unit/plugins/tooltip.spec.ts 覆盖了三条核心行为路径,可作为理解插件语义的参照:
- enable 生效:
showById('node3')因不满足条件,.tooltip的visibility保持hidden;showById('node1')则变为visible; - 空内容拦截:
getContent返回空串时气泡不展示; - 异步内容:Promise 返回的字符串正确渲染进气泡
innerHTML。
九、总结
Tooltip 插件是 G6 中最轻量、最常用的信息提示方案。接入只需一行 { type: 'tooltip' };进阶使用则围绕六个核心维度展开——trigger 决定交互方式、enable 决定作用范围、getContent 决定内容形态(支持异步与 HTML)、position/offset/enterable/style 决定呈现细节、onOpenChange 与其他插件协同、updatePlugin/getPluginInstance 负责运行期动态调整。配合 插件源码 与 测试用例 阅读,即可在真实项目中灵活组合出符合业务需要的提示交互。