做数据可视化大屏这几年,被问得最多的问题之一就是把数据铺到ECharts中国地图上。前两天同事拿着效果图过来说:这里要一个国家地图,背景要科技感,最好是3D,城市能闪光,还有飞来飞去的连线。我一边答应一边心里叹气,地图这东西太容易踩坑了。刚好这次把2D和3D两套地图方案从头到尾完整做了一遍,趁热把过程、配置和踩过的坑都记录下来。文章会先讲地图数据的准备和注册,再分别展开2D中国地图的series-map与geo组件用法,然后是3D地图中map3D与bar3D的组合玩法,最后是大屏集成时遇到的实际问题。不管你是刚开始做数据可视化,还是已经被地图坑过几回,这篇都能当一份可直接参考的笔记。
1. 地图数据才是真正的门槛:GeoJSON获取与注册
1.1 为什么ECharts 5之后地图全靠自己带
很多刚接触ECharts的人会遇到一个奇怪的现象:按照老教程把type写成map,map填上'china',页面却渲染出一个灰块,或者干脆什么都不显示。原因说起来也简单:ECharts 5.0起官方不再内置任何国家、省市的地图GeoJSON数据。ECharts只负责把GeoJSON绘制成地图,但它不再帮你下载数据。
这个改动影响很大。拿中国地图来说,数据本身存在边界、坐标体系、省市层级这些复杂问题,官方把准备数据的职责交给了开发者。所以做ECharts中国地图的第一步不是写option,而是准备一份合法且完整的中国GeoJSON,再把它注册进ECharts。
我遇到过有同事在ECharts 4老项目里直接复制了代码,升级到ECharts 5之后地图没了,第一反应是去查option配置有没有改动,折腾半天才意识到是内置地图数据被移除了。记住这条规则,能省很多排查时间。
1.2 常用数据源与选择建议
准备中国地图GeoJSON,我用得最多的是阿里云DataV的地理数据接口,它更新及时、覆盖省市县三级,而且接口返回的就是标准GeoJSON:
- 全国带子区域:
https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json - 各省:
https://geo.datav.aliyun.com/areas_v3/bound/440000_full.json(以广东省为例) - 市级:
https://geo.datav.aliyun.com/areas_v3/bound/440100_full.json(以广州市为例)
ECharts官方示例仓库的examples/data/asset/geo/目录也有China.json,但更新频率不高,适合快速做demo。社区里还有人把各省JSON打包成npm包,用起来方便,但质量参差不齐,需要确认数据是否完整、是否带坐标系偏移。
| 数据源 | 特点 | 适合场景 |
|---|---|---|
| DataV areas_v3 | 更新快,省市县都有,带区域编码 | 最推荐,内网部署需提前下载到本地 |
| ECharts官方示例 | 简单直接,但版本老旧 | 快速demo演示 |
| 第三方npm包 | 使用方便,但质量参差不齐 | 需要确认数据质量和坐标体系 |
还有一个要点:地图边界以数据源为准。DataV的全国JSON里已经包含了完整边界,不需要额外处理。如果你的业务只显示某个省,就把对应省份的JSON下载下来注册,这也是地图下钻的基础。
1.3 registerMap注册的其实是坐标几何
拿到GeoJSON之后,核心动作就是注册。GeoJSON本质是一个FeatureCollection,里面每个Feature都有一个geometry,记录了多边形或线段的经纬度坐标。ECharts在渲染地图时,会遍历这些feature,把坐标点投影到canvas坐标系上,再闭合路径生成区域。
注册的代码非常简单:
import * as echarts from 'echarts'; fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json') .then(res => res.json()) .then(chinaJson => { echarts.registerMap('china', chinaJson); // 注册完成后才能渲染 initChart(); });注册成功后,可以通过echarts.getMap('china')确认地图数据对象,里面会有geoJson和features字段。调试时这个接口非常有用,排查注册是否失败时,看一眼返回值是不是undefined就清楚了。
这里有一个极其隐蔽的坑:地图是按name匹配的。GeoJSON的properties.name是什么,series.data里的name就必须是什么。DataV的数据里省级单位叫“北京市”“广东省”“广西壮族自治区”,不会自动简写成“北京”“广东”。如果业务上想用简称,可以在拿到JSON之后手动遍历features替换name字段,再重新注册。我当时为了做省份下钻,直接写了一个公共注册函数:
function registerMapByName(name, mapId) { fetch(`/maps/${mapId}.json`) .then(res => res.json()) .then(json => { echarts.registerMap(name, json); }); }内网部署时特别建议用这种方式:提前把GeoJSON文件放到静态目录或后端接口里,避免线上依赖DataV地址不稳定。
2. 2D中国地图的两种打开方式:series-map与geo组件
2.1 别被两个map概念绕晕
ECharts里有两个跟地图相关、名字又很像的东西:一个是series中的type: 'map',一个是geo坐标系组件。最直观的区别是:series-map是一组统计数据,它负责给区域填充颜色;geo是地图坐标系,它本身不是数据系列,而是让其他系列(散点、飞线、路径)能落到地图上。
如果你只需要做一个“各省一个数值,颜色深浅代表大小”的省份分布图,用series-map就够了。如果你还要散点、飞线、气泡这些要素叠加在地图上,我更建议用geo组件,把各种系列挂到geo坐标系上。
这个选择决定了后面option的整体结构,所以动手前先想清楚需求,比写代码时再返工高效得多。
2.2 series-map:最直接的省份着色
一个最简的2D中国地图配置如下:
option = { tooltip: { trigger: 'item', formatter: function (params) { return params.name + '<br/>数值:' + (params.value || '-'); } }, visualMap: { min: 0, max: 1000, left: 20, bottom: 20, text: ['高', '低'], inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#313695'] } }, series: [{ type: 'map', map: 'china', roam: true, label: { show: true, fontSize: 10, color: '#333' }, itemStyle: { borderColor: '#fff', borderWidth: 1, areaColor: '#d5e8f7' }, emphasis: { label: { show: true, fontWeight: 'bold' }, itemStyle: { areaColor: '#ffd666' } }, data: [ { name: '北京', value: 120 }, { name: '广东', value: 980 } ] }] };几个关键点:
roam: true开启鼠标拖拽和滚轮缩放,这是大屏上的刚需。visualMap要和series数据匹配,map系列会自动根据data里的value映射到inRange颜色。如果发现区域颜色不变化,多数是visualMap的min/max范围设置不合理,把所有数据都压在一个色阶里看不出差异。emphasis里的配置是鼠标悬停或选中后的高亮效果,样式上我习惯把areaColor换成亮色调,提高交互反馈。
2.3 geo组件:把地图当坐标系用
geo组件的配置写法和series不一样,它是独立的顶层配置:
geo: { map: 'china', roam: true, zoom: 1.2, itemStyle: { areaColor: '#1a3a68', borderColor: '#5ab2ff', borderWidth: 1 }, label: { show: true, color: '#fff', fontSize: 10 }, emphasis: { itemStyle: { areaColor: '#2d5f9e' } } }然后需要叠加的series里通过coordinateSystem: 'geo'指定坐标系:
series: [{ type: 'scatter', coordinateSystem: 'geo', data: [[116.4, 39.9], [121.47, 31.23]] }]有人可能会问:既然geo已经是地图了,为什么series里不直接写map?因为geo的价值在于它可以被多个series共享。地图只是背景,真正表现数据的是你叠加在上面的散点、飞线、热度图。如果再用series-map去画一遍地图,就会多一层无谓的地图渲染,白白增加负担。
2.4 visualMap的联动小技巧
当option里只有geo没有series-map时,visualMap无法直接给geo的区域填充颜色,因为geo是坐标系组件,不参与视觉映射。那想要geo背景也有颜色变化怎么办?两个办法:一是把geo换成series-map,二是用map3D系列来当底图(后面会讲)。
所以在一份option里,geo和series-map通常二选一。不要同时用同样的map去画两张地图,层级和对齐都会变乱。如果实在需要两者都要,一定要把其中一个的show关掉,或者通过zlevel分层控制,但维护成本会明显上升,不值得。
3. 让2D地图动起来:散点、涟漪和飞线组合
3.1 effectScatter做重点城市“点亮”效果
大屏上最常见的需求是在地图几个重点城市上打点,并且让这些点不断向外扩散。这个效果靠effectScatter实现:
series: [{ type: 'effectScatter', coordinateSystem: 'geo', data: [ { name: '北京', value: [116.405285, 39.904989, 100] }, { name: '上海', value: [121.472644, 31.231706, 80] }, { name: '广州', value: [113.264385, 23.129112, 60] }, { name: '成都', value: [104.066513, 30.572269, 40] } ], symbolSize: 10, rippleEffect: { brushType: 'stroke', scale: 3, period: 3 }, itemStyle: { color: '#ffd666', shadowBlur: 10, shadowColor: '#ffd666' }, label: { show: true, position: 'right', formatter: '{b}' } }]这里的data数组格式是[经度, 纬度, 数值],ECharts会自动读取前两位作为地理坐标,第三位作为数值。symbolSize我一般写固定值,如果城市数值差别较大,也可以用函数动态返回大小:
symbolSize: function (val) { return val[2] / 10; }这种效果放在深色科技感背景上最好看。如果页面背景很浅,可以把itemStyle.color改成品牌色,不要把光晕拉太满,大屏上容易刺眼。
3.2 lines实现飞线
“线从北京出发到上海”这类动效,实际用的是lines系列。它不直接画一条静态线,而是可以通过effect模拟一个点沿曲线移动:
series: [{ type: 'lines', coordinateSystem: 'geo', zlevel: 2, data: [ { coords: [[116.405285, 39.904989], [121.472644, 31.231706]] }, { coords: [[121.472644, 31.231706], [113.264385, 23.129112]] } ], lineStyle: { color: '#5ab2ff', width: 2, curveness: 0.3, opacity: 0.6 }, effect: { show: true, period: 4, trailLength: 0.2, symbol: 'arrow', symbolSize: 5, color: '#ffffff' } }]curveness控制线的弯曲程度,0是纯直线,0.3左右视觉上最好看,太弯会显得航线不真实。这里的轨迹是两点之间通过贝塞尔曲线补出来的,不是真实航线,所以如果你要做航班实际路径,需要自己处理地理路径数据。
zlevel在这里很有用。地图各层默认zlevel是0,如果没有给线设置更高的层级,飞线很容易被其他元素覆盖,或者hover时相互干扰。我一般把map或geo留在默认层级,散点线设置成2或更大。
3.3 多系列叠加时的坐标系对齐问题
一个典型的组合option是:geo画背景 +effectScatter画城市 +lines画连线 +visualMap控制城市点大小或颜色。它看起来像是一个复杂的系统,其实本质就是多个series共享同一个geo坐标系。
遇到对不齐的情况,先检查registerMap用的GeoJSON是否统一。项目里如果同时在用“全国JSON”和“某省JSON”,一定要在切换时先重新registerMap,再setOption。跨层级时GeoJSON变更了,但option里仍写着map: 'china',那地图自然还是老的,散点坐标却可能来自新JSON,整体就乱了。
如果你用的是series-map,想再叠加散点也不是不行,但逻辑会绕。散点系列的coordinateSystem还是得指到某个地图坐标系上,而series-map本身不暴露geo坐标系统,通常做法是再配置一个geo组件。结果就是一份option里既有series-map又有一个geo,两个都画了同一张中国地图,视觉上想保持一致,得把两边的区域颜色、描边、label样式全部同步,维护起来非常麻烦。所以我的建议很明确:要做散点、飞线、气泡这类叠加效果,直接用geo当底图;纯展示省份数值分布,用series-map。
4. 3D中国地图实战:map3D与bar3D的两种路线
4.1 引入echarts-gl并解决版本匹配
3D地图不是ECharts核心库的能力,它依赖官方扩展echarts-gl:
npm install echarts echarts-gl如果项目用HTML方式引入,记得先引echarts,再引echarts-gl:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts-gl@2.0.9/dist/echarts-gl.min.js"></script>版本匹配是我见过最多的报错来源。ECharts 4时代需要配echarts-gl1.x,ECharts 5之后必须配echarts-gl2.x。如果版本不对,控制台经常会报Component series.map3D not exists,这句话的意思是:你写了map3D这个series类型,但当前环境里根本没有注册过这个组件。看到这个报错,第一反应不是去查option,而是查echarts-gl有没有正确加载、版本对不对。
4.2 map3D:让整片区域立起来
map3D的核心思想和series-map很像,也是把地图区域拉伸成3D块。基础配置如下:
option = { tooltip: { trigger: 'item' }, visualMap: { min: 0, max: 1000, inRange: { color: ['#1a3a68', '#5ab2ff', '#ffd666'] }, seriesIndex: [0] }, series: [{ type: 'map3D', map: 'china', regionHeight: 3, shading: 'lambert', itemStyle: { color: '#4a9eff', opacity: 0.9, borderWidth: 1, borderColor: '#0f2c52' }, label: { show: true, textStyle: { color: '#fff', fontSize: 12 } }, emphasis: { label: { show: true }, itemStyle: { color: '#ffd666' } }, light: { main: { intensity: 1.2, shadow: true }, ambient: { intensity: 0.4 } }, viewControl: { distance: 110, alpha: 40, beta: 0, autoRotate: true, autoRotateSpeed: 2 }, data: [ { name: '北京', value: 120 }, { name: '广东', value: 980 } ] }] };这里有四个容易踩的细节:
regionHeight是区域厚度,老版本里叫boxHeight。不同版本写法不一样,官方文档和旧博客混着看容易一头雾水。shading: 'lambert'是兰伯特光照模型,3D效果最自然;flat就是纯色无立体感。想写实可以往realistic方向走,但大屏上性能会差一些。light.main.shadow开阴影后,地图会在底部投出阴影,观感提升,但也会增加GPU开销。图表数量多的时候建议关闭。- 数据项里的value可以驱动visualMap,让区域顶面根据数据映射颜色。如果只想所有区域同一个颜色,可以不写visualMap,直接配置
itemStyle.color。
label在3D场景里还有一个遮挡问题。比如旋转视角后,省名可能会转到背面。这时可以固定viewControl.alpha的角度,或者在需要重点展示的几个城市上用标记点替代全量省名。
4.3 bar3D:让柱子从地图里“长”出来
如果说map3D满足的是“地图立体感”,那bar3D满足的则是“在指定经纬度上种柱子”。很多项目里会比较各省份数值,在省份中心或城市点上竖一根柱子,高度映射数据大小,这就是bar3D加map3D的组合:
option = { series: [ { type: 'map3D', map: 'china', regionHeight: 2, itemStyle: { color: '#173f66', opacity: 0.9, borderWidth: 1, borderColor: '#0f2c52' }, label: { show: false } }, { type: 'bar3D', coordinateSystem: 'geo3D', data: [ { name: '北京', value: [116.405285, 39.904989, 120] }, { name: '上海', value: [121.472644, 31.231706, 80] }, { name: '广州', value: [113.264385, 23.129112, 60] }, { name: '成都', value: [104.066513, 30.572269, 40] } ], barSize: 1.5, shading: 'lambert', itemStyle: { color: '#5ab2ff' }, emphasis: { label: { show: true } } } ] };bar3D的数据格式和scatter类似,前两位是经纬度,第三位是数值。柱子的高度会自动根据数值在3D场景内缩放,不需要额外设置maxHeight。barSize控制柱子粗细,单位是场景内的坐标单位,设太大柱子会互相重叠,设太小又看不清,具体值要看地图的缩放级别来试。
这里有一个潜在坑:bar3D数据里的经纬度必须落在你注册的GeoJSON覆盖范围内。如果经纬度点在海上或者地图边界外,柱子会悬空或者不显示。用之前最好把城市经纬度维护成一份单独的数据表,保证点位都在陆地范围内。
4.4 光照、视角与动态旋转参数调整
3D效果好不好,一半靠光照,一半靠视角。viewControl里的关键参数:
| 参数 | 作用 | 我的常用值 |
|---|---|---|
| alpha | 俯仰角度,0是平视,90是俯视 | 30~50 |
| beta | 水平旋转角度 | 0或配合自动旋转 |
| distance | 相机距离,越大地图越小 | 100~130 |
| autoRotate | 是否自动旋转 | true |
| autoRotateSpeed | 旋转速度,越大越快 | 1~3 |
大屏项目如果放在展厅里给访客观看,自动旋转可以开着,但速度要慢,否则用户来不及看清文字。如果是操作台交互场景,我一般把autoRotate关掉,让用户自己拖拽旋转更专业。
还有一个提升质感的选项是后处理辉光,在map3D系列里配置:
postEffect: { enable: true, bloom: { enable: true, bloomIntensity: 0.1 } }注意,一旦开启postEffect,地图会整体变亮,如果原始配色已经很亮,辉光会让文字看不清,这时需要把itemStyle颜色压暗一档。
5. 大屏项目里的坑:版本、性能、缩放适配
5.1 地图空白、区域不多、注册报错的排查顺序
我之前遇到过一张中国地图怎么都出不来数据,只有一小块灰的情况。排查顺序整理一下:
- 先看Network请求:GeoJSON是否请求成功,是不是被拦截或跨域。
- 再
console.log(echarts.getMap('china')):如果返回undefined,说明还没注册或注册失败。 - 打印geoJSON里的
features集合,确认properties.name和series.data里的name是否匹配。 - 如果区域能画出来但没着色,把visualMap的min/max范围先放大,或者临时改成
splitNumber模式,观察有没有颜色差异。
这套顺序我用了很多次,几乎能覆盖90%的地图渲染问题。还有一个常见情况:本地开发时从DataV拉JSON没问题,部署到内网后数据拿不到,地图就空了。解决办法是在构建阶段把JSON下载到静态资源目录,或者放进后端接口里,而不是在运行时依赖外网地址。
5.2 tooltip换行和展示格式
地图的tooltip和其他图表一样,需要在formatter里手动拼格式。如果一条tooltip要展示省份名、数值、排名、占比几个字段,用<br/>拼接:
tooltip: { trigger: 'item', formatter: function (params) { let lines = [params.name, '数值:' + (params.value || '-')]; if (params.data && params.data.rank) { lines.push('排名:' + params.data.rank); } return lines.join('<br/>'); } }如果内容太长,默认的tooltip框可能会超过屏幕或被截断,可以设置:
tooltip: { extraCssText: 'max-width: 240px; white-space: normal;' }white-space: normal才能让文字在框里自动换行,不然浏览器默认的nowrap会顶破容器。这个方法在处理长名称、多指标时非常实用,尤其是地图上展示“省份+多个指标”的场景。
5.3 大屏性能与rem适配
地图类型本身就是ECharts里开销比较大的图表,再叠加散点、飞线、3D柱子后更容易卡顿。我的经验是:
- 数据点超过几百个时,开启
large: true配合largeThreshold,提前进入大数据渲染模式。 - 不需要动画时全局关闭
animation: false,涟漪效果单独控制period。 - 3D场景下降低
autoRotateSpeed,关闭light.main.shadow和postEffect,能明显提升帧率。 - 如果使用ECharts 5.3以上,init时开启
useDirtyRect:
const chart = echarts.init(dom, null, { useDirtyRect: true, renderer: 'canvas' });还有一个很隐蔽的适配问题。大屏项目经常用postcss-pxtorem做rem适配,但这种方案只会处理DOM里的px,不会处理ECharts内部通过canvas绘制出来的文字和符号。你会看到图表容器被rem撑开了,但地图上的label字号、散点symbolSize纹丝不动。
所以别指望pxtorem管住ECharts。正确做法是:
- 监听窗口尺寸变化,调用
chart.resize()。 - 如果需要等比缩放,把chart容器按设计稿的宽高绝对定位,用CSS transform做整体缩放,而不是依赖rem改容器尺寸。
- 要动态调整字体或symbolSize,在resize回调里手动setOption更新这些值。
5.4 经纬度偏移与数据对齐
做地图叠加时,如果发现散点或bar3D柱子位置“漂移”,最可能的原因是数据坐标不一致。国内常用的坐标系有WGS84、GCJ-02、BD-09三种。ECharts里的GeoJSON如果来自GPS或国际标准数据,一般是WGS84;但很多业务数据来自高德或百度地图,返回的是加密后的GCJ-02或BD-09。直接混用就会出现点位偏移,严重时甚至跑到别的省。
处理方式有两种:一是统一源,让后端在接口层把坐标统一成地图使用的坐标系;二是前端用坐标转换库动态转。这个没有银弹,建议在项目开工前就把坐标规范定好。我见过很多半途接手的项目,图表本身没问题,问题全出在数据口径不一致上。
这次做地图的完整过程中,我个人最大的体会是:ECharts的2D中国地图本身并不难,真正花时间的是数据准备、坐标系对齐和版本匹配这些“地图周边”问题。2D适合快速看清全国各省的分布,3D适合放在大屏上做展示和重点城市联动。如果产品经理没有明确要求3D,我一般优先用2D加涟漪和飞线,简洁、清楚、性能好;一旦确认要3D,就提前把echarts-gl版本、局域网数据源、坐标转换方案都定下来。希望这篇记录能帮你少走几步弯路。