资讯详情

资讯详情

eCharts县域地图实战:344个县区GeoJSON与两种加载方案

简介面向需要在中国县级行政区划层面做数据可视化的前端开发者这份 eCharts 地图资源专门整理了 344 个县区的完整地图数据并附带两种实现方式的示例 Demo直接解决县区级地理数据获取难、配置繁琐的常见问题。这两种方式分别对应预置地图数据集的快速引用以及通过自定义 JSON 边界数据完成地图集成覆盖人口统计、经济指标、地域分布等常用可视化场景也方便初学者对比不同写法的差异。压缩包共 391 个文件以 380 个 JSON 地理数据文件为主配合少量 JS 脚本、HTML 示例页面、project 工程文件以及 1 份 docx 说明文档整体体积仅 2.82MB结构轻量下载后即可直接运行和二次修改。目前已有 1552 人学习下载适合具备基本 HTML/JavaScript 基础、希望快速上手县区级地图的前端开发者和数据分析人员。参照示例可以掌握引入 ECharts 库、准备地图数据、初始化容器、配置 option、绑定悬浮与点击交互的完整链路还可根据项目需要选择最合适的数据接入方式同时文档中也对地图不能下载或加载异常等问题给出了排查思路能有效缩短开发和调试时间。1. eCharts地图344个县区一套能直接用的中国县域GeoJSON与两种加载方案做数据可视化的人十有八九都被“地图数据”卡过脖子。省市级的地图网上随便一搜就是一堆但一落到县区级要么数据残缺要么坐标系混乱要么素材老旧到连地名都对不上。这份eCharts地图资源一次性整理了344个县区的GeoJSON数据并附带了两种引入方式的demo解决了从“拿到数据”到“渲染上屏”之间最磨人的那段路。它不是教程页面是拆开就能用的代码资料包适合正在做县域经济分析、人口热力分布或者区划数据看板的前端开发者。我当初拿到这套数据时第一反应是“终于不用再满网找边界文件了”但真正把它跑起来之后发现更值钱的是demo里对边界数据异常和加载时序的处理思路。2. 县区级地图的底层原理为什么GeoJSON比常规写法更适合县域渲染2.1 弄清楚GeoJSON的结构与坐标系是第一步不是废话我们直接看一份从这套资源里抽出来的县区边界数据长这样{ type: FeatureCollection, features: [ { type: Feature, properties: { adcode: 110101, name: 东城区, center: [116.418, 39.917] }, geometry: { type: MultiPolygon, coordinates: [ [ [ [116.388, 39.941], [116.392, 39.935], [116.404, 39.935] ] ] ] } } ] }这段数据结构里有两个关键点。第一是properties它承载着每个县区的行政区划代码adcode、名称和中心点坐标我们的 tooltip、label、事件绑定都要从这里取数据第二是geometryMultiPolygon意味着这个区域可能由多个不相连的多边形组成典型的例子是那些下辖岛屿的区县。eCharts 的registerMap方法就是读取这套结构生成地图实例。关于坐标系这套数据用的是 GCJ-02 坐标系也就是俗称的国测局坐标。如果你手里的业务数据是从 GPS 设备或者原始的 WGS-84 坐标采集来的那么直接塞进地图里会出现几百米的偏移。常见做法是在数据入库前做一次坐标转换把 WGS-84 转成 GCJ-02或者在拿到经纬度之后先跑一遍转换函数再交给 eCharts 渲染。注意判断一份 GeoJSON 能不能直接用最简单的办法是拿center字段里的坐标画一个点再拿对应区划的边界画个面两者叠在一起如果吻合就没问题偏移超过五百米就得检查坐标系了。2.2 动态注册地图是县区级渲染唯一靠谱的姿势eCharts 渲染地图的流程不像折线图那样“丢数据进去就完事”。它必须先走registerMap把 GeoJSON 注册成一个 echarts 能识别的名称然后再用这个名称去初始化 option 里的map字段。这套资源里的 demo 也是这么做的// 引入eCharts核心 import * as echarts from echarts; // 引入这份资源里提供的县区GeoJSON import countyData from ./geo/344_counties.json; // 关键先把地理数据注册成名为county_map的地图 echarts.registerMap(county_map, countyData); // 拿到容器实例 const chartDom document.getElementById(chart-container); const myChart echarts.init(chartDom); // 在option中引用已注册的名称 const option { tooltip: { trigger: item, formatter: function (params) { return params.name br/ (params.value || 无数据); } }, series: [ { type: map, map: county_map, roam: true, label: { show: true, fontSize: 10 }, data: [ { name: 东城区, value: 86 }, { name: 西城区, value: 72 } ] } ] }; myChart.setOption(option);这段逻辑有几个值得展开说明的细节。roam: true开启了缩放和平移县区级地图在交互时经常要放大看某个具体区域这个参数建议保持开启。label.show在边界密集的县区地图上要谨慎使用因为县域面积小、名字又长全量显示 label 很容易糊成一团我一般会把fontSize压到 10px 甚至更低或者在缩放级别达到一定阈值时才显示名字。data里的name字段必须和 GeoJSON 里properties.name完全一致差一个空格就匹配不上这是一个非常隐蔽的坑后面避坑章节我会再详细说。3. 两种demo的实战拆解script标签直引与npm模块化3.1 方案一script标签直接加载适合无需构建环境的临时看板这套资源里第一种 demo 走的是最传统的路子直接通过 script 标签把 eCharts 和 GeoJSON 引进来。这种方式的适用场景很明确比如你做的是一个临时性的数据看板或者公司的内网管理系统不方便上构建工具又或者只是想在本地快速验证一下数据长什么样。它的核心代码结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 title县区地图-直引方案/title style #map { width: 100%; height: 600px; border: 1px solid #eee; } /style /head body div idmap/div !-- 先引eCharts核心库 -- script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script !-- 再引本地GeoJSON文件 -- script src./geo/344_counties.json/script script // 注意json文件被script引入后会以js对象形式暴露出来 const countyData window.countyData || JSON.parse( document.getElementById(county-data).textContent ); // 注册地图 echarts.registerMap(county_map, countyData); const chart echarts.init(document.getElementById(map)); chart.setOption({ series: [ { type: map, map: county_map, label: { show: true }, data: [ { name: 海淀区, value: 128 }, { name: 朝阳区, value: 95 } ] } ] }); /script /body /html这里有个实操中很容易踩的细节直接用script src./geo/344_counties.json这种方式引入 JSON 文件不一定能通过window访问到数据因为浏览器对纯 JSON 文件的 script 引入没有统一的处理规范。更稳的做法是用 fetch 去拉fetch(./geo/344_counties.json) .then(res res.json()) .then(countyData { echarts.registerMap(county_map, countyData); // 剩余渲染逻辑 });fetch 方案的另一个好处是浏览器会异步加载不会因为 GeoJSON 文件体积过大而阻塞页面首屏渲染。这套资源里的 344 个县区合并数据文件大小通常在一百多KB到几百KB之间在本地服务和内网环境下还好如果部署到公网就必须考虑用 gzip 压缩传输或者做数据裁剪。3.2 方案二npm模块化引入适合Vue/React工程化项目第二种 demo 面向的是正式的工程化项目这也是我在实际业务中采用的方式。资源里给出了一个基于 Vite Vue 3 的示例结构核心代码长这样// 在Vue3组件中引入 import * as echarts from echarts; import { onMounted, ref, shallowRef } from vue; import countyGeo from /assets/geo/344_counties.json; // 用shallowRef管理chart实例避免Vue的响应式代理干扰echarts内部状态 const chartRef ref(null); const chartInstance shallowRef(null); onMounted(() { // 注册地图 echarts.registerMap(county_map, countyGeo); chartInstance.value echarts.init(chartRef.value); chartInstance.value.setOption({ tooltip: { trigger: item, formatter: (params) { const val params.value ?? 0; return ${params.name}${val}; } }, visualMap: { min: 0, max: 100, left: left, top: bottom, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [ { type: map, map: county_map, roam: true, scaleLimit: { min: 1, max: 5 }, itemStyle: { borderColor: #ccc, borderWidth: 0.5 }, emphasis: { itemStyle: { areaColor: #ffd666 } }, data: [ { name: 东城区, value: 86 }, { name: 朝阳区, value: 112 } ] } ] }); });模块化方案和直引方案最大的区别不只是代码组织方式更重要的是import机制让 GeoJSON 变成了打包过程中的一个静态资源模块。Vite 会把它做一次 JSON 解析并缓存后续组件的热更新不会再触发重复的网络请求。还有个细节是visualMap组件的使用——县域数据如果要按数值做色阶展示visualMap的min和max必须提前根据你的数据范围计算好否则高值县区全部显示最深色低值县区全是浅色视觉区分度会非常差。我一般会先遍历一遍数据求最大最小值再把它们动态设置到visualMap里。3.3 两种方案的选型建议从项目形态和团队技术栈出发这两种 demo 的选型逻辑其实不复杂。如果你只是做一个内部用的临时页面、或者手里只有静态HTML文件、不愿意引入node_modules那一大堆依赖方案一足够了。它的运行条件只有一个能打开浏览器并访问到 echarts 的 CDN 地址。但方案一的隐患在于 CDN 外链如果挂了地图就白屏了。方案二适合代码需要长期维护、迭代频繁的产品级项目。模块化引入让地图数据和业务代码处在同一个构建体系内版本有保障也方便做按需加载。我个人的经验判断是凡是活在代码仓库里、后续有小迭代需求的项目直接上方案二凡是跑在单机环境、生命周期不超过三个月的临时工具方案一更省心。这两种方式资源包里都给了完整demo不存在取舍问题。4. 县区级地图的常见坑从边界数据到渲染时序4.1 细节一数据匹配不上——name字段完全一致才算数这个坑我印象很深第一次用这套数据时拿业务库的县区名称直接去匹配结果地图上空白一片只显示轮廓不显示任何高亮区域。后来检查发现业务库里写的是“东城区”GeoJSON 的 properties 里也是“东城区”但业务库里有的县区名称带了一个空格或者用了简称。eCharts 的 map 系列做数据关联时用的是严格的全等匹配。解决的办法是在塞数据之前做一次名称归一化处理// 先读取GeoJSON里的合法名称列表 const validNames new Set( countyGeo.features.map(f f.properties.name) ); // 过滤业务数据中无法匹配的项 const filteredData rawData.filter(item validNames.has(item.name)); // 可选打印出没匹配上的数据人工核对 const unmatched rawData .filter(item !validNames.has(item.name)) .map(item item.name); console.warn(以下名称未匹配到地图数据, unmatched);这段代码的逻辑是先构建一个合法名称的Set集合再过滤业务数据。这样做的意义在于与其让 eCharts 在渲染时静默丢弃匹配不上的数据不如在前置环节显式地发现同义词问题。4.2 细节二拖动缩放时图形变形——坐标系被强制拉伸还有一次遇到的情况是地图渲染出来了但整个图形是扁的圆形湖泊变成了椭圆。问题出在容器元素上——echarts 初始化时读取的是容器当前的宽高如果容器宽度和高度不是按地图比例设置的渲染出来的地图就会被拉伸。针对这种状况最直接的解决方法是给容器设定一个合理的宽高比例或者在初始化后监听容器尺寸变化手动调chart.resize()// 容器尺寸变化时重绘 window.addEventListener(resize, () { chart.resize(); });如果你明确希望地图按等比例缩放且保持固定宽高比可以在series里加一个aspectScale参数这个参数控制了地图宽高比的自适应范围。不同的 GeoJSON 数据因为纬度跨度不同默认的宽高比也不一致aspectScale太小图形会被拉扁太大会被拉高需要先出图看一眼再微调。4.3 细节三地图外轮廓有零散小岛——边界噪声的滤波处理这类问题出现在某些沿海或者江河交汇区域的县区。GeoJSON 数据里会包含很多面积非常小的多边形碎块渲染出来就是地图边缘多了一些针尖大小的色块视觉上非常脏。这套数据里同样存在类似现象我处理时用了一个基于面积的过滤函数function filterTinyPolygons(feature, minArea 0.0001) { const geometry feature.geometry; if (geometry.type Polygon) { return polygonArea(geometry.coordinates) minArea; } if (geometry.type MultiPolygon) { geometry.coordinates geometry.coordinates.filter( coords polygonArea(coords) minArea ); } return true; }判断面积的方法是用鞋带公式遍历多边形顶点计算近似面积。这里需要注意minArea的具体取值取决于你的坐标系单位在 GCJ-02 坐标系下经度跨度一度约为一百公里级别0.0001 对应的实际面积大约就是一平方公里以下的小岛。过滤时要留意别把那些面积虽小但属于行政区域主体的飞地给过滤掉了最稳妥的操作是先跑一遍过滤函数然后逐个检查被筛掉的多边形名称。4.4 细节四GeoJSON加载时序问题——初始化必须在数据到位之后这个坑在工程化项目里尤其常见。有人在onMounted里直接写echarts.init但 GeoJSON 是异步 import 进来的初始化动作先执行了等到数据返回再注册地图就晚了。表现就是页面上这个部分完全空白控制台不报错排查起来极其迷惑。正确顺序永远是先拿数据、再注册、再初始化、最后setOption。如果采用异步加载代码是这样组织// 异步加载GeoJSON完成后再初始化图表 async function initCountyMap() { const geoModule await import(./geo/344_counties.json); echarts.registerMap(county_map, geoModule.default); const chart echarts.init(document.getElementById(map)); chart.setOption(option); } initCountyMap().catch(err { console.error(地图数据加载失败, err); });把动态import和异步函数组合在一起数据加载、地图注册、实例初始化整个流程串行执行顺序就有保证了。这也是我习惯了在工程化项目里使用的写法不依赖外部状态去保证时序。5. 从demo到项目落地的改造指南数据格式、多级联动与性能优化5.1 业务数据如何正确转换成eCharts所需的散点映射格式很少有人会把 demo 里的静态数据直接用于生产环境通常业务数据来自接口且字段命名五花八门。常规做法是在拿到原始数据后做一次中间层转换我在实际项目中习惯先写一个纯函数的映射器/** * 业务数据转地图数据 * param {Array} bizData 业务接口返回的数据 * param {string} nameField 名称字段 * param {string} valueField 数值字段 * returns {Array} 转换后的地图数据 */ function toMapData(bizData, nameField, valueField) { return bizData.map(item ({ name: item[nameField], value: item[valueField] })); }这个转换函数做的工作很朴素但它的价值在于统一了数据出入口。如果接口返回的数据里有超出 344 个县区范围的外地数据在这个环节就可以顺手过滤掉。还有一个需要注意的细节是value字段的类型eCharts 在做色阶映射时如果遇到字符串类型的数字偶尔会表现异常所以转换时最好顺手Number()一次。5.2 多级下钻从省级到县区的缩放联动实际项目中常见的需求是点击某个省、下钻到该省下辖各县区的数据面板。做这一类联动有几个具体的实现路径但核心思路都是在点击地图区域时用adcode作为筛选条件去切换 GeoJSON 和业务数据。在这套资源处理中一个比较顺手的办法是先注册省市级的地图集合然后预加载所有县区的 GeoJSON// 点击下钻 myChart.on(click, function (params) { const adcode params.adcode; // 在registerMap时挂到properties里 const targetGeo countyGeoMap.get(adcode); if (targetGeo) { // 重新注册新的地图作用于当前实例 echarts.registerMap(current_county, targetGeo); myChart.setOption({ series: [ { map: current_county, data: getCountyData(adcode) } ] }); } });这里我习惯给每个县级 GeoJSON 的properties里挂上省级上级的adcode这样在点击事件里可以通过params.adcode拿到当前区域编码。下钻的实质是重复调用setOption替换 series 里的map字段指向不需要销毁图表实例。返回上一级时同理只需要把省市级 GeoJSON 再注册回去。5.3 性能优化数据裁剪、按需注册和防抖渲染说到性能县级地图和大区域地图的差异非常明显。344 个县区的边界数据全部渲染如果直接塞给 eCharts 一次画出来首屏要承受较大的绘制压力。针对这种情况我做三层优化第一层是数据裁剪。如果当前视口只需要展示某几个县区那在保存 GeoJSON 时就按区域分包存储不让全局地图一次性加载。第二层是懒注册。地图实例初始化时不注册任何区域当用户缩放到某一个层级时再把对应的 GeoJSON 注入。第三层是在业务数据的频繁变化场景下使用防抖// 业务数据更新时防抖渲染 function debounceRender(chart, nextData, wait 300) { let timer null; return function () { if (timer) clearTimeout(timer); timer setTimeout(() { chart.setOption({ series: [ { data: nextData } ] }); }, wait); }; }也就是说当你在一个看板里同时放多个地图实例、或者数据每隔几秒刷新一次时防抖能把重复的渲染任务合并成一次。这个套路的底气在于 GeoJSON 数据通常变化频率极低需要高频更新的只是散点数值。5.4 字体与label优化县域密集区域的可读性设置县区地图因为区域密集label 展示策略直接影响阅读体验。344 个县区如果全部常显名称文字互相遮挡的问题非常严重。我采用的取舍方式是只在缩放级别达到一定程度后显示 label。eCharts 没有直接提供按缩放程度控制 label 显隐的参数需要靠监听georoam事件来判断myChart.on(georoam, function () { const zoom myChart.getOption().series[0].zoom; // 当前缩放比例 myChart.setOption({ series: [ { label: { show: zoom 3, // 放大到3倍以上再显示名称 fontSize: zoom 5 ? 12 : 9 } } ] }); });虽然这个监听写法会频繁触发setOption但相比每次缩放都让图形重绘的代价这个开销完全可接受。如果你用的县区名比较长还可以配合label.overflow设置截断方式避免名称超出区域边界。6. 一条必须掌握的验证路径渲染前检查数据渲染后核对坐标拿到这套数据后不要急于套进业务代码里建议先走一遍快速验证流程。我每次拿到新的 GeoJSON 都会在本地起一个最简单的 HTML 页面或一个空白的 Vue 工程先只渲染地图、不给 series 传任何业务数据确认轮廓和边界数据完整。这一步能排除 90% 的“地图空白”问题。接着第二步我用地图本身的center坐标属性生成几个散点叠加在图上逐一点击核对经纬度是否落在对应区域内。举个例子某县区的center是 [116.4, 39.9]那这个点应该落在该县区轮廓内点出去很远就说明坐标系有问题。第三步才接入业务数据按照前面说的转换函数和名称过滤逻辑把业务数据渲染出来。渲染完成后我会随机抽三个县区点击一下看看 tooltip 里的名称和值是否正确。这三步全部通过这个地图模块才能正常交付。回到这套资源本身它的价值在于把最耗时的数据整理工作做完了。344 个县区的 GeoJSON 以及两种加载 demo 都已经封装好剩下的就是按你自己的业务字段做映射。我到现在还很清楚地记得某次项目交付前一刻因为一个县的名称多了个空格地图上整整五个区域没有数据排查了三个小时。从那以后我每次跑地图数据都会强制走一遍名称匹配校验把不匹配的项提前暴露在控制台里而不是等渲染结束再盯着空白区域发呆。这份资源里的数据文件结构是标准的 GeoJSON 格式只要你不去手工改动里面的 name 字段按我上面说的验证流程走一遍基本不会有意外情况。希望这些经验和代码能帮你在做县域地图可视化时少走一段弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →