
做数据可视化大屏的朋友应该都有体会当业务数据按省份分布展示时地图一定是优先级最高的选择。而 ECharts 里做省一级的地图最让人头疼的往往不是画地图本身而是弹出来的 tooltip 永远排版稀烂默认的 “省份: 数值” 又丑又不够用想加点自定义指标、加个环比、换个颜色全都得跟 formatter 死磕。这篇博文就围绕 echarts 省地图例子和 tooltip 自定义提示框展开我会先讲清楚省地图的数据准备和注册逻辑再把 tooltip 的几种自定义方式逐个拆开最后给一个完整的实战例子和常见的排查清单。无论你是刚接触 echarts 的新手还是已经在做公司大屏的中级前端这篇文章都能帮你少踩几个坑。1. 省地图可视化为什么值得认真做1.1 这个例子的核心价值先想一个问题同样是展示各省业务数据为什么用柱状图、饼图都不如地图来得直观因为地图天然利用了人的空间认知能力。你说“华东区销量最高”看表格要一行行找看地图一眼就能定位到颜色最深的那块区域。省地图做的是更细一层的事情把全国地图聚焦到某一个省或者把某个省份的市级数据摊开来看能够承载的信息密度更高商业汇报场景里也更有表现力。tooltip 则是这种可视化方案里最容易出彩也最容易翻车的环节。很多人以为 tooltip 就是鼠标悬浮弹出一行字但实际上它是用户和图表数据的第一个交互触点。默认的 tooltip 只显示系列名、数据名和值当你想展示多个指标、带有单位的数值、同比环比甚至插入一个小表格的时候就必须掌握 formatter 的写法。这篇博文里我会把字符串模板、回调函数、富文本三种实现方式都拆开讲配合一个实际的省地图例子让你能直接抄作业。1.2 技术方案与选型思路为什么选择 ECharts 做省地图而不是 Leaflet、Mapbox 这类 GIS 框架这是个很实际的选型问题。Leaflet 和 Mapbox 的核心能力是地图渲染、经纬度投影、图层叠加它们更接近一个“地图引擎”而 ECharts 的核心能力是数据可视化地图只是它众多图表类型中的一种。如果你的需求是“在地图上打点、画路线、做动态轨迹”那应该考虑 GIS 框架但如果需求是“把各省销量映射为颜色深浅鼠标移上去看明细”ECharts 绝对是成本最低、上手最快的方案。另外要理解 ECharts 地图的底层逻辑它本身不加载任何地图瓦片而是把 GeoJSON 这种地理数据当作普通的几何图形来绘制。也就是说你要展示哪个省就得先把那个省的 GeoJSON 数据拿到手通过registerMap注册进去然后才能用series-map去渲染。这一步理解清楚后面遇到的很多“地图不显示”的问题就都能找到方向了。2. 地图数据的准备与注册2.1 GeoJSON 从哪里来做 ECharts 地图第一个绕不开的问题就是 GeoJSON 数据。我最早做的时候去网上找过各种资源包质量参差不齐有的边界线残缺有的坐标偏移严重后来固定用 DataV.GeoAtlas 提供的地图数据接口稳定性和数据精度都不错。它的 URL 规律比较清晰比如河南省的边界数据是https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json其中410000是河南省的行政区划代码_full表示包含子级区域市级的完整数据。如果你想做省级地图并显示市级数据这个接口直接返回的就是全省所有地级市的边界集合非常方便。如果只是想画一个单独的省轮廓也可以不带_full后缀但实际项目中带子级数据的需求更常见所以一般都直接用_full。获取 JS 层面可以直接用fetch或者axios请求也可以通过 script 标签引进来后面会给出完整写法。这里要提醒一句行政区划是会调整的比如某些地方改名、合并、升级地图数据也会跟着更新。如果你发现某个市在 GeoJSON 里找不到先别急着怀疑代码检查一下数据文件的更新时间是不是早于行政区划调整的时间。DataV 这个接口的数据一直在更新一般不会出大问题。2.2 注册地图的两种方式与关键细节拿到 GeoJSON下一步就是注册进 ECharts核心 API 是echarts.registerMap。有两种常见的用法第一种直接全局注册。适合数据已经通过异步请求拿到手的场景fetch(https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json) .then(res res.json()) .then(geoJson { echarts.registerMap(henan, geoJson); // 注册完成后再初始化图表并 setOption initChart(); });第二种把 GeoJSON 下载到本地直接在 HTML 或 JS 文件里引入。适合数据量不大、不想依赖网络请求的场景script src./maps/henan.json/script script // 如果 henan.json 是标准的 GeoJSON 对象直接用即可 echarts.registerMap(henan, henanJson); /script这里有个非常容易踩的坑registerMap的调用时机必须在setOption使用该地图之前而且如果地图数据是通过异步请求加载的一定要在请求回来之后再执行 initChart。很多人图表初始化写在页面顶部地图数据还没加载完结果页面上只看到一片空白控制台报错也看不明白其实就是时序问题。实际开发里我习惯封装一个loadMapData方法把注册和初始化放到 then 里面确保顺序不会乱。2.3 让数据与地图对上号地图注册好之后还要解决一个关键问题series 里的数据如何和地图里的每个区域对应ECharts 的规则很简单series 数据中的name字段会和 GeoJSON 中每个区域的name属性也就是properties.name做精确匹配。比如 GeoJSON 里河南省的区域名是“河南”你的数据里就必须写“河南”写成“河南省”哪怕只差一个字也不会显示。所以做省地图之前建议先打开 GeoJSON 看一眼里面的properties.name确认到底有没有带“省”“市”“自治区”这些后缀。如果数据源和地图数据名称对不上有两种处理方式一是改自己的数据二是用nameMap做映射。实际项目中经常遇到接口返回的是行政区划代码、地图里是中文名的情况这时可以自己写一个转换函数把代码映射成中文名再塞进 series。例如const codeMap { 410100: 郑州, 410200: 开封, // ... }; const data rawData.map(item ({ name: codeMap[item.code], value: item.value }));这一步虽然简单但千万不能省。地图渲染不报错、却没有任何数据上色十有八九就是名称没对上。3. tooltip 自定义提示框的工作原理3.1 默认 tooltip 的组成ECharts 的 tooltip 在鼠标悬浮到图形上时触发默认显示的内容包含系列名seriesName、数据名name和数值value。对地图来说series 的 name 通常是地图系列名数据名就是省份或城市名称数值就是 data.value。默认格式类似“河南1234”看起来能用但信息量太少样式也无法定制。tooltip 的自定义能力集中在formatter上有两种主要写法字符串模板和回调函数。搞清楚这两种写法各自的优缺点才能按场景选择。另外还要知道trigger这个配置项是item还是axis。省地图场景一般用item表示悬停到哪个区域就触发哪个区域的提示如果用柱状图、折线图这种直角坐标系图表axis会更顺手——鼠标在某个 x 轴刻度区域移动时把该刻度下的所有系列数据都展示出来。3.2 字符串模板法简单场景快速上手字符串模板最适合内容比较单一的提示框。ECharts 在模板字符串里内置了一批占位符常用的有{a}系列名即 series.name{b}数据名对地图来说就是省份名称{c}数值即 data.value{d}百分比饼图、环形图场景下才有意义一个最基础的模板可以写成tooltip: { trigger: item, formatter: {b}br/销量{c} 件 }这里有个细节字符串模板里的br/才是真正的换行符写在 JS 字符串里如果直接用\n在 tooltip 的 HTML 渲染里并不会生效会挤成一行。我最早做的时候在这里吃过亏后来统一习惯性写成br/。字符串模板更适合快速出效果但它的能力边界很明显没法做条件判断没法对不同指标设置不同颜色也没法嵌套 HTML 结构。一旦提示框需要展示多行、多指标就得上回调函数。3.3 回调函数法最灵活的自定义方式回调函数是formatter里最常用的方式它接收一个params参数返回一个字符串或 HTML 字符串。地图场景下params里常用的字段有formatter(params) { console.log(params); return ${params.name}br/数值${params.value}; }params对象常见字段包括componentType固定为 series、seriesType、seriesName、name区域名、data该区域的原始数据项、value数值地图场景可能是 number 也可以是数组取决于你 data 怎么写的等。调试时可以在 formatter 里先console.log(params)打出来看一遍比自己瞎猜字段名高效得多。回调函数里可以做的操作非常自由。比如你想判断数值大小显示不同文案formatter(params) { const value params.value; let level 偏低; if (value 1000) level 优秀; else if (value 500) level 良好; return ${params.name}br/销售额${value} 万元br/评级${level}; }需要注意一个细节当 series 的 data 里的 value 是数组时params.value也是一个数组这时候要用params.value[0]、params.value[1]这种下标去取。如果不确定数据格式建议打印看一眼再取值千万不要上来就写params.value.toFixed(2)一旦 value 是数组就会直接报错导致 tooltip 显示不出来。3.4 富文本样式与自动换行控制tooltip 返回的字符串可以直接包含span style...这类行内 HTML这是最省事的自定义样式手段。但 ECharts 对 tooltip 内容的样式解析有一套自己的规则它内部的 tooltip 内容层会开启rich能力你可以用 ECharts 的富文本语法来做更精细的排版。先看一个用行内 HTML 实现的例子这也是我日常项目里最常用的方案tooltip: { trigger: item, backgroundColor: rgba(10, 20, 40, 0.9), borderColor: #2e6bff, textStyle: { color: #fff, fontSize: 13 }, formatter(params) { const value Number(params.value); return div stylepadding: 4px 6px; div stylefont-size: 15px; font-weight: bold; color: #ffd666; ${params.name} /div div stylemargin-top: 6px; font-size: 13px; span stylecolor: #8c9bb5;销售额/span span stylecolor: #4deaa6; font-weight: bold;${value} 万元/span /div div stylemargin-top: 4px; font-size: 13px; span stylecolor: #8c9bb5;同比/span span stylecolor: ${value 0 ? #ff6b6b : #4deaa6};${value 0 ? : }${value}%/span /div /div ; } }这种写法本质上就是拼 HTML样式直接写在style里浏览器渲染时怎么显示tooltip 里就怎么显示几乎没有学习成本也很好维护。再讲自动换行。很多人在 tooltip 里写了很长的指标名结果一行太长撑破提示框。处理方式有两种一种是在合适的位置手动插入br/强制换行另一种是利用 CSS 的word-break或max-width让内容自动换行。行内 HTML 里给最外层 div 设置max-width: 240px; word-break: break-all;是一个很实用的兜底方案能避免长数字或长英文把 tooltip 撑出屏幕。至于 ECharts 富文本的rich模式它能做效果更炫的排版比如用{name|河南}这种方式定义样式块。但说实话项目里的 tooltip 定制用行内 HTML 就覆盖了九成场景rich更适合用来做 label 的曲线标注或者特别复杂的图例排版。我会在后面的完整例子里给一个简单的 rich 写法但不会强制大家用毕竟可维护性也很重要。4. 完整可运行的省地图例子理论讲了不少下面直接上一个能跑起来的完整例子。以河南省为例展示各地级市的销售数据地图上标注省会郑州鼠标悬浮到每个市显示自定义 tooltip。这个例子可以直接复制到本地运行也可以作为你自己项目的基础模板。4.1 搭一个最小 HTML 页面先写最基础的页面结构包含 ECharts 的 CDN 引入和图表容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title河南省级地图示例/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style #mapChart { width: 800px; height: 600px; margin: 40px auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } /style /head body div idmapChart/div script // 后续代码都写在这里 /script /body /html容器必须有明确的宽高这是 ECharts 初始化的硬性要求。实际项目中如果容器宽度是百分比要注意父级也需要有高度不然图表初始化后会发现画布高度为 0页面上一片空白。4.2 配置省地图的 series 与视觉映射接着写地图核心逻辑。这里我直接用fetch加载 GeoJSON加载完成后注册地图再初始化图表const chartDom document.getElementById(mapChart); let myChart null; fetch(https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json) .then(res res.json()) .then(geoJson { echarts.registerMap(henan, geoJson); myChart echarts.init(chartDom); myChart.setOption(buildOption()); }); function buildOption() { return { title: { text: 河南省各市销售额分布, left: center, textStyle: { fontSize: 18, color: #333 } }, tooltip: { trigger: item, formatter: function(params) { return params.name br/销售额 params.value 万元; } }, visualMap: { min: 0, max: 5000, left: 20, bottom: 20, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [{ name: 销售额, type: map, map: henan, roam: true, label: { show: true, color: #333, fontSize: 10 }, itemStyle: { borderColor: #fff, borderWidth: 1, areaColor: #f0f4fa }, emphasis: { label: { show: true, fontWeight: bold }, itemStyle: { areaColor: #ffd666 } }, data: [ { name: 郑州, value: 4821 }, { name: 开封, value: 1236 }, { name: 洛阳, value: 3654 }, { name: 平顶山, value: 982 }, { name: 安阳, value: 1130 }, { name: 鹤壁, value: 421 }, { name: 新乡, value: 1576 }, { name: 焦作, value: 1328 }, { name: 濮阳, value: 876 }, { name: 许昌, value: 1452 }, { name: 漯河, value: 765 }, { name: 三门峡, value: 657 }, { name: 南阳, value: 2673 }, { name: 商丘, value: 1198 }, { name: 信阳, value: 1543 }, { name: 周口, value: 1341 }, { name: 驻马店, value: 1087 }, { name: 济源, value: 236 } ] }] }; }这段代码里的visualMap负责把数值映射成颜色min和max决定了颜色映射的范围。如果数据里最大值超过max超出部分会按最大值渲染如果所有数据都很小但max设得很大色差就不明显。实际项目里我一般会先对数据做一遍排序找出最大值再设置max为比最大值略大的整数让颜色分布更有层次感。4.3 增加 markPoint 标注省会省地图里经常要把省会或重点城市单独标出来ECharts 里用markPoint实现。它可以在 series 里直接配置也可以作为独立的series使用。在地图上标注城市位置最常见的方式是使用coord指定经纬度比如郑州的经纬度大约是[113.62, 34.75]series: [{ // ... 地图系列配置 markPoint: { symbol: pin, symbolSize: 50, label: { show: true, formatter: 郑州, color: #fff, fontSize: 12 }, data: [ { name: 郑州, coord: [113.62, 34.75], value: 4821 } ] } }]symbol设置为pin就是一个图钉效果视觉上很像地图 App 的气泡。symbolSize控制大小coord必须是数组形式的经纬度。如果你的 GeoJSON 坐标系不是标准的 WGS84可能会发生标点偏移这时候要确认数据源是否正确。用 DataV 的 GeoJSON 配合常见的 GPS 坐标基本不会出现偏移问题。markPoint也支持直接用name去匹配地理区域而不填坐标例如data: [{ name: 郑州 }]ECharts 会尝试通过地图数据里区域的中心点来自动定位。不过这种方式的定位精度看数据质量有时候中心点会偏离实际城市位置所以我更推荐显式指定coord可控性更高。4.4 联动自定义 tooltip 的完整配置把自定义 tooltip 和地图数据联动是这篇文章的核心。我在实际项目里经常要在一个城市 tooltip 里展示好几个维度的数据比如销售额、订单量、用户数、同比变化代码会写成这样tooltip: { trigger: item, backgroundColor: rgba(13, 29, 58, 0.92), borderColor: #2e6bff, borderWidth: 1, padding: [12, 16, 12, 16], textStyle: { color: #dfe6f2, fontSize: 13 }, formatter(params) { if (!params || !params.data) return ; const value params.data.value; const order Math.round(value * 0.36); const user Math.round(value * 0.18); const rate (value / 5000 * 100).toFixed(1); const trend value 1500 ? up : down; const trendColor trend up ? #ff6b6b : #4deaa6; const trendText trend up ? ↑ : ↓; return div stylemin-width: 180px; div stylefont-size: 15px; font-weight: 700; color: #ffd666; padding-bottom: 6px; ${params.name} /div div styledisplay: flex; justify-content: space-between; padding: 3px 0; span stylecolor: #8c9bb5;销售额/span span stylecolor: #fff; font-weight: 600;${value} 万元/span /div div styledisplay: flex; justify-content: space-between; padding: 3px 0; span stylecolor: #8c9bb5;订单量/span span stylecolor: #fff;${order} 单/span /div div styledisplay: flex; justify-content: space-between; padding: 3px 0; span stylecolor: #8c9bb5;用户数/span span stylecolor: #fff;${user} 人/span /div div styledisplay: flex; justify-content: space-between; padding: 3px 0; span stylecolor: #8c9bb5;目标完成率/span span stylecolor: #4deaa6; font-weight: 600;${rate}%/span /div div styledisplay: flex; justify-content: space-between; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.2); margin-top: 6px; span stylecolor: #8c9bb5;同比/span span stylecolor: ${trendColor}; font-weight: 600;${trendText} ${Math.abs(value 1500 ? 28 : 15)}%/span /div /div ; } }这里有几个关键细节值得说。第一params.value可能是 number 类型也可能是数组类型如果数据里配置了value: [123, 456]这种就必须要按数组取。我在formatter开头先做了空值校验返回空字符串避免可能出现的报错导致 tooltip 弹不出来。第二tooltip 返回的是一个 HTML 字符串CSS 样式直接内联这种写法对新手最友好渲染结果所见即所得。第三backgroundColor、borderColor、padding这些 tooltip 容器样式可以直接在配置项里设置不用另外写 CSS。实际跑一下这个例子鼠标移到“郑州”上会弹出一个深色背景的卡片里面有多行指标和分隔线比默认 tooltip 高大上不少。5. 常见问题与排查技巧实录写 ECharts 地图和 tooltip 的过程里我踩过的坑不少下面这些问题基本覆盖了最常见的报障场景按“现象—原因—解法”列出来方便大家遇到问题时快速排查。5.1 地图空白不显示地图区域整个不渲染最常见的原因是 GeoJSON 没有注册成功。排查顺序可以参考这张表现象可能原因排查方式页面空白且控制台报错registerMap 名称和 series.map 不一致检查两个地方是否完全一致页面空白无报错GeoJSON 还没加载完就 setOption在 fetch.then 里初始化图表地图轮廓出来了但所有区域都是一种颜色数据 name 和 GeoJSON 区域名不匹配打印 GeoJSON 的 properties.name 和 data 的 name 对比地图显示但城市名全部错位GeoJSON 数据异常换一个数据源重新测试最坑的是第二种接口请求慢的时候地图空白请求快的时候又能显示这种偶发问题很迷惑人根源就是异步时序。稳妥的做法是把初始化动作统一放到数据加载完成的回调里避免任何竞态。5.2 tooltip 不出现或内容错乱tooltip 完全不出现检查tooltip对象是否已经配置以及trigger是否设成了item。地图系列的tooltip.trigger用axis是基本不会触发的因为轴的概念属于直角坐标系。tooltip 内容错乱通常就是 formatter 里的变量取错了。常见的坑包括params.value是数组直接用toFixed报错。数据项为 null 或 undefinedformatter 没做保护。在 formatter 里拼 HTML 时变量包含特殊字符导致结构破坏。这些坑大多可以通过在 formatter 开头加一行console.log(params)快速定位。把参数打出来看一遍字段名就不会猜错了。另外一个建议是formatter 里尽量做一层防御判断params params.data存在再继续返回不了内容就返回空字符串不能让 tooltip 直接报错。5.3 自适应与 pxtorem 的坑做可视化大屏的人应该都熟悉 rem 适配方案很多项目会引入postcss-pxtorem之类的插件把 CSS 里的 px 自动转成 rem。但这类方案对 ECharts 基本没用因为 ECharts 是在 canvas 上绘制的它的宽度、高度、字号全部是通过 JavaScript 设置不会经过 postcss 的 CSS 处理流程。所以你会发现页面里的文字都跟着 rem 缩放了但图表里的字体大小纹丝不动。这是目前“pxtorem 对 echarts 没起到效果”这类问题多的根本原因。处理方式分两种如果只是想保证图表适合不同屏幕不要依赖 rem而是监听window.resize事件调用chart.resize()让图表容器跟着 CSS 宽度走。如果想要整体缩放的可以在初始化前根据屏幕宽度计算出缩放系数再动态传入textStyle.fontSize等配置。不要指望 pxtorem 能自动处理 canvas 内部的尺寸。我的建议是大屏项目里ECharts 容器的尺寸用 CSS 的百分比加vw/vh来控制图表内部的字体和间距通过 JS 统一按设计稿比例计算一次然后监听 resize 重新调用chart.resize()。这样既适配了不同分辨率也不会被 rem 适配方案干扰。5.4 在 Vue 3 中使用省地图的要点Vue 3 项目里使用省地图和原生写法逻辑一样但要注意组合式 API 的生命周期。核心代码大概是这样template div refchartRef stylewidth: 100%; height: 600px;/div /template script setup import { ref, onMounted, onBeforeUnmount } from vue; import * as echarts from echarts; const chartRef ref(null); let chart null; onMounted(async () { const res await fetch(https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json); const geoJson await res.json(); echarts.registerMap(henan, geoJson); chart echarts.init(chartRef.value); chart.setOption({ /* 配置项同上 */ }); window.addEventListener(resize, handleResize); }); function handleResize() { chart chart.resize(); } onBeforeUnmount(() { window.removeEventListener(resize, handleResize); chart chart.dispose(); }); /script这里有几个容易漏掉的点第一echarts按需引入还是全量引入项目打包体积敏感的可以用echarts/core按需注册地图组件。第二组件卸载时一定要调用dispose否则会有内存泄漏的风险尤其是在大屏页面反复切换标签页时问题会更明显。第三如果项目里使用 TypeScript地图系列的类型需要用MapSeriesOption来声明不然类型检查会报错。Vue 里数据变化后更新图表不要去频繁setOption整份配置而是用chart.setOption({ series: [{ data: newData }] })做增量更新性能会好很多。6. 关于 tooltip 定制的一些个人经验做地图可视化几年下来tooltip 这块我的体会是不要一上来就追求复杂炫酷的富文本排版先把数据内容和交互逻辑梳理清楚再考虑怎么美化。在拿到业务数据后我先列一个清单这条数据在 tooltip 里要展示哪几个维度数值单位是什么需不需要显示占比或者环比要不要根据数值大小给不同颜色这些问题想清楚再去写 formatter 会快很多。很多 tooltip 做得难看的项目问题不在样式而在于展示维度混乱用户扫一眼不知道重点是什么。另外一个很实用的经验是把 tooltip 的 formatter 抽成独立的纯函数不要塞在 option 里写一大坨。因为 tooltip 格式化逻辑其实和业务强相关独立出来既方便单测也方便在多个图表之间复用。比如我经常写一个formatMapTooltip(params, extraConfig)函数不同地图页面传不同的指标配置代码干净很多。如果你在开发中需要更多高级效果比如 tooltip 里嵌套图表、tooltip 跟随鼠标做动画、tooltip 里加操作按钮这些通过返回 HTML 字符串都能实现只是要额外处理事件绑定。常规的数据展示需求上面说的字符串模板和回调函数已经足够覆盖了。最后想说一句地图可视化的本质是把空间数据变成人能一眼看懂的信息tooltip 是其中最重要的信息解释层。把 GeoJSON 注册、数据对齐、tooltip 定制这三件事做好一个能交付给业务方的大屏地图功能就成了七八分。剩下的优化基本都是在细节上打磨比如视觉映射的分档、标注点的选择、自适应策略的完善。希望这篇文章里的例子和经验能帮你在实际项目里少折腾几个晚上。