Chart.js Tooltip 定位模式实战:从 average/nearest 到自定义 Positioner
【免费下载链接】Chart.jsSimple HTML5 Charts using the项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js
本篇文章围绕 Chart.js 的 tooltipposition定位模式展开:先讲解内置average、nearest两种定位模式的算法差异与源码实现,再以官方样例 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.mode与interaction.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):
- 遍历所有传入的 tooltip 元素,跳过
hasValue()为 false(无有效值)的元素; - 对每个有效元素,通过
el.tooltipPosition()取得其 tooltip 锚点位置; - X 坐标去重:把所有 x 值放入一个
Set中再求平均;Y 坐标则直接对所有 y 值求算术平均; - 若没有任何有效元素(
count === 0或xSet.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):
- 以事件位置
eventPosition(鼠标在 canvas 坐标中的位置)为起点; - 遍历激活元素,用
distanceBetweenPoints计算事件位置到元素中心el.getCenterPoint()的欧氏距离,记录距离最小的那个元素; - 若找到了最近元素,取该元素的
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)在运行时切换average、nearest以及自定义的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/blue与Utils.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', }; };逐步拆解
- 注册入口:
Tooltip.positioners(或浏览器全局方式下的components.Tooltip.positioners/Chart.Tooltip.positioners,详见 utils.md 中关于 components 的说明)就是一个可扩展的映射表,往里添加以模式名命名的函数即可注册新定位模式; - 复用内置算法:先调用
average(items)计算出锚点 x,复用了内置平均逻辑,避免重复造轮子; - 处理空状态:当没有有效元素时
average返回false,这里原样返回false,tooltip 便不会显示; - 利用
this拿到 chart:positioner 以 tooltip 实例为this被调用(与 test/specs/plugin.tooltip.tests.js 中断言fn.calls.first().object instanceof Tooltip的调用约定一致),因此可以通过this.chart访问chart.chartArea.bottom——即图表绘图区底边界的 y 坐标,这正是“贴底”定位的数据来源; - 返回坐标与对齐:返回
{x: pos.x, y: chartArea.bottom}让锚点落在底部,同时返回xAlign: 'center'、yAlign: 'bottom'覆盖默认的对齐计算——xAlign/yAlign决定 tooltip 箭头相对 tooltip 框的位置,这两项在 配置文档 中可取值left/center/right与top/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中,对齐优先级是:
- positioner 返回的
xAlign/yAlign(若提供); - 配置项
options.xAlign/options.yAlign; - 根据 chart、tooltip 尺寸与空间自动推断(
determineXAlign/determineYAlign)。
随后getBackgroundPoint(src/plugins/plugin.tooltip.js)依据对齐方向与caretSize、caretPadding、cornerRadius计算出 tooltip 左上角坐标,并通过_limitValue保证 tooltip 不会被挤出画布边界。这解释了为什么样例的自定义 positioner 要同时返回xAlign与yAlign:yAlign: 'bottom'会让 tooltip 框整体位于锚点上方,视觉上呈现“从底部向上冒出来并贴着图表底边”的效果;同时caretSize(默认 5)与caretPadding(默认 2)这两个配置(见 src/plugins/plugin.tooltip.js 的默认值)会参与箭头与框体之间距离的计算。
相关配置项速查
定位模式相关的核心配置集中在options.plugins.tooltip命名空间(全局默认值见Chart.defaults.plugins.tooltip,完整参数表见 Tooltip 配置文档):
| 配置 | 类型 | 默认值 | 说明 |
|---|---|---|---|
position | string | 'average' | 定位模式,内置average、nearest,可自定义 |
xAlign | string | undefined | 箭头在 X 方向的位置:left/center/right,未设置时自动推断 |
yAlign | string | undefined | 箭头在 Y 方向的位置:top/center/bottom,未设置时自动推断 |
caretSize | number | 5 | tooltip 箭头的大小(px) |
caretPadding | number | 2 | 箭头端点与锚点之间的额外距离(px) |
mode | string | interaction.mode | 决定哪些元素出现在 tooltip 中,与定位模式相互独立 |
intersect | boolean | interaction.intersect | 是否仅在命中元素时激活 tooltip |
延伸阅读
- Tooltip 配置文档:完整的 tooltip 参数表、回调、Tooltip Model 与外部(HTML)tooltip;
- Interactions 配置文档:
interaction.mode与intersect的完整说明; - 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 the项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考