简介:一套覆盖全国、地市、区县三级地图的js资源共343个文件,专为ECharts使用设计,压缩包约25.79MB。所有文件按“行政区划代码_省份_城市”命名,涵盖黑龙江、吉林、新疆等多省市,便于按区域定位并引入对应地图。开发者只需引入ECharts库与对应js,设置series的type为map并指定mapType,即可绘制可交互地图;随后可通过visualMap分段着色、绑定业务数据、添加标记或事件交互,适合经济指标展示、人口分布对比、选举结果可视化等场景。地图js由官方不再直接下载,这套资源弥补了空白,已有2183人学习使用,能省去繁琐的GeoJSON坐标整理,直接用于大屏或后台数据可视化项目。
1. 一张地图文件折腾两天:ECharts 行政区划 JS 到底怎么选
做可视化大屏的人迟早都会撞上同一个需求:要在 ECharts 上画中国地图、点出地市、下钻到区县。早期 ECharts 官网还能直接下载地图 JS,后来官方把下载入口砍了,网上搜出来的资源要么版本对不上、要么坐标系飘了、要么只有全国没有区县。这个标题对应的就是一套完整的解决方案——全国、地市、区县三个层级的 ECharts 地图 JS 文件,本质上是把 GeoJSON 地理数据包了一层registerMap,让浏览器能直接加载渲染。适合正在做大屏看板、需要地图下钻、或者刚接手一个地图模块却不知道数据从哪来的开发者。本文往下给的每一段,都是我实际处理过多个地图项目后沉淀下来的做法,能把选型、获取、渲染、排错这条路走通。
2. 地图 JS 文件的前世今生:从 GeoJSON 到 registerMap
2.1 JS 文件里装的到底是什么
ECharts 的地图 JS 文件,打开看其实就两部分:一大段坐标数据加一行注册代码。坐标数据就是 GeoJSON 格式的 FeatureCollection,每个 Feature 的coordinates里存的是经纬度数组,properties里放着name和adcode(行政区划编码);注册代码则是一个define或registerMap调用。常见做法是,整个文件用 AMD 风格包裹:
// 全国地图的典型结构:文件末尾会有一行类似这样的定义 (function (root, factory) { if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define([], factory); } else { root.chinaMap = factory(); } })(this, function () { return { "type": "FeatureCollection", "features": [ /* 省份边界坐标 */ ] }; });逻辑说明:这段代码做了一件事——把 GeoJSON 数据暴露出去,让下面echarts.registerMap('china', chinaMap)能拿到数据。你如果用的是 npm 包管理,也可以直接import chinaMap from './china.json'再注册。参数说明里最关键的是type字段,必须是FeatureCollection,这是 ECharts 能识别的最外层结构;features里的每个 Feature 对应一个行政区。需要注意的边界是,有的第三方文件会在注册时加一个svg缩放参数,官方文档里registerMap的specialAreas选项就是给南海诸岛这类离岛用的。
ECharts 5 之后对 GeoJSON 的校验更严格,只要你拿到的数据是合法 GeoJSON,不管文件名是什么,注册名自己定就行。我这里习惯把全国地图注册名固定为china,地市用cityName,区县用countyName,代码里好记,后续做下钻联动时命名规则也统一。
2.2 三个主流获取渠道:哪条路最省事
先说结论,我只推荐阿里云 DataV 的 GeoJSON 数据服务,不是因为它完美,而是因为大部分免费源要么覆盖不全,要么不稳定。表格里是没有收录完整地市区县的免费源的,DataV 有全套。
渠道对比:
| 渠道 | 层级 | 数据格式 | 坐标系 | 更新频率 | 坑点 |
|---|---|---|---|---|---|
| DataV GeoAtlas | 全国/省/市/县 | GeoJSON | GCJ-02 加密坐标 | 按月更新 | 部分虚线边界需要自己处理 |
| 某开源 GitHub 聚合项目 | 全国/部分省/市 | JS 文件 | WGS-84 / GCJ-02 混用 | 不定期 | 版本老旧,区县覆盖不全 |
| 自建 GeoJSON(OSM 导出) | 自定义 | GeoJSON | WGS-84 | 完全可控 | 需要清洗边界,工作量大 |
选型理由:DataV 的坐标是 GCJ-02 火星坐标,跟国内在线底图是对得上的,如果你叠加的是高德底图,完全不用做坐标转换;开源的 WGS-84 数据放到国内底图上会有几十米偏移,做网格下钻时这种偏移直接让散点落在隔壁区县。A 同学当初图省事用了一个 WGS-84 的 JS 包,结果某市散点始终压在邻市边界上,排查平台只看到坐标漂移,最后换成 GCJ-02 才解决。自建这条路适合定制化强、对边界精度要求高的项目,成本也高,小项目别碰。
还有一个渠道经常被忽略:echarts 官方仓库的map/js目录是历史遗留,里面还躺着旧版的中国地图 JS,格式和 ECharts 5 不兼容,直接拿去用会报Cannot read property 'push' of undefined。每次网上搜到这种资源的,我都劝先看文件头部有没有旧的echarts全局依赖,有就直接放弃。
2.3 三个层级的文件组织方式和大小差异
全国地图、地市地图、区县地图,这三层不是一个文件,是三个目录。常见的组织方式是/static/map/china.js、/static/map/province.js、/static/map/city.js,用行政编码做文件名后缀来区分,比如330000.js表示浙江省。文件大小上,全国约几百 KB,地市根据边界精度在几十到几百 KB 之间,区县单个一般不超过 100 KB。
很多人会犯一个错:把全国地图下载回来后,在里面找地市数据。全国 GeoJSON 只精确到省级边界,城市边界和区县边界是不存在的。三级地图之间没有包含关系,必须分别下载。我一般拿到项目先做一个盘点脚本,把需要的省份和城市名单列出来,一次性检查文件是否存在,而不是等到渲染时才发现缺了某块。
关于精度,DataV 数据默认到县级,乡镇级的 GeoJSON 不在本标题范围内。如果标题里的“区县”是指街道乡镇,那数据源就不一样了,别硬用区县文件去画乡镇边界。
3. 从下载到渲染:把地图 JS 接进项目的完整步骤
3.1 目录组织和静态资源引入
项目结构按这种来放,前后端分离和传统多页都能用:
frontend/public/static/map/ ├── china.js # 全国省级地图 ├── province/ │ ├── 330000.js # 浙江省(含地市级边界) │ └── 440000.js # 广东省 └── city/ ├── 330100.js # 杭州市(含区县级边界) └── 440100.js # 广州市对数说明:用行政编码做文件名是第一推荐,别用拼音缩写,后端的区域表里都是数字编码,接口返回code: 330100时前端可以直接拼出 URL;用拼音还得维护一张映射表,纯属给自己加工作。引入方式上,传统 script 标签加全局变量最省心,Vue/React 工程化项目里可以用import 'echarts'配合动态import做按需加载。
3.2 全国地图注册与一个能跑起来的最小示例
// 全国地图渲染的最小代码:先注册,再 setOption import * as echarts from 'echarts'; // 假设已经在 index.html 里用 <script src="/static/map/china.js"></script> 引入 // 或通过 import chinaMap from '@assets/map/china.js' 引入 const chart = echarts.init(document.getElementById('map-container')); // 指定 map 为 'china',这个名称必须和 registerMap 的第一个参数一致 chart.setOption({ tooltip: {}, visualMap: { min: 0, max: 1000, left: 'left' }, series: [{ type: 'map', map: 'china', roam: true, label: { show: false }, data: [ /* 后端返回的数据 [{ name: '广东省', value: 300 }] */ ] }] });逻辑说明:echarts.init的容器要是块级元素且有明确高度,否则地图画不出来;registerMap这步要么在引入文件时自带(很多 JS 文件在 define 之后直接执行注册),要么显式调用一次。如果项目是纯前端,没有后端渲染数据,也可以直接用前端 mock 数据填data字段。roam开启后支持鼠标拖拽和缩放,大屏展示建议设为true,但要注意如果容器宽度小,roam 会放大误操作概率。
参数需要调的核心就三个:map的值对应注册名,label.show控制省名是否显示,visualMap控制颜色深浅。实际项目里视觉映射不要用默认的百分位,直接给min和max才能稳定看到颜色分布。另外 ECharts 5 的 series 里必须设map类型,否则地图不会渲染,这是新一代版本跟旧版最大的区别。
3.3 地市、区县地图的下钻实现
下钻的交互逻辑很简单:点击省,加载省文件,渲染地市边界;点击市,加载市文件,渲染区县边界;点击空白,返回上一层。关键代码在于动态加载:
// 地市下钻:点击省份后加载对应地市 JS 文件 function drillDown(adcode) { const jsonpUrl = `/static/map/province/${adcode}.js`; // 动态加载 script,加载完成后再执行渲染 const script = document.createElement('script'); script.src = jsonpUrl; script.onload = function () { // 此处假设文件的 define 中已经把地图注册成 'jiangsu' 或按省份拼音注册 // 实际项目在文件末尾会调用 echarts.registerMap('jiangsu', json) chart.setOption({ series: [{ type: 'map', map: getMapName(adcode) }] }); }; document.head.appendChild(script); }逻辑说明:adcode是行政区划编码,点击省份时从params.name映射到编码。getMapName要维护一张“行政编码 → 注册名”的映射表,因为 DataV 下载的文件注册名可能是拼音,如zhejiang,不是数字编码。这段代码里没有写registerMap,因为很多现成 JS 文件在define之后就自动注册了,你直接用注册名渲染即可;如果是纯 JSON 文件,就得先await fetch再registerMap,那种情形下载的时候看文件格式是 JS 还是 GeoJSON 就能判断。
区县级文件用法完全一样,只是 URL 路径换成/static/map/city/${adcode}.js。返回上一层的逻辑,一般用chart.on('click', params => { if (!params.name) goBack(); }),点击空白区域时params.name是空的,正好用来做返回。
4. 加载策略与性能取舍:一次性全载还是按需加载
4.1 按需加载的三种方案对比
地图文件如果一次性全部引入,首屏会多出好几 MB 的 JS 开销,这在移动端尤其致命。常见做法是拆分加载:
- 方案 A:全国文件随首屏引入,省市文件按需动态加载。适合大屏项目,数据量大但是层级清晰。
- 方案 B:所有文件打包成异步 chunk,用动态 import 触发。适合中后台管理系统。
- 方案 C:后端把 GeoJSON 人存在数据库,前端通过接口拿。适合行政区划频繁调整的项目,但前置条件是后端能维护这套数据。
我通常选方案 A,原因是大屏数据看板是重前端,用户交互路径固定是“全国 → 省 → 市”,把全国文件常驻缓存,省市文件用 script 插标签的方式加载,兼容性最好。用 webpack 拆 chunk 没有错,但有跨域和构建配置成本,插标签的方式配合静态目录反而最简单。
4.2 文件加载失败怎么办:兜底和重试
动态加 script 标签很容易踩到网络请求失败、文件不存在这类问题,加一层兜底逻辑:
// 加载脚本并在失败时给出提示 function loadScriptWithTimeout(url, timeout = 10000) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; const timer = setTimeout(() => { script.onload = null; script.onerror = null; reject(new Error('timeout')); }, timeout); script.onload = function () { clearTimeout(timer); resolve(); }; script.onerror = function () { clearTimeout(timer); reject(new Error('load error')); }; document.head.appendChild(script); }); }逻辑说明:设置 10 秒超时是因为地图文件再大也不至于超过这个时间;若超时就提示“地图数据加载失败”,同时保留上一次的渲染结果不变。很多项目在这里还会做一个重试按钮,但实际用户更强的是直接刷新页面,所以把加载失败的错误提示写得具体点就行——写清楚“哪个区域的地图加载失败”,不要只弹一个“系统错误”。
4.3 渲染数据的缓存与复用
地图文件加载后是不变的,不需要每次渲染都重新拉。做好这两个缓存:一个是浏览器的 HTTP 缓存(静态文件服务器配Cache-Control),另一个是内存缓存。内存缓存的写法很简单:
const mapCache = new Map(); async function loadMap(adcode) { if (mapCache.has(adcode)) { return mapCache.get(adcode); } const res = await fetch(`/static/map/${adcode}.js`); const text = await res.text(); mapCache.set(adcode, text); return text; }注意动态 script 标签不会触发 fetch,这里是走另一种方式——把 JS 文件当文本 fetch 下来,存入缓存后通过eval或new Function执行。这种方式能拿到内容做预处理,但注意 CSP(内容安全策略)会禁止eval。大多数常规项目不限制eval,限制严的项目还是要用 script 标签加载,这里要跟安全部门提前确认。
5. 地图渲染避坑指南:白屏、偏移、漏岛这些坑我都踩过
5.1 只显示网格线,看不到任何区域边界
现象:初始化完成后,页面只有一个灰蓝色背景的 canvas,坐标轴都没有,一片空白。 原因:最常见的是registerMap没有被执行,或者执行了但注册名和series.map不一致;其次是 GeoJSON 数据格式不合法,比如features里的coordinates是空数组。 解决:先在控制台输入echarts.getMap('china'),返回undefined说明没注册,返回对象但没有geoJson字段说明数据有问题。另外要确认代码执行顺序,registerMap必须在setOption之前;如果你是用动态加载,要在onload回调里先registerMap再setOption,别在回调外执行。
5.2 地图边界满天飞,位置完全不对
现象:地图渲染出来了,但图形整体变形,边界线纠缠在一起,缩放后像网格碎片。 原因:数据源坐标是 WGS-84,而你渲染时使用的是 ECharts 默认的 Web 墨卡托投影,或者坐标系定义混乱。某些开源资源甚至把经纬度数组顺序写反了。 解决:优先确认数据源的坐标系说明。国内在线地图默认是 GCJ-02,如果拿到 WGS-84 数据就做坐标转换;如果只是个别边界像锯齿一样,那是数据精度问题,换一个来源的 GeoJSON 就行。另一种常见情况是你在一个自建坐标系的地图上混用了 GeoJSON,ECharts 对坐标系的适配能力有限,除非用自定义系列去画,否则别指望它能自动纠正底图偏移。
5.3 南海诸岛和台湾显示异常
现象:全中国地图渲染出来后,右下角的南海诸岛没有显示,或者台湾不在预期位置。 原因:ECharts 5 对 GeoJSON 的specialAreas支持需要显式声明,数据源如果缺失这部分,地图自然没有南海。台湾位置偏是因为数据里它不在主要区域的bbox范围内,投影后位置看起来“掉出去”。 解决:使用registerMap的第三个参数:
echarts.registerMap('china', geoJson, { '南海诸岛': { scale: 1.2, left: 0, top: 0 } });这里的scale控制南海地图块的缩放倍数,left和top控制它在画布上的位置。官方示例推荐scale: 1.0和left: -20这类微调,实际项目中我会根据容器长宽比手动调scale到 1.2~1.5,不然南海区域太小看不清。这个参数的作用仅限调整显示位置,不影响数据映射。
5.4 地市代码对不上,点击地区无法下钻
现象:点击省份后没有进入下钻,或下钻到了错误的省份。 原因:params.name返回的是 GeoJSON 里的name字段(如“江苏省”),而你的下钻函数需要的是行政编码(如320000),这两者之间的映射关系没有维护。 解决:建立地名到行政编码的映射表。不要手写,直接从数据源附带properties里取,DataV 的 GeoJSON 里properties.adcode就是编码。代码里优先用params.data.adcode而不是通过名称查表。如果项目里已经有后端接口返回区域树,让后端把编码和名称一起返回,前端把 GeoJSON 的编码跟接口编码做一次对齐校验,避免表格数据和地图数据不一致。
5.5 地图渲染性能差,缩放手感卡顿
现象:地图能显示但拖动和缩放帧率很低,尤其是在某省地图上操作时更明显。 原因:GeoJSON 数据精度过高,坐标点密度大,或渲染容器尺寸过大导致散点图形绘制压力大。 解决:在数据精度和性能之间做平衡。上游拿到的区县级数据,坐标点动辄上千,实际上画到屏幕上并不需要那么多点。用简单抽稀或引入工具库做坐标抽稀后,坐标点能减到三分之一。另一个更快的办法是直接换成 ECharts 5 的svg渲染模式:
const chart = echarts.init(container, null, { renderer: 'svg' });逻辑说明:renderer参数使用svg时,地图边界是矢量绘制,缩放时不需要重算像素,很多场景下比默认的canvas流畅。代价是 svg 模式下大量散点和动态数据会吃力,所以只在地图为主的数据大屏里推荐用 svg 模式。如果两者都不能满足,就压缩 GeoJSON 文件体积,把coordinates数组做定点舍入到 5 位小数,肉眼几乎无差别,体积能小三分之一。
6. 进阶用法:用 label 和 visualMap 把地图做成年报效果
地图文件接进来只是第一步,真正让大屏显得专业的往往是 label、颜色映射和联动交互这三块。我自己有一个固定的优化顺序:先调 label 字体和避让,再调 visualMap 的分段颜色,最后加点击事件联动旁边的折线图。
label 的避让是大屏最容易忽略的点。全国地图的省名默认全部显示,字号一大就糊成一团。常用配置是:
label: { show: true, fontSize: 12, formatter: function (params) { // 数值为 0 时不显示标签,减小视觉噪音 return params.value ? params.name : ''; } }formatter的作用不只是改文字内容,还能做条件筛选。地图省级边界多,隔壁省份数值为零时,密集的标签会互相遮挡,用这个逻辑过滤掉空值省份会比较舒服。如果还是不理想,再调labelLayout: { hideOverlap: true },这是在 ECharts 5 里内置的防重叠参数,比手工算位置省心太多。
visualMap 的坑在于默认连续渐变在极差值场景下会失效。做犯罪热力或销售分布这种帕累托分布很强的数据时,最大值比中位数大几个数量级,连续色条基本看不出层次。把visualMap切成分段:
visualMap: { type: 'piecewise', pieces: [ { gte: 1000, label: '高', color: '#c23531' }, { gte: 500, lt: 1000, label: '中', color: '#e6b450' }, { gte: 0, lt: 500, label: '低', color: '#749f83' } ] }pieces数字范围可以按项目数据分布动态生成,不要写死。视觉上,分段比连续更能突出“重点区域”,也更适合大屏上拉远看。另外 map 系列里还有一个zoom参数,控制地图初始缩放倍数,我经常把它和center配合,让首屏聚焦目标省份:
series: [{ type: 'map', map: 'china', zoom: 1.2, center: [105, 36] }]center的值是经纬度,中心点位置需要先反复试两次,不同投影下同一个经纬度的出画位置差异明显,调一次不理想就再挪一点。配合前面第 4 章的按需加载,这个配置可以让下钻后的城市地图初始就放大到适合操作的尺度。
最后一个习惯:地图文件引入到项目后,先写一个自动比对脚本,校验每个 GeoJSON 的adcode是否覆盖了业务需要的全部行政区,别等到上线了才发现某县没有边界数据。用户开放注册的区县如果有新增,行政区划代码会变,地图文件是静态的,这部分只能靠定时手工核对。希望这些方案帮到你,至少能让你在这个方向上少折腾两天。
本文还有配套的精品资源,点击获取