1. 先说明白:echarts 中国地图的核心卡点在哪
做数据可视化的人应该都有过这种时刻:大屏架子搭好了,指标算完了,结果卡在"怎么把中国地图画出来"这一步。尤其是用 echarts 的时候,5.x 版本开始官方不再内置地图数据,想象中的map: 'china'直接变成一张空白画布。更难受的是,网上随手搜出来的"全国省市json"大多是几年前的老文件,行政区划早就调整过好几轮,下钻到区县时缺边界、错名字成了家常便饭。
这个需求我帮人排查过很多次,核心其实就三件事:第一,去哪里拿一份新的、标准的 GeoJSON 数据;第二,怎么在 echarts 里正确注册并渲染;第三,真到了生产环境会遇到哪些坑。这篇就把这三件事一次讲透,全程基于我实际跑通过的方案,不需要注册什么平台账号,也不需要申请什么密钥,直接抄作业就行。
1.1 为什么会卡在地图数据处理
很多人一开始以为画中国地图是 echarts 的一个内置功能,填个配置就行。这个认知在 4.x 时代确实成立,但 5.x 之后官方把地图数据从包里拆掉了,理由很简单:行政区划数据需要跟随主管部门的调整持续更新,塞在框架包里既跟不上节奏,又让框架体积白白变大。于是"地图数据"和"图表框架"被彻底解耦,开发者自己负责搞数据、注册数据。这个设计的本意是更好的,但对很多没跟上变化的人来说,就成了第一道门槛。
更隐蔽的问题在数据本身。行政区划代码和边界数据每年都可能微调,网上流传的很多 json 文件还停留在几年前,有的县级单位已经合并撤销了。还有些仓库为了压缩体积,把部分附属岛屿要素砍掉了,这种数据在对外展示的项目里是绝对不能用的。所以"数据从哪来"比"代码怎么写"更值得花心思。
1.2 这篇文章能帮你解决什么
我准备把整条链路完整走一遍:先对比几种获取地图数据的方案,告诉你哪个亲测可用、为什么可用;然后从零到一演示 echarts 里注册地图、渲染底图、上色、做省市区县下钻的完整过程;最后把我在真实项目里踩过的坑整理成一份速查表,你照着排查基本都能解决。
适合看这篇的人:前端开发、数据可视化工程师、做大屏看板的独立开发者,以及准备在作品集里加一个"中国地图下钻"场景的同学。看完你可以拿到一套完整可复用的代码逻辑,以及一个能即时下载全国省市区县边界数据的公开渠道。
2. 全国省市区县 GeoJSON 数据怎么拿:三条路对比与亲测方案
地图数据获取这件事,看着简单,实际坑很多。我把网上能找到的主流方案都试过一遍,下面直接说结论。
2.1 开源仓库、厂商平台、在线数据接口的真实体验
第一条路:从开源仓库或 npm 包下载现成的china.json。这条路最快,但坑也最多。很多仓库最后一次提交还在好几年前,期间行政区划已经变过好几轮,你拿到的文件跟如今的地图对不上。还有一部分仓库为了省体积,把部分边界要素精简掉了,这种数据在需要精确展示的场景里完全不能用。如果你只是做个一次性 Demo,这条路可以;做正经项目,不建议赌运气。
第二条路:用地图厂商的开放平台。问题在于,绝大多数平台提供的是 JS SDK 和在线渲染服务,你要的是"一份静态 GeoJSON 文件",这跟他们的产品形态对不上。有的平台即使开放了数据接口,也需要申请 Key、配置白名单,甚至只允许在线调用,不允许下载到本地。对做大屏项目来说,运行时还要跨域请求别人的服务器,风险和延迟都很不划算。
第三条路也是我最终推荐的路子:使用一个公开的行政区划数据在线服务。它本质上是一个"行政区划选择器"页面,可以按全国、省、市、区县四级提供标准的 GeoJSON 文件,URL 完全规律化,不要 Key,直接 HTTP 访问就能拿到。这个服务我用了很长时间,亲测可以稳定拉取,也是社区里做地图下钻时普遍在用的数据源。
三条路放一起对比,差异很明显:
| 方案 | 获取速度 | 数据新旧 | 是否要Key | 格式标准 | 适合场景 |
|---|---|---|---|---|---|
| 开源仓库打包下载 | 快 | 普遍偏旧 | 否 | 参差不齐 | 快速Demo、不追求准确 |
| 地图厂商开放平台 | 中 | 较新 | 多数要 | 需自行转换 | 在线SDK项目 |
| 行政区划在线数据服务 | 快 | 随调整更新 | 否 | 标准GeoJSON | 生产项目、下钻场景 |
2.2 亲测可用的即时获取入口与 URL 规律
这个服务的入口,业内一般称为 GeoAtlas 行政区划选择器。打开之后页面上就是一张全国地图,左边有省级列表,你点什么区域,页面右侧就会给出对应的 GeoJSON 下载地址。它的数据文件地址非常有规律,核心是行政区划代码,也就是 adcode。比如:
- 全国:
https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json - 四川省:
https://geo.datav.aliyun.com/areas_v3/bound/510000_full.json - 成都市:
https://geo.datav.aliyun.com/areas_v3/bound/510100_full.json
也就是说,你只要知道某个区域的 adcode,就能直接拼出它的完整下载地址。adcode 就是统计部门公布的行政区划代码,一共 6 位,全国是 100000,省一级是"省代码 + 四个零",市一级再往后接两位。你在选择器页面上点选区域的时候,地址栏里也会直接出现对应文件的 URL,复制下来就能用,不用死记。
这里有个高频细节值得单独说:URL 里的_full后缀。带_full的文件表示"包含下一级行政区的完整边界"。比如100000_full是全国带省级边界的版本,510000_full是四川带市级边界的版本,510100_full是成都带区县级边界的版本。如果你只需要本级轮廓、不想要下一级边界,把_full去掉同样能访问,体积更小。做下钻交互时用_full,做单层展示时用普通版,按需取用。
还有一点要注意:不设区的地级市(比如东莞、中山这类直筒子市),下钻到市级就到头了,没有区县一级文件,别再去拼下一层 adcode,白费功夫。判断有没有下一级,可以看 GeoJSON 里properties.childrenNum这个字段,大于 0 就说明还有下级。
2.3 数据下载后的完整性检查与本地化
文件拿到手后,我建议第一时间做三件事。第一,打开 JSON 确认是合法的FeatureCollection结构,有features数组,每个元素里有geometry和properties。第二,检查properties里有没有adcode、name、childrenNum这些字段,后面写交互逻辑要依赖它们。第三,把文件下载到项目静态目录,存成"本地资源",不要每次启动页面都去请求远程地址。这样部署之后加载速度稳定,也避免源站万一调整导致线上地图突然白屏。
生产环境还要格外注意底图的完整性。对外展示的可视化大屏,地图底图必须包含完整的国界线要素和南海诸岛等附属要素。这个在线服务返回的 full 版本默认是带上的,但你在网上随便下的第三方精简 json 就未必。所以我养成了一个习惯:任何来源的地图数据,上线前都做一次完整性检查,打开文件搜一下关键要素是否存在,不要等到客户在大屏上发现缺了东西才后悔。
坐标系也要确认。这个服务的 GeoJSON 用的是国际通用的经纬度坐标,与 echarts 的默认处理方式一致,不需要额外纠偏。但如果你从其他地理数据库导出的数据,一定要先确认坐标系,否则后续叠加散点、飞线时,点位会和区域边界错位。
3. echarts 加载中国地图的完整落地流程
数据搞定之后,剩下的就是 echarts 的标准操作了。我把从零到可交互的完整流程拆成四步,每一步都给出可以直接复制的代码。
3.1 版本选择与最小工程搭建
先明确一个原则:哪怕项目还在用 4.x,我也建议尽早按 5.x 的方式组织代码。原因很简单,4.x 把地图数据内置在包里,看起来省事,实际上数据和框架耦合在一起,数据更新极不方便;5.x 把"获取数据"和"注册地图"拆开,后续更新地图数据只需要换 json 文件,业务代码一行都不用动。这种解耦在维护期能省下大量时间。
安装不复杂:
npm install echarts@5如果用 CDN 方式引入,直接用 echarts 5 对应的压缩文件即可。注意 5.x 没有任何内置的中国地图注册方法,这是很多人一上来就懵的根本原因——不是写错了,是少了一步"注册"。
3.2 注册地图与基础渲染:一个能跑的最小示例
假设你已经把100000_full.json下载下来,放到了项目的static/map/china.json。下面的代码是最小可运行版本:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>echarts 中国地图最小示例</title> <script src="./echarts.min.js"></script> </head> <body> <div id="map" style="width: 100%; height: 600px"></div> <script> fetch('./static/map/china.json') .then((res) => res.json()) .then((geoJson) => { // 关键步骤:把 GeoJSON 注册成 echarts 可识别的地图 echarts.registerMap('china', geoJson); const chart = echarts.init(document.getElementById('map')); chart.setOption({ tooltip: { trigger: 'item' }, geo: { map: 'china', roam: true, itemStyle: { areaColor: '#e8f0fb', borderColor: '#606c84', borderWidth: 1 }, label: { show: true, fontSize: 10 } } }); }); </script> </body> </html>有两个关键点。第一,echarts.registerMap('china', geoJson)里的'china'是注册名,它决定了后面geo.map和series.map里要填的名字,两者必须完全一致。很多人白屏就是因为这里写成了'China'、'china-map'之类,跟注册名对不上。第二,geo组件的map属性引用注册名,作用是画一个纯底图。如果只需要展示地图轮廓,不叠加业务数据,用geo就够了,配置简单,性能也好。在真实项目里,geo更常见的用途是当"底图",在上面叠加散点图、飞线图、涟漪特效这些系列。
在打包工程里,也可以直接用 import 引入 json:
import geoJson from './static/map/china.json'; echarts.registerMap('china', geoJson);3.3 数值着色与 visualMap 的正确姿势
光有底图没意义,大多数场景是要把省份的数值映射成颜色深浅。这种需求要用series里的map系列。它的原理是:echarts 根据data数组里每一项的name,去 GeoJSON 的 feature properties 里找到同名区域,然后按数值大小结合 visualMap 映射到颜色。所以重点来了:地图能不能正确上色,靠的是 name 的严格匹配。
const chart = echarts.init(document.getElementById('map')); fetch('./static/map/china.json') .then((res) => res.json()) .then((geoJson) => { echarts.registerMap('china', geoJson); chart.setOption({ tooltip: { trigger: 'item', formatter: (params) => params.name + ':' + params.value + ' 万元' }, visualMap: { min: 0, max: 100, left: 20, bottom: 20, text: ['高', '低'], calculable: true, inRange: { color: ['#e8f3ff', '#a8c8e8', '#3e6fa3'] } }, series: [ { type: 'map', map: 'china', roam: true, label: { show: true, fontSize: 10 }, emphasis: { label: { show: true, fontWeight: 'bold' } }, data: [ { name: '四川', value: 86 }, { name: '广东', value: 92 }, { name: '北京', value: 75 } ] } ] }); });常见问题是"某个省没上色",十有八九是data里的name和 GeoJSON 里的name不一致。比如 GeoJSON 里可能写的是"内蒙古自治区",你数组里写的是"内蒙古",哪怕肉眼觉得是同一个地方,echarts 也只认字符串完全相等。解决办法是给数据做标准化映射,或者更简单——直接遍历 GeoJSON 的features,把properties.name收集成一份标准名单,业务数据在名单基础上填充值,从源头杜绝 mismatch。我强烈推荐后者,一劳永逸。
visualMap 的min和max也要注意。如果写死成 0 到 100,而你的真实数据范围只有 60 到 80,地图会全是一个颜色,看不出差异。正确做法是用数据实际极值:
const values = data.map((item) => item.value); const max = Math.max(...values); const min = Math.min(...values);如果业务上想分档展示(比如 0-50 是一档、50-80 是一档),用 visualMap 的pieces配置会更直观,比连续渐变色更容易读懂。
3.4 省到市到区县的下钻联动实现
三级下钻是这个需求里最有含金量的部分。实现逻辑其实不复杂:监听地图点击事件,从 GeoJSON 的 feature properties 里拿到点击区域的 adcode,然后拼出下一级 json 地址,registerMap注册成新地图,再setOption更新视图。
// 建议初始化时从全国 geojson 的 features 里动态生成映射,不要手写 const adcodeMap = { 四川: 510000, 广东: 440000, 北京: 110000 }; chart.on('click', (params) => { const adcode = adcodeMap[params.name]; if (!adcode) return; fetch(`https://geo.datav.aliyun.com/areas_v3/bound/${adcode}_full.json`) .then((res) => res.json()) .then((geoJson) => { // 每次用 adcode 生成唯一注册名,避免覆盖旧地图 const newMapName = 'level' + adcode; echarts.registerMap(newMapName, geoJson); chart.setOption({ series: [ { type: 'map', map: newMapName, roam: true, label: { show: true }, data: generateCityData(params.name) } ] }); }); });有几个细节必须说明。第一,注册名不要反复用'china',因为同一个名字重复registerMap会覆盖注册表,而旧 option 里可能还引用着旧名字,容易出现白屏或者地图混乱。用'level' + adcode这种命名方式,天然不会冲突。第二,adcode 映射表不要手工维护,那是灾难。正确姿势是初始化时遍历全国 GeoJSON 的 features,每个 feature 的properties里都有adcode和name,循环一遍就自动生成了。第三,下钻之后把地图中心移到对应区域,体验会好很多。GeoJSON 的properties.center字段就是区域中心经纬度:
chart.setOption({ geo: { center: feature.properties.center, zoom: 6 } });返回上一级也简单:用一个数组保存下钻历史,点击"返回"按钮时 pop 出上一级地图信息,重新注册并 setOption。如果你不需要交互,只是展示静态地图,可以直接按需下载对应层级文件,绕开下钻逻辑,代码量更少。
4. 地图高频问题排查与上线避坑实录
代码写完只是第一步,我见过太多项目在"看着能跑"和"真正能上线"之间翻了车。这一节把高频问题和生产细节一起说清楚。
4.1 白屏、不上色、错位等 6 类问题速查
先把最常踩的 6 个问题整理成速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 整个区域白屏 | 没调用 registerMap,或 map 名字不匹配 | 检查注册名与 geo/series 的 map 值完全一致 |
| 有背景但没有任何边界 | GeoJSON 文件损坏或 features 为空 | 用 JSON.parse 校验,检查文件完整性 |
| 部分区域不上色 | data 的 name 与 geojson 的 name 不一致 | 从 features 动态生成标准 name 名单 |
| 地图和散点位置错位 | 坐标系不一致 | 统一使用国际通用经纬度坐标,不要混用加密坐标 |
| 标签密集重叠 | 默认 label 全开 | 区县级关闭 label,或用 formatter 选择性显示 |
| 切换下钻后地图空白 | 重复注册同名 map,或旧实例未清除 | 每次用 adcode 生成唯一注册名,销毁旧实例 |
白屏要分两种排查。第一种是 canvas 有网格线但没地图轮廓,说明 geoJson 没读进来或者解析失败,优先检查 fetch 路径、服务器有没有正确返回 json 文件;第二种是 canvas 完全空白,连 tooltip 都触发不了,优先检查 echarts 是否引入成功、init容器是否存在。打开控制台看有没有报Component series.map not exists,只要看到这句话,就是注册环节的问题。
4.2 代码之外容易被忽略的 4 个生产细节
第一,本地调试时如果你直接双击 index.html 用 file 协议打开,fetch 会被浏览器跨域策略拦住,地图必然加载不出来。解决办法是在项目目录起一个本地静态服务,这是前端开发的常规操作,但经常有人忽略,以为是自己代码写错了。
第二,大屏项目里一个页面往往有多个图表实例,如果同一个容器重复init,canvas 会叠加,内存也会一直涨。在 React 或 Vue 工程里,组件卸载时务必调用chart.dispose()。切换数据时,最好也先 dispose 再重新 init。
第三,自适应问题。大屏通常要跟随窗口缩放,地图也需要跟着动。常规做法是监听 resize 事件调chart.resize()。但注意 resize 只重算画布尺寸,不会重算你手动设置过的zoom和center,所以大屏适配时可能需要按比例重新设置,否则地图会看起来被拉变形。
第四,数据更新节奏。行政区划代码不是一成不变的,每年都可能调整。我习惯在项目里留一份"地图数据更新记录",把每次更新的时间、涉及区域写清楚。这个习惯看似多余,真能帮后续维护的人省下大把排查时间。
5. 走通之后:批量资源库、大屏组合玩法与心得
基础流程通了,后面就是效率问题和玩法扩展了。
5.1 用脚本批量建立本地地图资源库
全国 34 个省级行政区,加上几百个地级市、几千个区县,如果一个个去页面里点、手动下载,浪费时间不说,还容易漏。我后来的做法是写一个 Node 脚本,利用 adcode 的编号规律批量拉取:先获取全国 JSON,解析每个省份的 adcode,循环拼接 URL 下载省级文件;每个省级文件里再解析市级 adcode,继续循环。脚本跑完,本地就有一套完整的地图资源库,之后任何项目要用地图,直接复制目录过去,从本地读取,速度快、稳定性高。
脚本的思路用伪代码说就是:读一级文件 -> 遍历 features 收集下一级 adcode -> 并发下载 -> 存成map/{adcode}.json。实际操作要注意控制并发数,不然几十个请求同时打过去,容易触发对方限流;同时做好失败重试和断点记录,下载到一半断了可以从上次的位置接着跑。
5.2 大屏场景的组合玩法与实操心得
底图就位后,玩法很开放。你可以用geo底图叠加散点系列表示城市热力,叠加航线系列做客流或物流飞线,叠加涟漪特效标记重大事件点。这些系列的坐标也是经纬度,只要保证坐标系和底图一致,后端直接返回[lng, lat]就能对上位置。再配合前面的下钻逻辑,一个大屏从全国到省、到市的联动框架就完整了。
最后说一点个人体会:地图类的需求,最容易出问题的从来不是 echarts 本身,而是数据源。数据源可靠、格式规范、更新及时,地图渲染只是几行代码的事。所以我的习惯永远是把地图数据当作一等公民,单独管理、单独更新、单独测试,不跟业务代码耦合在一起。这套方案我在很多项目里验证过,从大型数据平台到单页 Demo 都好用。如果你照着流程走一遍还有卡住的地方,优先检查三处:注册名、json 路径、name 匹配,这三个位置基本能覆盖九成以上的问题。