ECharts里tooltip相关的需求,基本是每个做数据可视化的人都绕不过去的坎。鼠标放到图形上要显示什么、格式怎么排、样式怎么美化、特殊场景怎么处理,这套东西看着简单,真做起来全是细节。我这些年用ECharts做过不少大屏和后台管理系统,从最初的默认提示框一路摸索到各种自定义弹窗,踩过的坑和总结出来的经验都在这篇文章里了,需要的直接拿走。
1. tooltip基础配置:先搞清楚它能干什么
1.1 什么场景下必须配置tooltip
ECharts默认是不开启tooltip的,很多新手第一次用图表时发现鼠标放上去没反应,就是这个原因。tooltip在图表中的作用,说白了就是当鼠标悬停在图形元素上时,弹出一个信息展示窗口,帮你把当前命中点的数据完整展示出来。
这个功能在几种场景下几乎是刚需。首先是折线图和柱状图,坐标轴上的刻度往往只显示粗略值,鼠标悬停需要展示精确数据和对应的时间点或分类名;其次是饼图,扇区占比和具体数值通常需要靠tooltip来呈现;再就是地图,比如中国地图上每个省份的数据,默认只能靠visualMap的色彩深浅来感知,具体数值必须通过tooltip才能读出来。
我在实际项目里最常见的需求是三种:第一种就是最简单的默认tooltip,显示系列名、数据名和数值;第二种是要自定义显示内容,比如加上单位、百分比、环比变化、排名等信息;第三种是彻底自定义弹窗样式,做成带背景色、边框、甚至带图标的富文本卡片。这篇文章会把这三种需求全部讲到。
1.2 三个核心参数:trigger、triggerOn、formatter
配置tooltip首先就要理解这三个参数,它们决定了tooltip什么时候出现、怎样被触发、显示什么内容。
trigger是最基础的参数,取值有item和axis两种。item表示鼠标悬停在数据项上时触发,适合饼图、散点图、地图这类没有坐标轴的图表;axis表示鼠标悬停在坐标轴刻度区域时触发,适合折线图、柱状图这类直角坐标系图表,它会同时显示该刻度下所有系列的数据。
我个人的经验是:折线图多系列对比时用axis最舒服,因为一眼就能看到同一时间点所有曲线的数值;但如果是单条折线,item反而更精确,不会出现鼠标明明在某条线上,弹窗却显示了一个大范围数据的情况。
triggerOn控制触发方式,默认是mousemove,即鼠标移动时触发。有时用户希望点击后才显示tooltip,可以设置为click。这个参数在移动端很重要,因为移动端没有hover概念,只能通过点击触发,配合tooltip的show方法可以做出很流畅的交互体验。
formatter是tooltip的灵魂,负责格式化显示内容,支持字符串模板和回调函数两种写法,后面我会详细拆解。
2. 从默认到自定义:formatter的两种进阶写法
2.1 字符串模板:最简单的格式化方式
ECharts的formatter支持字符串模板,直接指定要显示哪些变量。模板中{a}代表系列名,{b}代表数据名(比如类别名、省份名、时间点),{c}代表数值,{d}代表百分比,这些是最常用的。
tooltip: { trigger: 'item', formatter: '{b}<br/>数量:{c} ({d}%)' }这个写法在饼图上很好用,比如展示某个商品的销售占比时,{b}显示商品名,{c}显示销售额,{d}%显示占比。如果想调整字段顺序,或者额外加一些提示文字,直接改模板字符串就行,非常灵活。
但字符串模板有一个最大的限制:无法根据数据条件去动态改变样式或文案。比如我想让数值超过1000的显示红色,字符串模板就做不到了。这种情况就要用回调函数。
还有一点需要注意,字符串模板中<br/>是换行符,如果想在同一行内拼接多个变量,直接写就行,不要加多余的空格,否则显示出来会有很奇怪的空隙。
2.2 回调函数:完全掌控显示内容的写法
回调函数是formatter最强大的用法,它接收一个params参数,根据你的配置返回一个字符串,这个字符串会直接渲染成tooltip的内容。由于可以写JavaScript逻辑,所以可以做条件判断、字段拼接、单位换算、甚至返回带样式的HTML。
tooltip: { trigger: 'item', formatter: function(params) { var data = params.data; var value = data.value; var percent = (value / total * 100).toFixed(1); var colorSpan = '<span style="display:inline-block;width:10px;height:10px;'; colorSpan += 'border-radius:50%;background-color:' + params.color + ';margin-right:5px;"></span>'; return colorSpan + params.name + '<br/>销售额:' + value + ' 万元<br/>占比:' + percent + '%'; } }这段代码做了几件事:先用params.data取出原始数据,计算百分比,然后用params.color获取当前系列的颜色,拼了一个小圆点作为图例标识,最后返回自定义格式的HTML字符串。
回调函数里params的结构根据触发方式不同会有差异,这个是很多新手容易搞混的地方。item触发时params是一个对象,包含seriesName、name、value、color、data等字段;axis触发时params是一个数组,数组里每个元素对应一个系列的数据对象。
我建议大家在写回调函数时先console.log(params)看一下结构,不同图表类型里字段名差异很大。比如折线图的params.value是数字或数组,饼图的params.percent是百分比,地图的params.value则是对应地区的数值。
2.3 多系列折线图的formatter处理
使用axis触发时,params是数组,默认情况下tooltip会把所有系列的数据都列出来。但有时我们只想显示其中一部分,或者想给不同系列加上不同的前后缀,这时候就需要对数组做二次处理。
tooltip: { trigger: 'axis', formatter: function(params) { var result = params[0].axisValue + '<br/>'; params.forEach(function(item) { if (item.seriesName === '已忽略的系列') return; result += '<span style="color:' + item.color + '">●</span> '; result += item.seriesName + ':' + item.value + '件<br/>'; }); return result; } }这种写法很常见,比如一条曲线是"计划值",一条是"实际值",你可能只想让计划值显示在tooltip里,或者想让实际值加粗显示。通过遍历params数组单独处理每个系列的数据,想怎么排就怎么排。需要注意的是,axisValue是横轴的值,不同图表类型字段名不一样,折线图和柱状图是axisValue,类目轴有时候是name。
3. 自定义tooltip弹窗:样式随你掌控的进阶玩法
3.1 用textStyle和extraCssText做基础美化
默认的tooltip是个小黑框白字,丑是丑了点,但符合大多数后台系统的审美。如果想快速美化,不用写CSS,直接通过tooltip内部的textStyle、backgroundColor、borderColor等基础配置项就能搞定。
tooltip: { trigger: 'item', backgroundColor: 'rgba(255,255,255,0.95)', borderColor: '#409EFF', borderWidth: 1, padding: [10, 12], textStyle: { color: '#333', fontSize: 13 }, extraCssText: 'box-shadow: 0 2px 12px rgba(0,0,0,0.1); border-radius: 6px;' }extraCssText是个很实用的配置项,可以往里追加任何CSS样式。我在大屏项目里经常用它来给弹窗加阴影、圆角、甚至背景渐变色。但要注意,ECharts渲染tooltip时用的是DOM元素,extraCssText里的样式会直接作用在这个DOM上,理论上能写很多高级样式,比如模糊背景、动画等,实测下来都支持。
3.2 富文本和自定义DOM结构的实现
当基础样式满足不了需求时,就要考虑返回一个更复杂的HTML结构了。我曾经做过一个大屏项目,需要tooltip里展示设备名称、运行状态、当前温度、告警等级,还要求弹窗像一个专业的状态卡片,这就需要构建一个完整的DOM结构返回。
tooltip: { trigger: 'item', confine: true, backgroundColor: 'transparent', formatter: function(params) { var data = params.data; var statusMap = { 'normal': ['正常运行', '#67C23A'], 'warning': ['告警中', '#E6A23C'], 'error': ['已停机', '#F56C6C'] }; var status = statusMap[data.status] || ['未知', '#909399']; return [ '<div style="min-width:200px;padding:12px 14px;background:linear-gradient(135deg,#1f2d3d,#2c3e50);border-radius:8px;color:#fff;">', ' <div style="font-size:14px;font-weight:bold;border-bottom:1px solid rgba(255,255,255,0.2);padding-bottom:8px;margin-bottom:8px;">', ' ' + params.name + ' 状态详情', ' </div>', ' <div style="font-size:12px;line-height:22px;">', ' 运行状态:<span style="color:' + status[1] + ';">' + status[0] + '</span><br/>', ' 当前温度:' + data.temperature + '℃<br/>', ' 设备编号:' + data.deviceId + '<br/>', ' 最近更新时间:' + data.updateTime, ' </div>', '</div>' ].join(''); } }这段代码返回了一个深色背景、带圆角和标题栏的卡片式弹窗,里面的每一个数据都来自原始data字段,状态显示还会根据数据动态变色。这种写法对设计稿还原度很高,基本上你在HTML里能画出来的样式,tooltip里都能做出来。
但这里有两个必须注意的地方。第一,backgroundColor要设为transparent,否则你自己设置的背景色会被默认的黑底盖住。第二,confine要设置为true,它表示tooltip限制在图表容器内显示,不然弹窗超出图表边界时会被截断或定位错乱。
3.3 富文本标签式的tooltip方案
ECharts还提供了rich富文本体系,主要用于label和tooltip内嵌样式的控制。它比直接返回HTML更规范,也更符合ECharts的渲染机制。不过说实话,遇到复杂设计稿时,我更喜欢直接用HTML拼接,因为CSS能做到的事情太多了,rich的flex布局能力比较弱,想做个复杂布局还得靠DOM来实现。
myEcharts的markdown显示的时候也支持rich,如果只是文字加粗、变色、调整字号,用rich反而是最轻量干净的方案。这两个方案建议根据场景灵活切换,不要拘泥于一种写法。
4. 典型场景实战:地图、大屏和移动端的tooltip处理
4.1 中国地图tooltip的三种隐藏玩法
中国地图的tooltip是很多人头疼的地方,因为地图组件里数据结构和普通图表不同。地图的data是一个对象数组,每个对象包含name(省份名)和value(数值),同时还可以带一些自定义字段。
geo: { map: 'china', roam: true }, series: [{ type: 'map', map: 'china', data: [ { name: '广东', value: 128, growth: '12.5%', rank: 1 }, { name: '江苏', value: 112, growth: '9.8%', rank: 2 } ] }], tooltip: { trigger: 'item', formatter: function(params) { var d = params.data; if (!d) return '数据缺失'; return params.name + '<br/>销售额:' + d.value + '亿<br/>增长率:' + d.growth + '<br/>排名:第' + d.rank + '名'; } }这里有个很细节的地方:地图tooltip的params.data在鼠标悬停到没有数据的地区时会是undefined,所以代码里要先做判断,否则会报错。另外地图上如果想加标注点(markPoint),tooltip的formatter要针对seriesType: 'map'和seriesType: 'scatter'分别写两套逻辑,因为scatter的params结构更接近散点图。
热词里提到的echarts 3d pie,也就是echarts-gl的3D饼图,tooltip的配置方式和普通饼图差异不大,但trigger必须用item。有一点要特别提醒,3D饼图的扇区没有实际的高度识别区域,鼠标悬停判定有时候不太灵敏,这是echarts-gl的通病,建议配合label做辅助展示。
4.2 大屏场景下tooltip的性能与定位问题
做数据可视化大屏时,tooltip最容易踩的坑有两个:一个是性能,一个是定位。大屏项目通常图表多、数据量大、更新频繁,鼠标在图表上来回移动时tooltip频繁触发,容易造成卡顿。
性能优化的思路有两个方向。一是开启confine,限制tooltip在容器内;二是给formatter方法做节流处理。比如你用回调函数去计算数据和拼接HTML,如果计算量比较大,可以在外部做一个简单的节流函数,减少触发频率。大屏上每秒60帧的图表渲染已经很吃性能了,tooltip里的复杂计算最好能省就省。
定位问题经常出现在弹窗靠近图表边缘的时候。默认tooltip会跟随鼠标移动,但如果弹窗内容较长、图表又靠近页面边缘,弹窗就容易超出屏幕或被容器截断。这时候除了设置confine: true,还可以配合position回调手动调整弹窗位置。
tooltip: { trigger: 'axis', position: function(point, params, dom, rect, size) { // point是鼠标位置,size是容器大小 // 当鼠标在右半边时,弹窗显示在左侧,避免超出边界 if (point[0] < size.viewSize[0] / 2) { return [point[0] + 20, point[1] - 20]; } return [point[0] - dom.offsetWidth - 20, point[1] - 20]; } }这个逻辑很好理解,鼠标在左半边时弹窗放右边,在右半边时弹窗放左边,保证弹窗始终在可视区域内。大屏项目中弹窗内容通常很多,我建议统一加上这种位置判断,别等到上线了才发现弹窗跑到屏幕外面去了。
4.3 移动端点击交互的tooltip处理
移动端没有鼠标hover,tooltip默认的mousemove触发方式在这类设备上失效了。ECharts处理移动端的方式是依赖touch事件,默认情况下手指点击图表时会把touch当成mousemove来处理,但用起来总感觉生硬。
更好的方案是用triggerOn: 'click'配合图表的dispatchAction来实现。比如我做过一个移动端报表页面,需求是点击折线图上的点时弹出该点数据详情。ECharts提供了dispatchAction方法,可以在图表外部触发点击事件,也可以主动控制tooltip的显示与隐藏。
chart.on('click', function(params) { chart.dispatchAction({ type: 'showTip', seriesIndex: params.seriesIndex, dataIndex: params.dataIndex }); });这种写法在移动端很实用,点击任意数据点就能精准弹出对应的tooltip。还没加完,还有一个细节,移动端弹窗不能太大,宽度最好限制在90vw以内,必要时配合confine来约束。
5. 常见问题与排查技巧实录
5.1 tooltip自动换行和长文本处理
tooltip内容太长挤在一行,显示效果会非常糟糕。ECharts官方没有提供直接的换行配置项,但我们可以从两个维度解决。
第一个维度是内容源头,formatter字符串模板里直接用<br/>手动换行,这个最可控。回调函数里也可以用\n配合textStyle里的lineHeight做换行,但HTML的<br/>是最稳定的。
第二个维度是样式处理,如果是数据特别长的文本,比如地图上省级单位的全称加数据,建议给弹窗设置固定宽度并让内容自动换行。可以借助extraCssText加max-width和word-break: break-all,实测下来效果不错。
tooltip: { trigger: 'item', extraCssText: 'max-width: 280px; white-space: normal; word-break: break-all;' }还有一种情况是数据量太大,比如折线图X轴有几百个刻度,提示框里会把每个刻度下的内容都显示出来,导致弹窗高度远超容器。这种情况建议在formatter里手动截断,只显示当前鼠标附近的数据。
5.2 Vue3中pxtorem对ECharts tooltip样式失效的问题
这个坑我印象太深了,公司后台项目用了postcss-pxtorem做移动端适配,所有px单位会自动转成rem。但ECharts的tooltip是运行时动态生成的内联样式,它不受postcss编译期转换的影响,于是出现了“配置的12px字体实际渲染还是12px,但页面上其他元素都变成rem了”的情况,字体大小不统一,布局看着很别扭。
根本原因在于:ECharts使用Canvas或SVG渲染图表主体,但tooltip的DOM节点是运行时动态append到容器里的,这些节点上的内联px样式不会被postcss处理。所以pxtorem对tooltip样式不生效是正常的,不是说你的配置写错了。
解决办法看项目需求。如果只想让tooltip字体跟随根字体大小变化,可以在初始化ECharts之前读取document.documentElement.style.fontSize,然后手动在formatter里拼上rem单位的字体。另一种方案是直接无视它,用px单位写死,因为tooltip本身是浮层,不参与页面流体布局,px在实际应用里问题不大。
5.3 tooltip不显示或显示位置错乱
tooltip完全不显示,优先排查三件事。第一,trigger配置是否正确,比如饼图用axis是永远不会触发的;第二,图表是否设置了dataset或数据为空,数据为空时tooltip自然没有内容可显示;第三,检查初始化后的DOM容器是否有overflow: hidden或者z-index过低,导致弹窗被遮挡。
位置错乱则要多考虑定位和滚动的情况。页面如果嵌在滚动容器里,而ECharts容器没有跟着滚动,tooltip的定位就会出现偏移。解决办法是在滚动事件里手动调用chart.getZr().refresh(),强制刷新组件。还有一种情况是初始化时容器是隐藏的(比如tab切换场景),ECharts计算宽度为0,tooltip会偏到角落,这种时候需要在容器显示后调用chart.resize()。
5.4 tooltip定制开发时遇到的杂项问题
分享几个零散的坑。第一个是tooltip的背景色是半透明白色时,弹窗里的文字颜色最好重新指定,否则在深色底图上会看不清。第二个是多个图表复用一个tooltip时,appendToBody这个配置项需要谨慎使用,它会改变弹窗的DOM层级,在个别浏览器里会出现弹窗在页面最上层但点击穿透的情况。
第三个是关于折线图X轴刻度显示不全的问题,这个以及热词里提到的echarts折线图x轴刻度是另一个话题,但和tooltip也有关联。刻度多时默认会自己跳着显示,这时tooltip用axis触发反而能弥补刻度显示不全的缺点,用户虽然看不清每个刻度对应什么,但鼠标悬停上去就能看到精确值,这也是ECharts常见的数据阅读方案。
还有个小技巧:当tooltip需要展示的数据过多时,可以考虑在formatter里返回一个带滚动条的容器。比如设置了max-height: 300px; overflow-y: auto;的div,这就既能展示全部数据又不占屏幕空间,实测在数据量大时非常实用。
我个人在项目里一般会封装一个统一的tooltip样式函数,把上面这些经验沉淀下来:底色、圆角、阴影、字体大小、换行规则、位置偏移逻辑全部统一定义,所有图表复用同一个配置。这样不仅统一了视觉风格,也把踩坑的解决方案固化到了代码里。后续有新人接入,直接用这个封装就能少走不少弯路。