简介:为使用 ECharts 实现河南省级及各地市区县地图的可视化开发而整理的数据包,面向前端工程师、数据可视化爱好者及需要在地图上展示区域数据的项目开发者。压缩包内共 176 个文件,包含 175 个按行政编码命名的 JSON 地理数据文件,覆盖河南省所有地市及区县,压缩包整体约 1.09MB,轻量易用。这些 JSON 数据可直接接入 ECharts 的 map 系列,配合行政编码可实现区域边界展示、行政名称标注、业务数据映射等功能,省去自行收集与转换 GeoJSON 的繁琐流程,特别适合实现省、市、区县多级下钻及热力分布图。已有 409 人学习使用,无论用于快速原型搭建还是正式项目集成,都能有效提升开发效率。 做地图可视化,最烦的不是ECharts配置写不出来,而是满世界找某个市、某个区的合法GeoJSON数据。前阵子因为项目要上一个河南省的市级、区县级联动下钻看板,我花了一整晚把河南省18个地级市(含济源示范区)以及所有区县的地图JSON数据全部整理了一遍,最终产出了这套“以行政编码命名”的JSON数据包,并打成了zip压缩包。如果你也是做可视化大屏、政务地图或者数据分析看板的,这篇文章直接把整个数据包的设计思路、目录机构、接入代码和排坑过程都聊透,你照着用就行。
1. 这套JSON数据包的整体设计思路
1.1 为什么偏偏用行政编码做文件名
早期我做过一个项目,地图文件全用中文拼音命名,比如“zhengzhou.json”“luoyang.json”,结果业务方需求一改,要把城市切换成地市维度、还要按编码做关联统计,代码里满屏的拼音映射表,改起来非常痛苦。
行政编码是国标GB/T 2260体系,每一级行政区都有唯一编码。直接用编码命名文件,最直接的好处有几点:
- 编码本身就是地区唯一标识,不会产生同名或异名问题。
- 后端接口返回的数据通常也是按编码维度聚合的,前端拿编码直接拼路径加载JSON,联动逻辑非常干净。
- 省市级联时,通过编码前缀(前两位是省,前四位是市)就能知道层级关系,不需要额外维护父级子级关系表。
这套数据包正是围绕“编码即文件名”的核心思路来组织的,拿到zip之后你不需要看任何说明文档,只要知道目标地区的编码,就能瞬间定位到对应JSON文件。
1.2 数据包目录结构与命名规范
解压之后,目录结构是这样的:
henan-maps/ ├── 410000.json # 河南省省级地图 ├── city/ │ ├── 410100.json # 郑州市 │ ├── 410200.json # 开封市 │ ├── 410300.json # 洛阳市 │ ├── 410400.json # 平顶山市 │ ├── 410500.json # 安阳市 │ ├── 410600.json # 鹤壁市 │ ├── 410700.json # 新乡市 │ ├── 410800.json # 焦作市 │ ├── 410900.json # 濮阳市 │ ├── 411000.json # 许昌市 │ ├── 411100.json # 漯河市 │ ├── 411200.json # 三门峡市 │ ├── 411300.json # 南阳市 │ ├── 411400.json # 商丘市 │ ├── 411500.json # 信阳市 │ ├── 411600.json # 周口市 │ ├── 411700.json # 驻马店市 │ └── 419001.json # 济源示范区 ├── district/ │ ├── 410102.json # 郑州市中原区 │ ├── 410103.json # 郑州市二七区 │ ├── ... # 更多区县 │ └── 411728.json # 驻马店市遂平县 └── README.md省级文件放在根目录,市级放在city/,区县级放在district/。所有文件统一用“6位行政编码.json”命名。这样设计的好处是,不管后续要扩展哪个省的数据包,目录结构完全一致,维护成本极低。
1.3 数据来源与坐标系说明
地图JSON数据本质上就是GeoJSON,描述的是每个行政区的边界坐标。这套数据包里的边界数据,是基于国家基础地理信息标准数据整理而来,坐标统一处理成了GCJ-02坐标系(火星坐标系)。
这里要特别说明一点:国内地图产品(高德、腾讯、ECharts内置地图等)用的都是GCJ-02,而部分开源数据源用的是WGS-84原始坐标系。如果直接用WGS-84数据,边界在高德底图上会发生偏移,严重的能偏出去几百米。所以拿到手的数据如果出现边界对不齐的情况,先别急着怀疑数据坏了,大概率是坐标系没统一。这套数据包已经处理成了GCJ-02,和大多数业务场景直接兼容。
2. 行政编码规则与JSON结构解析
2.1 省、市、县三级编码的规律
想把数据用到极致,就得理解行政编码的编码结构。中国行政区划代码一共12位(还有一种说法是6位起步),后6位是主体编码,前6位中:
- 前两位:省级编码,河南省是41。
- 前四位:地市级编码,比如4101就是郑州市。
- 6位完整编码:区县级编码,比如410105是郑州市金水区。
以郑州市为例:
| 级别 | 行政区 | 编码 |
|---|---|---|
| 省 | 河南省 | 410000 |
| 市 | 郑州市 | 410100 |
| 区 | 金水区 | 410105 |
你发现规律了吗?只要知道了省级编码41,就能推导出市级编码范围是410100-419001;知道了市级编码410100,也能推断出下辖区县编码的范围。这种前缀规律在联动下钻时非常有用。
2.2 GeoJSON的核心字段都在表达什么
打开任意一个JSON文件,比如410100.json,你会看到类似这样的结构:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "adcode": 410102, "name": "中原区", "center": [113.61285, 34.74825], "centroid": [113.613, 34.748], "childrenNum": 0, "level": "district", "parent": { "adcode": 410100 } }, "geometry": { "type": "MultiPolygon", "coordinates": [...] } } ] }重点看这几个字段:
type:必须是FeatureCollection,这是ECharts识别GeoJSON的基本条件。features:数组,每一项代表一个行政区。properties.name:行政区的显示名称,图例、tooltip默认从这里取。properties.adcode:行政编码,和文件名应该是对应的,用于联动和数据匹配。geometry:边界坐标数据,MultiPolygon表示该行政区由多个多边形组成,一个区县可能包含多个互不相连的区块。
实际操作中,有一个容易踩的坑:有的数据源里字段名不叫adcode,而叫code或adcode_pro,这会导致ECharts按编码匹配时找不到目标字段。这套数据包里我统一用adcode,兼容性最好。
2.3 如何快速校验一份JSON数据是否可用
在把数据接入项目之前,我建议你先做一组快速校验,避免等页面全白才发现数据有问题。
打开浏览器控制台,执行下面这段代码:
fetch('/maps/410100.json') .then(res => res.json()) .then(geoJson => { console.log('type:', geoJson.type); console.log('features数量:', geoJson.features.length); console.log('第一个区域:', geoJson.features[0].properties.name); });检查三个点:
type是否为FeatureCollection。features数组长度是否大于0。- 每个feature的
properties.name和properties.adcode是否有效。
如果type不是FeatureCollection,ECharts的registerMap会直接报错;如果features为空,地图会渲染出空白区域,排查起来很费时间。这套数据包里的每个文件我都验证过,这几点都是通的。
3. 实操过程:把数据包接入ECharts项目
3.1 市级地图渲染:注册地图与基础配置
先演示最基础的接入流程,以渲染郑州市地图为例。
安装ECharts(npm方式):
npm install echarts --save然后创建地图实例并注册地图数据:
import * as echarts from 'echarts'; import zhengzhouJson from './maps/city/410100.json'; echarts.registerMap('zhengzhou', zhengzhouJson); const chartDom = document.getElementById('mapContainer'); const myChart = echarts.init(chartDom); const option = { tooltip: { trigger: 'item', formatter: function(params) { return params.name + ':' + (params.value || '暂无数据'); } }, series: [{ type: 'map', map: 'zhengzhou', roam: true, label: { show: true, fontSize: 10 }, data: [ { name: '中原区', value: 120 }, { name: '二七区', value: 86 } ] }] }; myChart.setOption(option);这里注意两个关键点:
第一,registerMap的第一个参数是给这个地图起的别名,可以随意命名,但必须和series.map里的名称保持一致。
第二,data数组里的name字段,必须和JSON文件里properties.name完全一致,包括空格、多音字,否则对应区域显示不出数据。这个坑我踩过两次,每次都是因为某个区名字里多了个空格。
3.2 区县级联动下钻:点击后加载对应区县数据
整套数据包最有价值的地方就是支持省-市-区县的联动下钻。下面这段代码实现的是:点击市级地图的某个区域时,自动加载该区域的区县级JSON。
// 省级/市级地图点击事件 myChart.on('click', function(params) { const adcode = params.data?.adcode || getAdcodeByName(params.name); if (!adcode) return; // 根据编码拼接区县JSON路径 const districtMapPath = `/maps/district/${adcode}.json`; fetch(districtMapPath) .then(res => res.json()) .then(districtJson => { // 切换到下一级地图 echarts.registerMap('district', districtJson); myChart.setOption({ series: [{ map: 'district', data: transformDistrictData(districtJson) }] }); }) .catch(() => { console.log('该区域没有下级区县数据,停止下钻'); }); });这里有个隐藏的逻辑点:params.data能否拿到adcode,取决于配置series时是否把adcode放进了data中。所以在上一步配置数据时,我建议每个数据结构写成这样:
data: zhengzhouJson.features.map(f => ({ name: f.properties.name, value: getRegionValue(f.properties.adcode), adcode: f.properties.adcode }))这样点击事件里params.data.adcode就能直接拿到,不用再做名称到编码的映射。项目的业务数据也是按adcode聚合的,比如某接口返回[{ code: 410102, value: 120 }],就可以直接用adcode关联,后端代码也简单许多。
3.3 地图自定义样式:visualMap与数据映射
实际项目中地图不会只是干巴巴的边界,通常要按数值大小做颜色渐变。ECharts里用visualMap组件实现。
const option = { visualMap: { type: 'piecewise', // 分段型,适合数值区间较少的场景 pieces: [ { min: 1000, label: '1000以上', color: '#1e90ff' }, { min: 500, max: 999, label: '500-999', color: '#87ceeb' }, { min: 100, max: 499, label: '100-499', color: '#add8e6' }, { min: 0, max: 99, label: '0-99', color: '#f0f8ff' } ] }, series: [{ type: 'map', map: 'zhengzhou', data: regionData }] };对于连续型数据,把type改成continuous即可。需要注意一点:visualMap的最小值和最大值如果没有手动指定,ECharts会默认取数据中的最小最大值。如果想让颜色层次更稳定,建议手动设置min和max,否则当数据波动较大时,地图配色可能会一直在变。
另外,想做“立体”效果的话,可以配合viewControl和boxHeight开启伪3D视角:
series: [{ type: 'map3D', map: 'zhengzhou', boxHeight: 2, viewControl: { alpha: 50, beta: 20, distance: 100 } }]前提是安装了echarts-gl插件。实测下来,3D地图在展览大屏上视觉效果确实强不少,就是交互流畅度比2D差一些,移动端要谨慎开启。
3.4 处理非法坐标系和name不匹配问题
这里单独讲一个容易让人崩溃的问题:地图区域名称匹配不上。
ECharts的data数组是按name关联到地图区域的。如果你有一批区县数据是通过外部接口拿到的,地名写法和JSON里的properties.name不完全一致(比如多了一个“市”字、用了繁体、或者取了别名),地图上就会有一段区域没有数据展示。
解决思路有两种:
- 后端返回数据前,统一映射成
adcode,前端直接按编码关联,彻底绕开name匹配问题。 - 前端维护一个别名映射表,把接口返回的地名映射到JSON里的标准名称。
const nameAlias = { '郑州市中原区': '中原区', '中原区(郑州)': '中原区' };这套数据包在README.md里我已经把各市、各区县的准确名称全部列清楚了,接入前可以先对一下接口字段,能免掉不少联调沟通成本。
4. 常见问题与排查技巧实录
4.1 问题速查表
我在使用这套数据包和ECharts的过程中,整理了遇到频率最高的问题,基本覆盖日常开发九成以上的坑:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 地图区域空白/白屏 | GeoJSON未正确注册 | 检查registerMap是否执行,文件名和路径是否一致 |
| 区域变暗但无数据展示 | data里的name和JSON中的name不匹配 | 打印JSON的properties列表,逐个比对名称 |
| 边界偏移/和底图对不上 | 坐标系不一致或底图用了其他投影 | 统一使用GCJ-02坐标系数据,或调整底图 |
dom.clientWidth等报错 | 容器尺寸为0或未渲染完成 | 在DOM挂载完成后再echarts.init |
| 移动端点击无响应 | 事件被遮挡或roam配置干扰 | 检查容器z-index,或改用tap事件 |
| 地图显示但无行政区名称 | label.show为false或字体太小 | 开启label,调大字号,或设置formatter |
| 点击时加载子级无反应 | 对应区县JSON不存在 | 先到district/目录确认文件是否存在 |
4.2 疑难问题深度排查:地图边界不显示
说一个高发但不好排查的案例:地图轮廓能显示,但某个区域的边界线特别粗、或者干脆不显示。
排查方式是这样的:
- 先用
JSON.stringify(geoJson)检查坐标数据里有没有层级过深的嵌套。GeoJSON支持Polygon和MultiPolygon,一般两级就够,如果某些数据源嵌套了三层以上,需要先做数据扁平化。 - 再检查
geometry.type是否统一。一个数据包里如果有混用Polygon和MultiPolygon的情况,ECharts渲染时偶尔会抽风。处理方式是在接入前做一次标准化:
function normalizeGeoJson(geoJson) { geoJson.features.forEach(f => { if (f.geometry.type === 'Polygon') { // 统一转为 MultiPolygon f.geometry.coordinates = [f.geometry.coordinates]; f.geometry.type = 'MultiPolygon'; } }); return geoJson; }- 最后用在线GeoJSON校验工具跑一遍,确认边界坐标闭合。坐标不闭合会导致区域填充色异常,但有时框架不报错,UI上看就是“奇奇怪怪”的效果。
这套数据包里的JSON文件都做了这种标准化处理,但如果你从其他渠道找到的数据做合并场景,这个排查流程几乎是必走的。
4.3 一个容易被忽略的zip使用细节
压缩包解压后,如果直接双击打开文件,有时候会看到README.md或JSON文件出现中文乱码。这多半和压缩工具编码相关,和文件内容无关。我的处理方式是:解压时选择UTF-8编码,或者用较新的7-Zip版本,能规避大批乱码问题。
另外,有几个老项目里我踩过“同名字段被覆盖”的坑——两个JSON文件如果放到同一个目录且编码相同,加载时缓存覆盖会导致区域数据串掉。所以千万不要把市级和区县级文件混放在同一个目录下,建议始终沿用city/和district/这种分离目录结构。
5. 这套数据包的扩展用法
5.1 从静态JSON切换到远程动态加载
如果地图数据不常变,直接打包进前端资源是最高效的方案。但如果区县边界将来会有调整(比如行政区划合并、拆分),我更推荐把JSON放在静态资源服务器或对象存储上,用异步加载的方式接入:
async function loadMap(adcode) { const res = await fetch(`https://your-cdn.com/henan-maps/district/${adcode}.json`); const geoJson = await res.json(); return geoJson; }这样地图数据的更新和前端代码发布解耦,业务调整时只需替换JSON文件,前端一行代码都不用改。在我实际的项目里,这块非常管用,有一次区划调整后我只换了两个JSON文件,整个下钻链路没动过。
5.2 结合ECharts的视觉映射实现数据动态刷新
配合前后端分离架构,数据包里的地图JSON只管边界,业务数据通过接口动态刷。比如每隔10秒请求一次最新数据,然后更新series的data即可:
setInterval(() => { fetch('/api/region-stat') .then(res => res.json()) .then(data => { myChart.setOption({ series: [{ data: data.map(item => ({ name: regionNameMap[item.adcode], value: item.value, adcode: item.adcode })) }] }); }); }, 10000);这里又要回归到行政编码的价值了——接口返回adcode,前端通过编码换标准名称,或者干脆在data里带上adcode,即使名称变了也能精确匹配。
5.3 适配多级联动的通用封装思路
如果项目里不止河南省,将来可能扩展到其他省份,我建议你封装一个按编码加载地图的工具函数:
const mapCache = new Map(); async function getRegionGeoJson(adcode) { if (mapCache.has(adcode)) { return mapCache.get(adcode); } const level = adcode.endsWith('0000') ? 'province' : (adcode.endsWith('00') ? 'city' : 'district'); const path = `/maps/${level}/${adcode}.json`; const res = await fetch(path); const geoJson = await res.json(); mapCache.set(adcode, geoJson); return geoJson; }这样只要保持目录命名规范,切换到其他省份时只需要更新数据包和编码范围,前端代码完全复用。
6. 收尾:一点实操心得
这套数据包整理下来,最大的体会是:地图可视化的复杂度从来不在于ECharts配置本身,而在于数据是否规范、命名是否统一、坐标是否可靠。用行政编码作为文件命名的思路,让我在后来的多个项目中省去了大把联调时间,也避免了“手写映射表”这种极易出错的方案。
如果你要在自己的项目里复用这套数据包,我建议从目录结构开始就保持统一,不要擅自改文件名和编码规则。前端代码加一个简单的加载函数,把注册地图和异步请求封装好,遇到新增省份或区划调整时,只需要换数据包,代码完全不用动。最后再说一遍,解压之后先别着急跑项目,打开两个JSON文件确认编码和字段名再接入,能少踩一半的坑。
本文还有配套的精品资源,点击获取