Chart.js Tooltip 定位模式实战:从 average/nearest 到自定义 Positioner
2026/9/18 6:39:56 网站建设 项目流程

Chart.js Tooltip 定位模式实战:从 average/nearest 到自定义 Positioner

【免费下载链接】Chart.jsSimple HTML5 Charts using thetag项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js

本篇文章围绕 Chart.js 的 tooltipposition定位模式展开:先讲解内置averagenearest两种定位模式的算法差异与源码实现,再以官方样例 docs/samples/tooltip/position.md 的完整代码为例,演示如何在运行时动态切换定位模式,并手把手实现一个把 tooltip 固定到图表底部(bottom)的自定义 positioner。读完本文,你将能掌控 tooltip 的锚点计算、对齐规则,并写出属于自己的任意定位逻辑。

定位模式(Position Modes)概述

在 Chart.js 中,tooltip 的摆放位置由options.plugins.tooltip.position控制,默认值为'average'。该配置在 tooltip 配置文档 中被明确定义,对应源码 src/plugins/plugin.tooltip.js 中插件defaults里的position: 'average'

内置的定位模式只有两个:

  • 'average':把 tooltip 放在 tooltip 内所展示元素的平均位置上;
  • 'nearest':把 tooltip 放在距离事件位置(鼠标位置)最近的元素上。

除此之外,你还可以通过往Tooltip.positioners映射表里添加函数来定义自定义定位模式(详见下文),官方配置文档的 Custom Position Modes 一节同样说明了这一点。

为什么 position 和 interaction 容易混淆

容易混淆的是:tooltip 的position(定位模式)决定“tooltip 画在哪里”,而options.interaction.mode(交互模式)决定“哪些元素被激活、出现在 tooltip 中”。两者是两套独立的机制,但会协同工作:

  • interaction.modeinteraction.intersect决定_active元素集合(即 tooltip 要展示哪些数据点);
  • position决定基于这些_active元素,把 tooltip 框放在哪个坐标。

官方样例中特意把interaction.mode: 'index'intersect: false组合使用:鼠标在图表区域移动时,同一 x 索引下的两个数据集点都会被选中,从而让average模式体现出“多数据点求平均”的效果。关于交互模式的完整说明见 Interactions 配置文档 与对应的 interactions 样例。

内置定位模式的源码级原理解析

两个内置定位器都定义在 src/plugins/plugin.tooltip.js 顶部的positioners对象中,并通过static positioners = positioners(src/plugins/plugin.tooltip.js)暴露给外部。

average:平均位置定位

average的实现逻辑(src/plugins/plugin.tooltip.js):

  1. 遍历所有传入的 tooltip 元素,跳过hasValue()为 false(无有效值)的元素;
  2. 对每个有效元素,通过el.tooltipPosition()取得其 tooltip 锚点位置;
  3. X 坐标去重:把所有 x 值放入一个Set中再求平均;Y 坐标则直接对所有 y 值求算术平均;
  4. 若没有任何有效元素(count === 0xSet.size === 0),返回false,避免除零产生 NaN。

其中“X 去重”是关键细节:当使用interaction.mode: 'index'时,同一列上的多个数据点往往共享同一个 x 坐标,若不去重,这个 x 会被重复计入而抬高权重。这一点在 test/specs/plugin.tooltip.tests.js 的测试用例中有专门验证——对 3 个激活元素(其中 2 个 x 相同)断言caretX不等于普通数组平均、而等于去重后的Set平均。

此外,test/specs/plugin.tooltip.tests.js 还验证了当传入的数据点均无有效值时,average不会抛异常而是返回false

nearest:最近元素定位

nearest的实现逻辑(src/plugins/plugin.tooltip.js):

  1. 以事件位置eventPosition(鼠标在 canvas 坐标中的位置)为起点;
  2. 遍历激活元素,用distanceBetweenPoints计算事件位置到元素中心el.getCenterPoint()的欧氏距离,记录距离最小的那个元素;
  3. 若找到了最近元素,取该元素的tooltipPosition()作为最终锚点;否则退回事件位置本身。

因此nearest的行为直观且“跟手”:tooltip 会吸附到离鼠标最近的元素上,适合点密度高、需要精准查看单个数据点的场景。

positioner 被调用的时机

positioners[options.position].call(this, active, eventPosition)在 src/plugins/plugin.tooltip.js 的update()中被调用,传入激活元素数组与事件位置,this指向 tooltip 实例。返回的{x, y}会成为 tooltip 的caretX/caretY(箭头指向点),随后determineAlignment(src/plugins/plugin.tooltip.js)再根据 tooltip 尺寸与xAlign/yAlign决定整个 tooltip 框的偏移方向,最终由getBackgroundPoint(src/plugins/plugin.tooltip.js)算出 tooltip 左上角坐标,并用_limitValue把坐标钳制在画布范围内防止溢出。

_positionChanged()(src/plugins/plugin.tooltip.js)中,同样会用当前定位模式计算新位置,与已有caretX/caretY比较以决定是否需要触发重绘。

样例完整解读:动态切换定位模式

position 样例 是一个可交互的 line chart:它通过三个“动作按钮”(actions)在运行时切换averagenearest以及自定义的bottom三种定位模式,并用图表标题实时显示当前模式。

数据结构与交互配置

样例的数据生成依赖 docs/scripts/utils.js 提供的示例工具函数(该文件仅用于官方示例,不应用于生产环境,详见 utils 说明):

  • Utils.months({count: 7}):生成 7 个月份名称作为 x 轴标签(实现见 docs/scripts/utils.js);
  • Utils.numbers({count: 7, min: -100, max: 100}):生成 7 个在 -100~100 之间的随机数(实现见 docs/scripts/utils.js);
  • Utils.CHART_COLORS.red/blueUtils.transparentize(color, 0.5):内置色板与半透明色(见 docs/scripts/utils.js)。

两个数据集都设置fill: false,只绘制折线与数据点,避免面积填充干扰观察 tooltip 锚点位置。核心配置如下:

const config = { type: 'line', data: data, options: { interaction: { intersect: false, mode: 'index', }, plugins: { title: { display: true, text: (ctx) => 'Tooltip position mode: ' + ctx.chart.options.plugins.tooltip.position, }, } } };

标题回调读取ctx.chart.options.plugins.tooltip.position并实时显示当前定位模式,是观察运行时切换效果的关键手段。

运行时切换 position 的三种动作

actions数组中的每个 handler 都接收当前 chart 实例,修改chart.options.plugins.tooltip.position后调用chart.update()完成切换:

const actions = [ { name: 'Position: average', handler(chart) { chart.options.plugins.tooltip.position = 'average'; chart.update(); } }, { name: 'Position: nearest', handler(chart) { chart.options.plugins.tooltip.position = 'nearest'; chart.update(); } }, { name: 'Position: bottom (custom)', handler(chart) { chart.options.plugins.tooltip.position = 'bottom'; chart.update(); } }, ];

这里演示了一个重要能力:tooltip 的position运行时可变的普通配置,直接修改配置对象并update()即可生效,无需重建 chart。第三条动作引用的bottom并非内置模式,而是下面要定义的自定义 positioner。

自定义 Positioner:把 tooltip 固定到图表底部

样例的精华在于定义了一个名为bottom的自定义 positioner,让 tooltip 始终出现在图表绘图区(chartArea)的底部中央。

实现代码

// Create a custom tooltip positioner to put at the bottom of the chart area components.Tooltip.positioners.bottom = function(items) { const pos = components.Tooltip.positioners.average(items); // Happens when nothing is found if (pos === false) { return false; } const chart = this.chart; return { x: pos.x, y: chart.chartArea.bottom, xAlign: 'center', yAlign: 'bottom', }; };

逐步拆解

  1. 注册入口Tooltip.positioners(或浏览器全局方式下的components.Tooltip.positioners/Chart.Tooltip.positioners,详见 utils.md 中关于 components 的说明)就是一个可扩展的映射表,往里添加以模式名命名的函数即可注册新定位模式;
  2. 复用内置算法:先调用average(items)计算出锚点 x,复用了内置平均逻辑,避免重复造轮子;
  3. 处理空状态:当没有有效元素时average返回false,这里原样返回false,tooltip 便不会显示;
  4. 利用this拿到 chart:positioner 以 tooltip 实例为this被调用(与 test/specs/plugin.tooltip.tests.js 中断言fn.calls.first().object instanceof Tooltip的调用约定一致),因此可以通过this.chart访问chart.chartArea.bottom——即图表绘图区底边界的 y 坐标,这正是“贴底”定位的数据来源;
  5. 返回坐标与对齐:返回{x: pos.x, y: chartArea.bottom}让锚点落在底部,同时返回xAlign: 'center'yAlign: 'bottom'覆盖默认的对齐计算——xAlign/yAlign决定 tooltip 箭头相对 tooltip 框的位置,这两项在 配置文档 中可取值left/center/righttop/center/bottom,同时也在determineAlignment(src/plugins/plugin.tooltip.js)中被优先采纳。

自定义 positioner 的签名约定

按照配置文档 Custom Position Modes 中的定义,positioner 函数的完整签名为:

/** * @param elements {Chart.Element[]} the tooltip elements * @param eventPosition {Point} the position of the event in canvas coordinates * @returns {TooltipPosition} the tooltip position */ Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) { // this 指向 tooltip 实例,可访问 this.chart return { x: 0, y: 0 // 可选:返回 xAlign / yAlign 覆盖默认对齐 }; };

样例的bottom定位器只用到了第一个参数items,通过复用average规避了事件位置参数,但若你要实现“基于鼠标偏移”的定位,直接读取eventPosition即可。

在配置中引用自定义模式

注册完成后,把position设为注册时使用的字符串名即可:

new Chart(ctx, { data, options: { plugins: { tooltip: { position: 'bottom' } } } });

如果你使用 TypeScript,还需要通过模块扩充(module augmentation)把新模式注册进TooltipPositionerMap,以获得类型提示与检查:

declare module 'chart.js' { interface TooltipPositionerMap { myCustomPositioner: TooltipPositionerFunction<ChartType>; } }

定位模式与 tooltip 对齐机制的关系

positioner 返回的{x, y}只是 tooltip 的“锚点”(即最终渲染模型中的caretX/caretY,箭头所指向的位置),tooltip 框本身如何摆放由对齐机制决定。在 src/plugins/plugin.tooltip.js 的determineAlignment中,对齐优先级是:

  1. positioner 返回的xAlign/yAlign(若提供);
  2. 配置项options.xAlign/options.yAlign
  3. 根据 chart、tooltip 尺寸与空间自动推断(determineXAlign/determineYAlign)。

随后getBackgroundPoint(src/plugins/plugin.tooltip.js)依据对齐方向与caretSizecaretPaddingcornerRadius计算出 tooltip 左上角坐标,并通过_limitValue保证 tooltip 不会被挤出画布边界。这解释了为什么样例的自定义 positioner 要同时返回xAlignyAlignyAlign: 'bottom'会让 tooltip 框整体位于锚点上方,视觉上呈现“从底部向上冒出来并贴着图表底边”的效果;同时caretSize(默认 5)与caretPadding(默认 2)这两个配置(见 src/plugins/plugin.tooltip.js 的默认值)会参与箭头与框体之间距离的计算。

相关配置项速查

定位模式相关的核心配置集中在options.plugins.tooltip命名空间(全局默认值见Chart.defaults.plugins.tooltip,完整参数表见 Tooltip 配置文档):

配置类型默认值说明
positionstring'average'定位模式,内置averagenearest,可自定义
xAlignstringundefined箭头在 X 方向的位置:left/center/right,未设置时自动推断
yAlignstringundefined箭头在 Y 方向的位置:top/center/bottom,未设置时自动推断
caretSizenumber5tooltip 箭头的大小(px)
caretPaddingnumber2箭头端点与锚点之间的额外距离(px)
modestringinteraction.mode决定哪些元素出现在 tooltip 中,与定位模式相互独立
intersectbooleaninteraction.intersect是否仅在命中元素时激活 tooltip

延伸阅读

  • Tooltip 配置文档:完整的 tooltip 参数表、回调、Tooltip Model 与外部(HTML)tooltip;
  • Interactions 配置文档:interaction.modeintersect的完整说明;
  • Interactions 样例:对比index/dataset/point/nearest/x/y等交互模式的运行效果;
  • Line 图表文档:样例所使用折线图的配置详解;
  • Data Structures 文档:labels与数据结构的说明;
  • Tooltip 源码:positioners定义(第 17~91 行)、update流程(第 638 行起)与默认值(第 1275 行起);
  • positioners 测试:自定义 positioner 调用约定、average去重逻辑与空数据兜底的验证用例。

【免费下载链接】Chart.jsSimple HTML5 Charts using thetag项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询