G6 Tooltip 插件从配置到源码:交互式提示框的完整实现指南

原创2026-09-22 16:15:50463 阅读
文章标签:数据可视化前端图表库

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 负责运行期动态调整。配合 插件源码 与 测试用例 阅读,即可在真实项目中灵活组合出符合业务需要的提示交互。

登录后查看全文
G6