河南省市区县级GeoJSON数据包:以行政编码为核心的ECharts地图可视化实践
简介为使用 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不是FeatureCollectionECharts的registerMap会直接报错如果features为空地图会渲染出空白区域排查起来很费时间。这套数据包里的每个文件我都验证过这几点都是通的。3. 实操过程把数据包接入ECharts项目3.1 市级地图渲染注册地图与基础配置先演示最基础的接入流程以渲染郑州市地图为例。安装EChartsnpm方式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文件确认编码和字段名再接入能少踩一半的坑。本文还有配套的精品资源点击获取