☰
ECharts中国地图实战:从GeoJSON注册到3D大屏展示
2026/9/30 4:32:35 网站建设 项目流程

做数据可视化大屏这几年,被问得最多的问题之一就是把数据铺到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 地图空白、区域不多、注册报错的排查顺序

我之前遇到过一张中国地图怎么都出不来数据,只有一小块灰的情况。排查顺序整理一下:

  1. 先看Network请求:GeoJSON是否请求成功,是不是被拦截或跨域。
  2. 再console.log(echarts.getMap('china')):如果返回undefined,说明还没注册或注册失败。
  3. 打印geoJSON里的features集合,确认properties.name和series.data里的name是否匹配。
  4. 如果区域能画出来但没着色,把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。正确做法是:

  1. 监听窗口尺寸变化,调用chart.resize()。
  2. 如果需要等比缩放,把chart容器按设计稿的宽高绝对定位,用CSS transform做整体缩放,而不是依赖rem改容器尺寸。
  3. 要动态调整字体或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版本、局域网数据源、坐标转换方案都定下来。希望这篇记录能帮你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询