1. 项目背景与整体思路拆解
1.1 为什么我选了 ECharts 而不是 Leaflet 或 Mapbox
我这些年做过不少数据可视化大屏项目,凡是涉及中国地图的场景,ECharts 基本是我默认的第一选择,原因很直接:它和普通业务图表是同一套技术栈,不需要我为了一个地图去单独引入一套 GIS 体系。Leaflet 强在交互和图层管理,Mapbox 强在地图底图的美观和 3D 场景,但它们都需要处理坐标投影、瓦片加载、图层叠加这些额外概念,如果只是想把一组业务数据按省、市分布展示出来,学习成本和维护成本都偏高。
ECharts 的地图组件走的是“GeoJSON 数据驱动 + series 配置”的路线。我只需要准备好一份区域边界数据,然后用 series-map 或 series-map3D 的配置项就能把数据映射到对应的区域上,这种模式和做柱状图、饼图的思维方式一致,团队成员接手也快。另一个让我长期选它的理由是社区生态成熟:百度地图、高德地图的边界数据可以直接转成 GeoJSON 用,网上有大量现成配置可以抄作业,遇到问题基本一搜就有答案。
当然它也有短板,比如对海量高精度地理数据的渲染能力不如专业 GIS 引擎,但这在我的项目里几乎不算问题。政务大屏、运营分析、企业报表这类场景,数据粒度最多到省份和城市,ECharts 完全扛得住。
1.2 2D 地图和 3D 地图的本质差异
先说结论:2D 地图和 3D 地图在数据层、注册层上没有区别,区别主要在渲染层和交互层。
2D 中国地图用的是 ECharts 内置的地图组件,本质是投影平面上的 Polygon 绘制。每个省份是一个多边形区域,视觉上靠填充色、描边、标签来区分,用户可进行的操作也就是 hover、点击、缩放和平移。它的优点是轻量、兼容性好、性能开销小,哪怕数据量再大也基本不会卡,适合传统报表和后台管理系统中对展示效果要求不高的场景。
3D 地图则依赖 echarts-gl 扩展库,地图底图被投射到一个三维空间平面上,然后通过光照、高度、视角旋转来制造立体感。用户可以拖拽旋转地图,从不同角度观察数据。我在实际项目中最常用的玩法是给不同省份设置一个“高度”属性,让数据值映射成柱体高度,或者在 3D 地图上叠加 3D 柱状图,视觉冲击力比 2D 强很多,特别适合展厅大屏和汇报演示。
但 3D 也不是没有代价:echarts-gl 包体积更大,渲染引擎对部分低端设备不友好,如果同时叠加大量 3D 图形还会出现帧率下降。所以在项目初期,我都会先问清楚这个地图是用在后台管理系统还是对外展示大屏——前者我基本只做 2D,后者我才会考虑上 3D。一句话总结:2D 保证信息清晰,3D 保证视觉震撼,选哪个取决于场景需要,而不是哪个更高级。
2. 环境准备与地图数据获取
2.1 依赖安装与版本选型
ECharts 本身一直在迭代,echarts-gl 的更新节奏却没有那么快,所以版本匹配是一个需要特别注意的点。我用过的组合里,echarts@5.x配合echarts-gl@2.x是最稳的,直接 npm 安装就行:
npm install echarts@5 echarts-gl@2如果用 Vue 2,我会把 echarts 挂到原型上,避免每个组件重复 import:
// main.js import * as echarts from 'echarts' import 'echarts-gl' Vue.prototype.$echarts = echarts如果用 Vue 3,我一般封装一个useECharts的 composable,把 init、setOption、resize、dispose 的逻辑统一管理起来,这样在多个组件里复用地图时不会出现实例泄漏的问题。
还有一个实用建议:如果项目只需要地图功能,不需要折线图、饼图这些,可以通过echarts/core按需引入,能明显减小打包体积。不过 echarts-gl 目前对按需引入支持不是特别友好,我实际测试下来,直接全量引入反而更省心,省得折腾各种 tree-shaking 的坑。
2.2 GeoJSON 数据获取与处理
地图注册最关键的一步是拿到一份合法、完整的中国地图 GeoJSON 数据。这里我多说一句,ECharts 从 4.x 开始就不再内置地图数据了,必须自己注册。所以项目里第一步永远是搞数据源。
我常用的数据获取方式有这几种:
- 阿里云 DataV.GeoAtlas 提供各省市县的 GeoJSON 下载接口,直接通过 URL 按编码取数据,非常方便。比如获取全国地图数据的接口格式是:
const url = 'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json' fetch(url) .then(res => res.json()) .then(geoJson => { echarts.registerMap('china', geoJson) }) - ECharts 官方示例里的地图数据也基本来自这里,社区里很多人直接下载到本地,然后放到 static 目录下,避免每次打开页面都发一次跨域请求。
- 如果只是做开发调试,直接把 json 文件放到项目里 import 也是可以的,打包时注意体积。
拿到 GeoJSON 之后,建议先了解它的基本结构:它本质上是一个 FeatureCollection,里面每个 Feature 对应一个区域,包含properties.name作为区域名,geometry负责记录边界坐标。我在项目里经常需要对这份数据进行二次加工,比如只保留大陆部分、合并某些区域、给每个区域附加额外的业务属性等,这些操作都是用 JavaScript 对数组做 filter 和 map 就能完成的,不需要额外引入 GIS 工具库。
2.3 地图注册:registerMap 到底在做什么
很多人第一次接触 ECharts 地图时,都会困惑一个问题:为什么明明配置了 series-map,图表就是空白?答案多半是忘了注册。
import * as echarts from 'echarts' fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json') .then(res => res.json()) .then(geoJson => { // 注册一个名为 'myChina' 的地图 echarts.registerMap('myChina', geoJson) // 后续 option 里的 map 属性就填 'myChina' myChart.setOption(option) })registerMap做的事情,可以理解成把一份 GeoJSON 数据解析并转换成了 ECharts 内部可以高效渲染的图形结构,同时建立了一个“区域名 -> 坐标范围”的索引。后面series-map的data数组里,每一项的name就是通过这个索引去匹配对应区域的。这里有个关键点:data 里写的 name 必须和 GeoJSON 里 properties.name 完全一致,不能多一个空格,不能简写,否则该区域的数据就映射不上。我早期做项目时,曾经因为数据里写的是“广西”,GeoJSON 里是“广西壮族自治区”,结果整个区域显示成了默认灰色,排查了半天才找到原因。
3. 2D 中国地图核心实现
3.1 最基础的中国地图配置
注册好地图之后,写一个最简单的 2D 中国地图只需要一个 series 配置:
option = { series: [{ type: 'map', map: 'myChina', roam: true, label: { show: true, fontSize: 10 }, itemStyle: { areaColor: '#aaccff', borderColor: '#fff', borderWidth: 1 }, emphasis: { label: { show: true, color: '#333' }, itemStyle: { areaColor: '#ffaa00' } } }] }这段配置里,roam: true允许用户拖拽平移和缩放,这个交互能力通常需要开启,因为大屏上展示全国数据时,用户希望放大看某个区域的细节。label.show控制是否显示区域名称,如果省份太多,我会关掉 label,等 hover 的时候再通过 tooltip 展示,否则屏幕上全是字,视觉噪点太多。
关于geo组件和series-map的选用,我的经验是:如果要在地图上再叠加散点图、涟漪图、飞行线等复杂效果,优先使用geo作为底图,然后用series携带coordinateSystem: 'geo'去绘制叠加层;如果只是单纯展示区域填色数据,直接一个series-map就搞定了,配置更简洁,visualMap联动也更稳定。两者不能同时用同一个地图区域配置,否则会出现图层覆盖、事件绑定的混乱,我踩过这个坑。
3.2 visualMap 数据映射配置
数据地图的核心功能就是把数值映射成颜色。ECharts 中最常用的方案是visualMap组件:
option = { visualMap: { type: 'piecewise', // 分段型 min: 0, max: 1000, left: 20, bottom: 20, text: ['高', '低'], calculable: true, inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } }, series: [ { type: 'map', map: 'myChina', data: [ { name: '北京', value: 320 }, { name: '上海', value: 540 }, // ... ] } ] }visualMap有两种类型:continuous连续型和piecewise分段型。在大屏上我更喜欢用piecewise,因为它可以直接把数据分成“低、中、高”几个档位,观众一眼能看懂业务含义;continuous则更精确,适合做数据分析工具。选哪个没有绝对标准,关键看用户是想看整体分布,还是想看精确数值。
这里有三个容易出问题的地方。第一,visualMap的min和max如果没有设置,ECharts 会自动根据数据取整,但遇到极端值会导致大部分区域颜色一样,看不出差异。我一般会显式设置,或者用一个比较合理的固定区间,比如 0 到 1000。第二,calculable: true会在渐变条上出现拖动手柄,用户可以直接筛选区间,但这个功能在触屏大屏上有时会误触,我通常会在正式环境关掉它。第三,如果visualMap在series里设置了selectedMode: false,数据点击时不会高亮,这在展示型大屏上反而更干净,用户只会看到 hover 效果。
3.3 标签、提示框与样式细节
2D 地图的细节往往决定了最终效果是“能用”还是“很好看”。我最常用的几个细节优化点:
tooltip 换行问题。ECharts 默认的 tooltip 在文本长度超出容器宽度时会自动换行,但有时在单元格模板里写了很长的 HTML,默认换行会变得混乱。我一般用formatter手动控制:
tooltip: { trigger: 'item', formatter: function (params) { return [ '<div style="font-weight:bold;margin-bottom:4px;">' + params.name + '</div>', '数值:<span style="color:#ffaa00;">' + params.value + '</span>', '<div style="margin-top:4px;color:#999;">点击可下钻查看</div>' ].join('') } }formatter返回 HTML 字符串时,内部使用<br/>或分段<div>都可以实现换行,比依赖默认行为要可控得多。
label 的显示策略。全国地图省份多,如果每个省都显示名称,页面会显得很拥挤。我通常设置为label: { show: true, fontSize: 10 },在 PC 端够用;如果是分辨率低的大屏设备,我会把字号调成 12 以上,或者用labelLayout做避让处理。ECharts 5 的labelLayout可以指定hideOverlap: true,自动隐藏重叠的标签,实测效果好很多。
itemStyle 的描边与高亮。地图区域之间的边界线是否清晰,直接影响地图的辨识度。我会给itemStyle.borderColor设置一个接近底色的深色,borderWidth设为 1,这样既能看到边界,也不会喧宾夺主。高亮色我用emphasis.itemStyle.areaColor单独设置,配合shadowBlur加一点外发光效果,视觉反馈更明显。
3.4 在地图上叠加散点和涟漪效果
只有填色地图有时候信息量不够,我经常需要把重点城市、重要站点用散点标出来。做法是先配置一个geo底图,再叠加effectScatter系列:
option = { geo: { map: 'myChina', roam: true, itemStyle: { areaColor: '#1a2a4a', borderColor: '#3e6b9e' } }, series: [ { name: '重点城市', type: 'effectScatter', coordinateSystem: 'geo', data: [ { name: '北京', value: [116.405285, 39.904989, 100] }, { name: '上海', value: [121.472644, 31.231706, 80] } ], symbolSize: 12, rippleEffect: { scale: 4 }, label: { show: true, formatter: '{b}', position: 'right' } } ] }这些坐标数据用的是经纬度,ECharts 会在地图投影坐标系中自动转换。这里有一个容易忽略的点:value数组的前两位必须是[经度, 纬度],第三位用于symbolSize映射。如果数据是从后端接口拿到的,我一般会在前端把经纬度和数值拼成这种格式再传给 series。
4. 3D 中国地图实现
4.1 引入 echarts-gl 的基本姿势
3D 地图的基础是echarts-gl,需要在引入 echarts 之后再引入它:
import * as echarts from 'echarts' import 'echarts-gl'如果是 CDN 方式,echarts-gl的 JS 文件要在echarts.min.js之后加载。引入完成之后,就能在 option 里使用map3D系列或者geo3D组件了。我最早在这块踩过的坑是版本不匹配:echarts 升到 5 之后,如果还用旧版的echarts-gl,控制台会直接报警告,地图区域渲染不出来。后来固定用echarts-gl@2,和echarts@5配合,再没出过问题。
4.2 map3D 基础配置与视角控制
一个最简单的 3D 中国地图配置长这样:
option = { tooltip: {}, visualMap: { min: 0, max: 1000, calculable: true, inRange: { color: ['#4a6b9a', '#3dd1dc', '#30d6a0'] } }, series: [ { type: 'map3D', map: 'myChina', shading: 'realistic', boxWidth: 200, boxHeight: 20, regionHeight: 3, environment: '#1a1a2e', itemStyle: { color: '#2a5a8a', opacity: 0.85, borderWidth: 1, borderColor: '#0cf' }, emphasis: { label: { show: true }, itemStyle: { color: '#ffaa00' } }, data: [ { name: '北京', value: 320 }, { name: '上海', value: 540 } ] } ] }这里有几个关键参数需要讲清楚:
boxWidth和boxHeight决定整个地图 3D 空间的宽高,这对布局影响很大。如果地图在大屏的左侧,我需要留出距离给右侧的图表,就会调大boxHeight让地图在纵向有合理的显示占比。regionHeight是每个省份区域在 3D 平面上的基础厚度,如果设置成 0,地图就是一张“贴纸”,设置了正值,省份就会像积木一样立起来。environment控制环境光源颜色,会直接影响整体色调。shading: 'realistic'支持更真实的光影效果,但开销较大;如果设备性能一般,建议用shading: 'color'或lambert。
视角控制依赖viewControl:
viewControl: { alpha: 40, // 俯仰角,40 度往下看 beta: 0, // 水平旋转角 distance: 120, // 视距 autoRotate: true, autoRotateSpeed: 8, minAlpha: 5, maxAlpha: 90 }autoRotate是我在大屏上必开的选项,地图自己转起来,科技感直接拉满。但要注意,自动旋转会让用户鼠标拖拽后停止,这是正常行为,autoRotate在用户主动交互后会自动暂停。如果希望旋转不停,需要监听事件重置,不过实际上很少这么干,因为用户控制权是必须保障的。
4.3 3D 地图的进阶用法:柱体高度映射数据
要让 3D 地图真正“3D”起来,最常用的高级玩法是把数值映射成省份的高度。具体做法是用map3D的regions属性:
regions: [ { name: '北京', height: 20, itemStyle: { color: '#ffd54f' } }, { name: '上海', height: 35, itemStyle: { color: '#4fc3f7' } } ]但配置文件里手写regions是非常不优雅的做法,数据一多就维护不了。我通常会用 JS 根据数据动态生成regions数组:
const regions = rawData.map(item => { const height = 5 + (item.value / maxValue) * 40 return { name: item.name, height: height, itemStyle: { color: getColorByValue(item.value) } } })高度和颜色的归一化计算是关键。5 + (value / max) * 40的意思是:最小高度 5,最大高度 45,保证数值为 0 的省份也有一个基础厚度,不会扁平到看不见。颜色渐变则可以通过第三方库如 d3-scale 或手动插值实现,也可以直接用visualMap的inRange.color控制。
这种“高度即数据”的表达方式,视觉冲击力非常强,适合用来展示 GDP、销量、人口等总量型指标。我在一次大屏项目里做全国销售分布时,用了这个方法,客户当场就满意了,因为一条条立起来的柱体区域让数据对比变得一目了然。
4.4 3D 地图上叠加 bar3D 柱状图
有时候单纯把省份地形抬高还不够直观,我会在地图上方叠加 3D 柱状图。做法是把地理区域转换成柱子的坐标,用bar3D系列实现:
function getCoordFromName(name) { // 利用 geo3D 组件获取区域中心坐标,或从 GeoJSON 中计算中心点 } series: [ { type: 'bar3D', coordinateSystem: 'geo3D', data: data.map(item => [ item.lng, item.lat, item.value ]), barSize: 3, shading: 'lambert', itemStyle: { color: '#4fc3f7' } } ]bar3D的坐标体系比较特殊,前两项是经纬度,第三项是柱体高度,也就是数据值本身。为了让柱子的位置落在对应省份的中心,我通常先用 GeoJSON 的geometry计算多边形中心点,或者直接根据已知的城市经纬度表来配。这个方法比regions高度更适合做“省份维度 + 数值对比”的场景,因为柱子的粗细和高度可以独立控制,视觉上更接近传统柱状图,但又能放在真实地理位置上,用户理解成本低,展示效果也高级。
唯一要注意的是性能。如果bar3D数据量超过 50 个柱子,低端显卡的旋转拖拽就会开始掉帧。我的优化策略是:关闭viewControl的自动旋转,减少光照计算,或者降低 canvas 分辨率。
5. 常见问题与排查技巧实录
5.1 地图区域不显示或白屏
这绝对是遇到最多的问题,几乎每个第一次做 ECharts 地图的人都会踩一次。常见原因有以下几种:
表格整理如下:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 整个图完全空白 | 容器高度为 0 | 给容器设置明确高度,如height: 600px |
| 有坐标系但没有区域 | 地图未注册或map名称不匹配 | 检查registerMap是否执行,map属性是否相同 |
| 区域出现但全是灰色 | 数据源 name 和 GeoJSON 的 name 不匹配 | 打印数据源 name,逐个核对 |
| 部分区域缺失 | GeoJSON 裁剪或过滤时误删数据 | 检查数据处理逻辑,确认所有 Feature 都被保留 |
| 请求跨域被拦截 | GeoJSON 用了线上 URL | 下载到本地部署,或配置代理 |
容器高度为 0 本身是最常见的。ECHarts 对容器尺寸非常敏感,初始化的时候如果父容器还没有高度(比如在弹窗里初始化),图表就画不出来,或者画出来只有一点点。解决办法是等容器渲染完成后再 init,或者在resize时手动设置高度。
另外,如果你用的是 Vue,在onMounted里初始化地图时,如果 DOM 还没有完全挂载,也可能导致容器高度为 0。稳妥的做法是await nextTick()再初始化。
5.2 2D 与 3D 共存时的冲突
有些页面需要同时展示 2D 地图和 3D 地图,甚至在同一张大屏上既有map又有map3D,这时候容易出现两个问题:
第一个是实例污染:两个图表如果共用同一个 DOM 容器,后初始化的实例会覆盖前一个。解决办法是每个地图独立容器,或者dispose掉旧实例再重建。第二个是全局样式冲突:如果两个图表在一个<div>中,echarts-gl 的渲染很可能把普通 echart 的 z-index 覆盖掉,导致 2D 地图消失。通常我给 2D 图表容器设置z-index,并确保两个容器的层级关系正确。
还有一点,如果同一页面引入了多个echarts实例,记得在beforeDestroy里 dispose,否则内存泄漏严重,页面切换几次后大屏就会变卡。
5.3 tooltip 自动换行与样式调整
tooltip 换行的问题,我在 3.3 节提过一部分,这里再补充两个高频细节。
第一个是confine: true。如果地图靠屏幕边缘,tooltip 会超出容器,导致页面出现横向滚动条。加上confine: true可以让 tooltip 自动约束在容器内部。
第二个是extraCssText。通过字符串直接添加 CSS 样式,比如限制最大宽度:
tooltip: { trigger: 'item', confine: true, extraCssText: 'max-width: 300px; white-space: normal; word-break: break-all;' }注意,tooltip的默认样式是white-space: nowrap,所以就算在formatter里写了<br/>可以换行,但一段超长的文字依然可能不换行。我习惯在extraCssText里把换行相关样式显式写上,这样在浏览器里基本不会再出现 tooltip 撑破大屏的情况。
5.4 pxtorem 对 ECharts 不生效的根因与解决
很多用 Vue 项目配置了pxtorem(比如移动端适配),然后发现 ECharts 图表并没有跟着 rem 字号调整,这是正常的。因为 ECharts 的fontSize和symbolSize等配置项在内部是以像素为单位的,canvas 绘制的时候不会经过 CSS 的 rem 转换流程,而且canvas的元素尺寸是由我们传入的容器像素决定的,页面缩放 rem 并不会自动通知图表重绘。
我的处理思路是:项目里做一个 rpx 转换函数,在设置 ECharts 配置的时候手动把像素值转成 rem 后的实际像素值:
function px2rem(pxValue) { const base = document.documentElement.clientWidth / 10 // 根据项目 rem 基准调整 return (pxValue / 1024) * base }然后在配置fontSize: px2rem(12)、symbolSize: px2rem(20)时调用它。同时监听窗口resize事件,触发chart.resize(),并把所有用到px2rem的配置重新 setOption。这个方法实测有效,但如果你只是想简单适配,直接把 echarts 实例放到一个固定大小的容器里,再配合transform: scale做整体缩放,也是一种取巧方案,就是交互坐标会有偏差,不太推荐。
5.5 性能优化与加载优化
地图项目如果只是单页展示还好,但一旦做成大屏,图表数量多,数据量大,性能问题就不得不考虑。我总结出几个实用办法:
- 地图 GeoJSON 请求放在本地,不要每次打开页面都发外部请求。线上部署的时候把 json 文件放到 CDN 上,利用缓存减少二次请求。
- 对于不需要下钻的静态地图,可以使用
animation: false关闭入场动画,减少首帧渲染耗时。 - 如果是大屏长时间运行,建议定时
dispose掉不用的图表实例,避免内存持续增长。 - 3D 地图的
environment: 'none'可以大幅降低光照计算量,在性能有限的设备上优先考虑关掉环境反射。 - 数据量很大的时候,把
visualMap的calculable设为false,减少拖动渐变条时频繁更新地图的性能开销。
6. 实操心得与扩展建议
这个项目的核心链路从获取 GeoJSON、注册地图,到配置 2D 的series-map、叠加effectScatter,再到引入echarts-gl实现map3D,整体脉络是比较清晰的。如果以后还有类似需求,我建议你在动手前先做好三件事:确定使用场景是 2D 还是 3D;确认 GeoJSON 数据的归属与精度;确认数据字段能精确匹配到地图区域的name。这三件事想清楚,后面所有配置基本就是套模板。
最后再分享一个我个人的小技巧:不要硬背配置项,把常用配置整理成基础模板文件。我有一个专门的地图配置文件,把visualMap、tooltip、label、itemStyle、viewControl这些基础配置都抽成公共常量,每次新项目直接引入覆盖,改改数据就能上线,效率提升非常明显。地图可视化这个东西,踩坑是难免的,但只要有条理地记录和总结,累积下来的模板和排查经验会让你越做越顺手。