ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

eCharts地图资源:344个县区GeoJSON与两种加载方式详解

eCharts地图资源:344个县区GeoJSON与两种加载方式详解

简介:面向需要在中国县级行政区划层面做数据可视化的前端开发者,这份eCharts地图资源提供了全国344个县区的可复用地图数据,并内置两种接入方式演示:直接使用预置地理数据集,以及加载外部JSON自定义地图。压缩包共391个文件,其中380个为JSON格式的县区边界与代码数据,另含6个JavaScript脚本、2个HTML示例页面、2个工程配置文件及1份docx说明,整体仅2.82MB,便于快速下载与接入项目。目前已有1552人学习或下载,适合正在搭建区域统计、人口经济指标展示或需要离线地图方案的初中级开发者参考。通过配套Demo可快速理解echarts.init、setOption等关键步骤,掌握地图配准、下钻与悬浮交互的落地写法;JSON数据按县区拆分为独立文件,可按需引用,省去自行爬取边界数据的成本。

1. 县级地图的“最后一公里”:这份 344 县区 eCharts 资源解决的是什么问题

做过数据大屏或区域分析项目的开发者大多有这个体会:eCharts 官方地图免费开放到市级,一旦想下钻到区县,能直接用的地图资源就突然变得很稀薄。要么找到的 GeoJSON 覆盖不全,要么坐标系偏了导致元素错位,要么数据格式不统一没法直接喂给 eCharts。这份“eCharts 地图 344 个县区(附带两种方式 demo)”的资源包,恰好填的是这个断层——它一次性给出 344 个县区的地图数据,并且用两种不同加载方式写了可直接运行的示例。适合正在做区县级数据可视化、需要地图下钻、或者想快速验证“某个县区数据落在图上什么位置”的前端开发者。

2. 资源包解剖:344 个县区地图数据长什么样

2.1 目录结构与文件清单

拿到资源包先别急着跑,建议先把目录结构过一遍。通常这类地图资源会按“数据”和“示例”两个维度组织在一棵目录树里,常见布局如下:

echarts-map-344-counties/ ├── data/ │ ├── geojson/ │ │ ├── 110101.json # 每个文件对应一个县区 │ │ ├── 110102.json │ │ └── ... │ ├── merged/ │ │ └── all_counties.json # 合并后的完整数据 │ └── countyList.json # 行政编码、名称对照表 ├── demo/ │ ├── demo-local.html # 方式一:本地加载 demo │ ├── demo-async.html # 方式二:异步加载 demo │ └── assets/ │ └── echarts.min.js # 已内置的 echarts 库 └── README.md # 数据说明与使用文档

这里的关键是data/geojson下的文件命名方式——用行政区划代码做文件名是行业惯例,好处是天然唯一、不会重名,而且可以轻易通过countyList.json把代码映射成中文名。merged/目录里的合并文件则省去了你自己写合并脚本的麻烦。demo/assets下已经放好了 echarts 的压缩版,意味着你不装任何依赖也能直接打开 HTML 看到效果。

2.2 数据格式:GeoJSON 里都有哪些字段

探讨这份数据之前,先花半分钟回顾一下 eCharts 对地图数据的消费方式。eCharts 的地图系列(map类型)不直接吃 GeoJSON 原始结构,它要求先通过echarts.registerMap注册地图,注册时内部会提取 GeoJSON 里的features数组,把每个 feature 的properties.name作为地图区域的名称标识。所以这份资源里每个县区的 GeoJSON 文件都是标准结构,核心字段如下:

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "某县", "adcode": 110101, "center": [116.4, 39.9] }, "geometry": { "type": "MultiPolygon", "coordinates": [...] } } ] }

properties.name就是地图上显示的区域名,务必跟你业务数据里的区县名保持完全一致。properties.adcode是行政区划代码,这是数据联动的关键字段,做下钻、点击事件、数据匹配时都优选它做关联键。geometry是真正的边界形状数据,MultiPolygon意味着一个县区可能有多个不相连的面,比如某个县有飞地或者包含岛屿,渲染时 eCharts 会自动把多个面合并显示。

2.3 数据规模与坐标系说明

344 个县区这个数字意味着什么?对比一下:全国县级行政区划总数接近 2800 个,这份资源覆盖的是其中一部分,但 344 个的数量级足够支撑大多数区域分析类项目。更重要的是,这份数据已经做了坐标系统一处理——常见的坑是各区县数据来源不一,有的用 WGS84、有的用 GCJ-02,混合加载时会出现边界错位甚至跑到海上。

从文件大小看,每个县区的 GeoJSON 在 20KB 到 200KB 之间,取决于边界复杂度,344 个文件合计大概是 30MB 到 80MB 的量级。这个规模给两种加载方式都留出了工程空间:本地直接注册适合数据一次性全量加载,异步合并加载则适合控制首屏速度或者做主从地图切换。

3. 方式一:本地注册 GeoJSON,一个 main.js 跑通

3.1 核心代码:读取 GeoJSON 并注册到 eCharts

方式一的思路非常直接:把需要的县区 GeoJSON 文件在页面加载时全部读取,然后逐个调用echarts.registerMap注册,最后在图表配置里通过map: '某县区名'指定渲染哪一个。下面这段代码是从 demo-local.html 里提炼出的完整主流程:

// 1. 引入本地的 echarts 和县区数据(按需列出你要用的) const echarts = require('./assets/echarts.min.js'); const countyData = require('./data/countyList.json'); const geojsonMap = { '110101': require('./data/geojson/110101.json'), '110102': require('./data/geojson/110102.json') }; // 2. 遍历注册地图 Object.keys(geojsonMap).forEach(adcode => { const geo = geojsonMap[adcode]; const name = geo.features[0].properties.name; echarts.registerMap(name, geo); }); // 3. 初始化图表并渲染指定县区 const chart = echarts.init(document.getElementById('map-container')); chart.setOption({ series: [{ type: 'map', map: '某县', // 对应上面注册时的 name roam: true, label: { show: true }, data: [ { name: '某县', value: 358 } ] }] });

逻辑说明:第 1 步用 CommonJS 的require方式拉取本地文件,这是 webpack 或浏览器打包环境下最常见的写法,如果你直接用<script>标签引入,把require换成全局变量读取即可。第 2 步是核心,registerMap的第二个参数必须是完整的 GeoJSON 对象,函数会读取features[].properties.name作为区域名,这就是为什么名字必须和业务数据一致。第 3 步设置map: '某县'时,echarts 会在已注册的地图中查找同名地图并渲染,roam: true开启缩放拖拽,这是县级地图几乎必开的功能,因为单县边界细节多,用户需要缩放查看。

3.2 渲染配置:地图初始化与 visualMap 联动

有一说一,单纯的县区边界图意义不大,实际项目里更多是往地图上叠数据做可视化。资源包的方式一示例里还带了一段visualMap配置,这能把数值映射成不同的颜色深浅,让数据分布一眼可见:

chart.setOption({ visualMap: { min: 0, max: 500, left: 20, bottom: 20, inRange: { color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695'] } }, series: [{ type: 'map', map: '某县', data: [ { name: '某县', value: 358 } ] }] });

参数说明:visualMap.min和max决定颜色映射的数值区间,超出区间的值会被钳制到端点颜色;inRange.color是一个颜色数组,数值从小到大对应数组从左到右,实际项目中建议根据业务场景调整配色,比如用红绿渐变表示正负值。这里我一般会踩一个坑——数据里的name是中文全称(比如“某县”),如果地图注册名是简称(比如“某”),匹配就会失败,整个县区直接不渲染。所以要养成在data数组里填写 GeoJSONproperties.name完整值的习惯。

3.3 适用场景与改造要点

方式一最适用的场景是单县区或者少量县区的展示,比如某个县的人口热力图、某个区县的 GDP 分布、或者固定区域的疫情数据看板。它的优点是代码量少、逻辑直观、不需要处理异步竞态,浏览器打开即渲染,没有任何网络请求延迟。

改造时常见的两个方向:一是把require替换成远程 http 请求获取 GeoJSON,适合地图数据统一存放在 CDN 或后端的场景;二是把data数组替换成从接口拉取的业务数据,核心就是保持name或adcode能和 GeoJSON 里的属性对应上。如果业务上需要同时展示多个临近县区,更推荐用方式二的合并策略,下面详细说。

4. 方式二:异步加载与按需合并,适合平台化项目

4.1 核心代码:Promise 加载 344 个文件之后合并

方式二解决的核心问题是 344 个文件不能一次性全部注册——否则浏览器有几百个地图对象占内存,页面卡顿。更合理的做法是:先用一份县区清单渲染一个总览地图(可以是省级轮廓),用户点击某个县时再去加载对应 GeoJSON 并注册渲染。demo-async.html 里的思路就是异步按需加载,核心逻辑如下:

// 1. 准备一个加载函数:输入 adcode,返回注册好的地图名 async function loadCountyMap(adcode) { const res = await fetch(`./data/geojson/${adcode}.json`); const geo = await res.json(); const name = geo.features[0].properties.name; if (!echarts.getMap(name)) { // 避免重复注册 echarts.registerMap(name, geo); } return name; } // 2. 根据区县 id 动态渲染 async function renderCounty(adcode, value) { const mapName = await loadCountyMap(adcode); chart.setOption({ series: [{ type: 'map', map: mapName, data: [{ name: mapName, value }] }] }); } // 3. 初始化时先展示某个默认县区 renderCounty('110101', 358);

这段代码最值得注意的就是echarts.getMap(name)的存在性检查。如果没有这一步,用户反复点击同一县区时会重复注册同名地图——echarts 虽然后注册的同名地图会覆盖前一个,但这一过程会有短暂的白屏和重新渲染开销。fetch返回的res.json()是异步的,用await挂起保证拿到完整 GeoJSON 再注册,避免后续渲染时找不到地图。这种方式下,344 个文件只有被访问到的才会进入注册池,内存占用大幅下降。

4.2 性能参数:加载策略与缓存

从工程上看,异步加载只是第一步,真正让 344 个县区可用的关键是配套的缓存策略。demo-async.html 在代码注释里提到一种常见做法——用fetch加上接口层的 HTTP 缓存,或者在 JavaScript 层做 Map 缓存,减少重复请求:

// 用 JavaScript Map 做一级缓存,避免重复 fetch const geoCache = new Map(); async function loadCountyMap(adcode) { if (geoCache.has(adcode)) { return geoCache.get(adcode).name; } const res = await fetch(`./data/geojson/${adcode}.json`); const geo = await res.json(); const name = geo.features[0].properties.name; echarts.registerMap(name, geo); geoCache.set(adcode, { name, geo }); return name; }

参数说明:geoCache以adcode为键存储已加载的 GeoJSON 引用,下一次点击同一县区时直接从内存取,请求时间为零。这里的适配成本很低——如果你的项目用的是 axios 而不是 fetch,把请求函数替换掉即可。对于数据量级更大的场景,还可以在 HTTP 层设置Cache-Control: max-age=86400,让浏览器缓存 GeoJSON 响应,这样即使刷新页面也不需要重新下载边界数据。两种缓存叠加之后,344 个县区的资源包在真实业务里基本感受不到重复加载的延迟。

4.3 两种方式的对比与选择建议

对比维度方式一:本地全量注册方式二:异步按需加载
首屏速度慢(所有县区一次加载)快(只用加载默认县区)
内存占用高(注册全部地图对象)低(仅已访问县区)
代码复杂度低,同步逻辑中,涉及 async/await
适合场景单县展示、内网工具页地图下钻、平台级应用
工程扩展性弱,加县区要改代码强,按 adcode 随意切换

我的建议是:如果你的项目只是一个固定的县区看板,比如某高校要做校内数据可视化展示,用方式一足够;如果做的是面向多区域的平台系统,需要用户在不同县区间切换查询,方式二是更稳妥的架构。方式二踩坑的地方主要在于异步时序——用户在快速切换县区时,前一个setOption还没执行完,后一个就来了,解决办法是用一个简单的请求序号,只让最后一次请求更新图表:

let requestSeq = 0; async function renderCounty(adcode, value) { const seq = ++requestSeq; const mapName = await loadCountyMap(adcode); if (seq !== requestSeq) return; // 已有新请求,放弃本次结果 chart.setOption({ /* ... */ }); }

这段代码虽然只有三行,但解决了一个很影响体验的翻车点——用户连续点击 A 县、B 县时,如果 A 县的加载时间比 B 县长,则 A 县的渲染结果可能后到并覆盖 B 县,加了序号判断后过期请求自然丢弃。

5. 避坑指南:画县级地图最容易翻车的四个地方

5.1 坑一:地图区域不显示,控制台也没有报错

现象:setOption执行后,地图区域空白,series 数据也没报错,但页面就是什么都不渲染。

原因:90% 的概率是map: '某县'的字符串和registerMap注册时的名字不一致——比如注册时用的properties.name值是“某某县”,而这里写成了简称“某某县”;另一个原因是 GeoJSON 内部的properties缺失或字段名不是name,导致 eCharts 无法识别区域名。

解决:打印geo.features[0].properties看实际字段,可以在注册前用console.log(name)验证一下;也可以通过echarts.getMap('某县')查询该地图是否注册成功,返回undefined就说明名字对应不上。血泪教训:这种问题通常是静默失败的,建议在renderCounty里加一层判断,找不到地图时主动抛错,而不是让人盯着空白的画布发呆。

5.2 坑二:地图位置偏移到海里,或者和底图错位

现象:地图渲染出来了,但位置明显不对,有的跑到海外,有的县界和相邻县重叠。

原因:GeoJSON 的坐标系不统一。标准地理数据用 WGS84(GPS 原始坐标系),而部分在线获取的数据经过 GCJ-02 加密偏移,两种混用就会导致错位。这份 344 县区资源基本已统一,但如果你二次开发时混入外部数据,这个问题立刻暴露。

解决:所有输入的地图数据尽量从同一来源获取,且优先选择标注“WGS84”或“CGCS2000”的数据。如果确实混入了 GCJ-02 的数据,可以写一个小工具做坐标转换再合并。这里我一般会用proj4库做投影转换,但考虑到项目体量不大时,直接放弃该数据源往往更省成本。

5.3 坑三:数据 name 匹配不上,地图上所有区域一个颜色

现象:地图能渲染,但所有区域颜色相同,visualMap似乎没生效。

原因:series.data数组里的name值与 GeoJSON 里的区域名对不上时,eCharts 会把该数据项丢弃,视觉上就是所有区域都落入默认色。这是县级地图最常翻车的地方,因为各区县命名习惯不统一——有的叫“xx区”,有的 GeoJSON 里叫“xx城区”,差一个字就匹配失败。

解决:统一用adcode做匹配是一个好办法,让后端接口返回adcode而不是名称,前端数据进入series.data时通过countyList.json把adcode映射成正式名称,从根上消掉命名偏差。如果数据端改不了,就建一个别名映射表,在渲染前把历史数据里的“xx老城区”“xx新城区”清洗成标准名。

5.4 坑四:344 个 GeoJSON 全部注册后页面卡顿

现象:一次性把 344 个县区全部registerMap后,页面滚动和缩放明显掉帧,打开开发者工具发现内存占用很高。

原因:一个 GeoJSON 少则几十 KB,多则几百 KB,全部解析后浏览器需要保存几百个地图对象及边界坐标数组,内存和渲染引擎都无法承受。

解决:回到第 4 章的异步按需加载方案,除了默认展示的县区外,其余全部在用户交互时才注册。如果你的场景真的需要首屏展示 344 个区域,最好的方式不是逐个注册,而是把 344 个 GeoJSON 合并成一个FeatureCollection,注册一张总地图。资源包的data/merged/all_counties.json就是为这个准备的,第 6 章接入测试时我会详细演示合并注册的写法。

6. 二次开发:从“344 县区”到“任意区县下钻”的改造实践

6.1 改造思路:把静态资源变成可配置的地图服务

拿到这份资源包,我第一个动手的方向是做一个“省份 → 区县 → 边界详情”的三级下钻模块。原始 demo 把 344 个县区和特定业务的耦合太紧,改造的目标是抽出一层独立的地图加载服务。核心思路:用一份县区清单(countyList.json)驱动地图层级,点击上级区域时按需加载下级边界,业务数据通过adcode关联而不依赖名称。

// 地图加载服务:负责数据获取和注册,对外只暴露 render 方法 const CountyMapService = { cache: new Map(), async ensureLoaded(adcode) { if (this.cache.has(adcode)) return this.cache.get(adcode); const res = await fetch(`./data/geojson/${adcode}.json`); const geo = await res.json(); const name = geo.features[0].properties.name; if (!echarts.getMap(name)) echarts.registerMap(name, geo); this.cache.set(adcode, name); return name; }, async render(adcode, bizData) { const mapName = await this.ensureLoaded(adcode); const chart = this.chart; chart.setOption({ visualMap: { /* ... 配置 */ }, series: [{ type: 'map', map: mapName, data: [{ name: mapName, value: bizData.value || 0 }] }] }); } };

这个改造把地图的“数据加载”和“业务渲染”解耦了,后续接任何业务只需要在bizData上做文章。有一点值得注意:文件名的adcode在资源包里统一是 6 位数字,而业务接口常返回的是省市区带连字符的编码,比如“110101”和“110101-001”,需要提前把非标准格式转换掉,否则请求直接 404。

6.2 验证方法:用随机数据做一次下行链路测试

改造完别急着接真实业务,先用一段随机数据验证整条链路是通的。我习惯的做法是写一个小的自测页面,遍历countyList.json中的 10 个县区,逐个调用CountyMapService.render,观察地图是否都有边界出图、数据点是否落在对应区域。代码大致是这样:

// 自测:随机取 10 个县区,依次渲染验证 const sample = countyList.slice(0, 10); for (const county of sample) { const ok = await CountyMapService.render(county.adcode, { value: Math.random() * 500 }); if (!ok) console.warn(`渲染失败: ${county.name}`); } // 检查 echarts 内部已注册地图数量 console.log('已注册地图数:', Object.keys(echarts.getMap()).length);

这个验证环节能帮你快速排查三类问题:文件路径是否匹配、GeoJSON 是否能被正常解析、注册名是否能被 map 配置引用。等验证通过后再接真实业务数据,基本就能把排查范围缩小到数据本身。从那以后我每次做地图类项目都强制走一遍这个流程——先随机样本验证地图链路,再接业务数据,这个习惯帮我躲过了至少 5 次因数据命名不统一引起的返工,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进