尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

杭州街道级GeoJSON在ECharts地图可视化中的应用与优化

杭州街道级GeoJSON在ECharts地图可视化中的应用与优化 简介面向Web可视化场景的杭州市街道乡镇级GeoJSON地图数据包解决开发者在制作精细化行政区划地图时缺乏公开数据的问题。覆盖上城、拱墅、滨江、萧山等主要城区及建德、淳安、桐庐等县市每个文件对应一个区划单元按区县拆分为13个JSON文件压缩后仅1.91MB便于按需加载与二次加工。数据可直接用于ECharts地图、Leaflet等前端框架实现行政区域着色、人口或GDP等指标分组统计展示也可配合后端接口快速搭建城市大屏适合Web开发者和数据可视化爱好者使用。已有377人学习下载。文件命名直观清晰路径层级简单能够帮助使用者快速定位任意区划数据无需额外清洗整体轻量适合在项目资源中随取随用是制作杭州地图看板或街道乡镇级统计图表的实用素材。1. 街道级GeoJSON为什么区县边界在统计地图里不够用做统计地图的人都有个共同的痛点市级边界一眼能看清全区全貌可一旦被问到“这批数据落在哪条街道”市级GeoJSON就彻底帮不上忙。杭州这份数据集把每个区县市单独拆成一份GeoJSON文件边界精确到街道乡镇一共13个文件天然适配ECharts的registerMap和按名称匹配的data映射机制。我拿到这套数据第一反应是直接丢给echarts.registerMap结果发现事情没那么简单文件名的全路径命名、properties里的字段结构、坐标系到底是经纬度还是投影坐标每一样都在影响最终渲染结果。这篇内容就顺着这条链路往下拆覆盖GeoJSON结构解析、ECharts注册加载、按街道聚合统计的完整实现以及几个会反复踩到的匹配规则。适合正在做指挥大屏、园区分析、网格化运营数据落图的从业者参考。2. 拆包杭州区县GeoJSONFeatureCollection结构与属性字段定位2.1 文件命名规则与包内组织逻辑解压后的目录结构非常规整13个文件全部平铺在根目录下330100.zip ├── 浙江省杭州市拱墅区.json ├── 浙江省杭州市建德市.json ├── 浙江省杭州市余杭区.json ├── 浙江省杭州市淳安县.json ├── 浙江省杭州市下城区.json ├── 浙江省杭州市上城区.json ├── 浙江省杭州市临安市.json ├── 浙江省杭州市萧山区.json ├── 浙江省杭州市江干区.json ├── 浙江省杭州市桐庐县.json ├── 浙江省杭州市富阳市.json ├── 浙江省杭州市西湖区.json └── 浙江省杭州市滨江区.json这套命名规则值得留意。“浙江省杭州市拱墅区.json”这样的全路径格式避免了多个城市出现同名区时互相覆盖的问题——比如“西湖区”在杭州和南昌都有如果只叫“西湖区.json”一旦注册两个地图后注册的会覆盖先注册的。带上省市前缀几个城市的数据包放在同一个项目里也不会冲突。和“一整个杭州一个json”的做法相比按区拆分的文件组织方式更利于下钻场景首屏可以先注册市级地图用户点击某个区时再动态加载对应区的GeoJSON文件而不是一次性把全市所有街道的边界都塞进浏览器。街道级的geometry比区县级复杂得多顶点数量动辄翻数倍全量加载对首屏渲染性能影响明显。2.2 用Node快速验证GeoJSON结构和字段命名在把数据交给ECharts之前我习惯先写一段几行的Node脚本看一眼结构确认properties里到底有哪些字段。不同来源的GeoJSON字段命名差异很大有的用name有的用NAME还有的干脆只给adcode不给名称。const fs require(fs); const path require(path); const file path.join(./geojson, 浙江省杭州市拱墅区.json); const data JSON.parse(fs.readFileSync(file, utf-8)); console.log(顶层type:, data.type); console.log(feature数量:, data.features.length); const first data.features[0]; console.log(首条feature属性:, JSON.stringify(first.properties, null, 2)); console.log(geometry类型:, first.geometry.type); console.log(首坐标点:, JSON.stringify(first.geometry.coordinates[0][0][0]));这段脚本做了四件事读取文件、JSON.parse反序列化、输出feature总数、打印第一条feature的属性和几何信息。运行后你能看到类似FeatureCollection的顶层type以及properties里应该有一个name字段比如“米市巷街道”这个name就是之后ECharts按名称匹配数据的键。如果看到的是NAME大写或者没有name需要在注册时显式指定nameProperty。GeoJSON的属性字段在不同生产方之间差别很大常见的有这几种字段名含义使用场景name街道或区县名称ECharts默认的data匹配键adcode行政区划代码后端接口按code联查数据center中心点经纬度数组地图初始化时定位视野childrenNum下级行政区数量判断是否有下钻层级2.3 坐标系判定经纬度还是投影坐标拿到GeoJSON后先别急着注册花十秒钟验证一下坐标系。ECharts的地图坐标系使用经纬度如果数据源是投影坐标渲染出来的图形会偏移或者干脆变形到不可用。判断方法很简单打印geometry里的第一个坐标点看数值量级。杭州的经度范围大约在118.3°E到120.4°E之间纬度在29.1°N到30.5°N之间。如果打印出来的坐标是类似[118.34, 30.29]这样的数值就是经纬度坐标可以直接用。如果看到[35350234, 3536221]这种百万甚至千万量级的数值说明坐标经过了投影转换需要先做坐标反算把投影坐标转回WGS-84经纬度再交给ECharts消费。正常情况下给可视化用的行政区划GeoJSON已经是经纬度格式但数据来源不可控时这个检查步骤不要省。3. 在ECharts里注册街道级地图加载、数据映射与tooltip3.1 用registerMap注册本地GeoJSON文件确认数据结构无误后就可以把GeoJSON注册成ECharts可用的地图。以拱墅区为例注册代码非常简短import * as echarts from echarts; import hzGs from ./geojson/浙江省杭州市拱墅区.json; echarts.registerMap(拱墅区, hzGs); const chart echarts.init(document.getElementById(mapContainer)); chart.setOption({ series: [{ type: map, map: 拱墅区, roam: true, label: { show: true, fontSize: 10 } }] });registerMap的第二个参数直接接收GeoJSON对象ECharts内部会解析features数组并默认用properties.name作为每个区域的唯一标识。map字段的值要和注册时的第一个参数一致。roam: true允许用户拖拽和缩放地图街道级别的区域面积小默认视野下很多街道名称挤在一起开启roam后用户可以自由放大查看细节。如果你使用的GeoJSON里名称字段不是name注册时传入第三个参数指定即可echarts.registerMap(拱墅区, hzGs, { nameProperty: NAME });3.2 把统计指标映射到街道级区域地图注册完成只解决了“画轮廓”的问题真正的业务需求是把统计值填到对应街道上。ECharts的map系列通过data数组里的name字段与GeoJSON的properties.name做精确匹配const option { series: [{ type: map, map: 拱墅区, data: [ { name: 米市巷街道, value: 233 }, { name: 小河街道, value: 189 }, { name: 拱宸桥街道, value: 317 } ] }] };匹配规则是严格的全等比对。data里写的name必须和GeoJSON里properties.name完全一致多一个空格、写错一个字这个街道就不会渲染颜色而且ECharts不会报任何错误表现形式是该区域显示为透明或默认色。我在项目里遇到最多的问题就是这里后面第四章会详细说排查方法。如果data里的数据比GeoJSON的feature少剩下的街道会以默认色显示但不会报错视觉上就是地图上有一片区域没上色这种问题很容易被误认为代码逻辑写错了。3.3 visualMap与tooltip配置数据映射到区域后还需要visualMap把数值转成颜色梯度以及配置tooltip让鼠标悬停时能查看具体数值。这两块是统计地图的核心交互。const option { tooltip: { trigger: item, formatter: function(params) { return params.name br/统计值 (params.value undefined ? 0 : params.value); } }, visualMap: { type: continuous, min: 0, max: 500, left: 20, bottom: 20, inRange: { color: [#e0f3f8, #abd9e9, #2c7bb6] } } };visualMap的min和max建议根据实际数据动态计算而不是写死。如果业务数据范围是0到100max设成500会让所有街道颜色都偏浅区分度非常差。我一般用Math.max(...values)拿到最大值再乘一个1.1的系数作为max。tooltip的formatter里params.value在ECharts 5的map系列中返回的是data里配置的value值如果某条街道没有数据params.value会是undefined需要兜底显示0否则悬停到无数据街道时tooltip会显示“undefined”非常影响交付观感。visualMap还有几个常用参数值得记录参数作用推荐值typecontinuous连续型 / piecewise分段型数据跨度大用piecewiseseriesIndex控制作用于哪个系列多系列混排时必须显式指定calculable是否显示拖拽手柄面向业务演示时建议开启text图例两端文字如[“高”“低”]4. 按街道聚合统计的完整链路从数据台账到图表渲染4.1 台账数据与GeoJSON名称对齐实际项目里统计数据往往以Excel或数据库表的形式存在字段通常是“街道名称”和“统计值”两列。直接用原始数据去匹配GeoJSON名称失败率很高原因是台账里的写法多种多样“拱墅区米市巷街道”“杭州市米市巷街道”“米市巷街道办”等等。这些写法在业务系统里是合理的但和GeoJSON里的标准名称对不上。第一步先做归一化处理把省市前缀剥离只保留到街道这一级。用正则处理即可const rawName 浙江省杭州市米市巷街道; const normalized rawName.replace(/^(浙江省|杭州市|浙江省杭州市)/, ); console.log(normalized); // 米市巷街道这个正则把“浙江省”“杭州市”以及两者的组合都匹配掉剩下的就是街道名。要注意“街道”和“街道办”“街道办事处”的差异GeoJSON里统一用“街道”结尾台账里多出来的“办事处”三个字需要一并处理否则仍然匹配不上。4.2 前端分组聚合reduce与Map两种写法数据量在十万行以内时在前端做分组聚合完全够用省去一次后端联调。以下是用reduce实现的按街道求和const rows [ { street: 米市巷街道, value: 23 }, { street: 米市巷街道, value: 55 }, { street: 小河街道, value: 10 }, { street: 拱宸桥街道, value: 88 } ]; const stat rows.reduce((acc, row) { const name row.street.replace(/^(浙江省|杭州市)/, ); acc[name] (acc[name] || 0) row.value; return acc; }, {}); const chartData Object.keys(stat).map(name ({ name: name, value: stat[name] }));reduce的回调函数里acc是累积对象每遍历一行数据就把对应的value累加到以街道名作为key的位置上。(acc[name] || 0)这一步是防止第一次遇到某个街道时acc[name]为undefined导致NaN。最后用Object.keys把对象转换成[{name, value}]结构正好匹配ECharts map系列的data格式。如果数据量再大一个量级可以用Map结构代替普通对象避免原型链上属性名的干扰const statMap new Map(); for (const row of rows) { const name row.street.replace(/^(浙江省|杭州市)/, ); statMap.set(name, (statMap.get(name) || 0) row.value); } const chartData Array.from(statMap, ([name, value]) ({ name, value }));Array.from的第二个参数是映射函数可以直接把[key, value]的二元数组转换成对象结构代码比reduce更紧凑性能也略好。两种写法结果一致选一种坚持用就行。4.3 后端预聚合SQL直接输出图表所需结构如果业务数据落在数据库里更高效的做法是让SQL直接完成分组聚合后端接口返回的JSON结构就是[{name, value}]前端拿到后不需要任何加工就能喂给EChartsSELECT street_name AS name, SUM(stat_value) AS value FROM biz_area_stats WHERE city_code 330100 GROUP BY street_name ORDER BY value DESC;330100是杭州市的行政区划代码前缀查询时按这个字段过滤可以避免其他城市的同名街道混入。GROUP BY的输出天然就是name和value两列配合Spring Boot或Express返回JSON时字段名也和前端约定的一致。这里有个实践要点SQL聚合是在数据库端完成返回的数据行数等于街道数量最多几百条对网络传输和前端渲染压力都很小。而如果把原始明细数据拉到前端再聚合可能要传几万行数据除非原始数据同时还要用于其他前端计算否则没有理由不在SQL里做掉。4.4 匹配不到的街道名排查与兜底方案数据聚合做完渲染结果里总有几个街道不上色这是做地图可视化最常见的故障。快速定位的脚本是直接做差集const geoJson require(./geojson/浙江省杭州市拱墅区.json); const geoNames new Set(geoJson.features.map(f f.properties.name)); const chartData [ { name: 米市巷街道, value: 233 }, { name: 小河街道, value: 189 }, { name: 半山街道, value: 98 } ]; const unmatched chartData.filter(d !geoNames.has(d.name)); console.log(匹配不到的街道, unmatched);把GeoJSON里所有街道名称塞进一个Set然后过滤chartData中不在Set里的条目。打印结果后通常能看到两类问题一类是名称不统一比如台账写“半山镇”GeoJSON里是“半山街道”另一类是区划调整导致的名称失效历史数据里的老街道名称在新版GeoJSON里已经不存在或者合并进了其他街道。第二类问题没有通用解法需要业务侧确认“老名称归属到哪个新街道”然后做一份映射表补上const legacyMap { 老街道A: 新街道X, 老街道B: 新街道Y }; chartData.forEach(d { if (legacyMap[d.name]) { d.name legacyMap[d.name]; } });映射表放在代码里维护还是放数据库由业务量决定但要注意清理避免历史映射规则越积越多影响后续数据准确性。整体链路完成后图表数据源、渲染、交互已经可达可用。5. 街道级GeoJSON的进阶加工字段扩展、坐标校验与性能收口5.1 把业务字段直接写进properties很多项目需要在地图上展示的不只是街道名称和数值可能还有区域编码、负责人、网格编号等附属信息。与其在渲染时维护一套外部的name - info映射不如直接把这些字段写进GeoJSON的properties里让一份数据文件同时承载几何信息和业务元数据const geoJson require(./geojson/浙江省杭州市拱墅区.json); const extraInfo { 米市巷街道: { gridCode: GS-001, manager: 张工 }, 小河街道: { gridCode: GS-002, manager: 李工 } }; geoJson.features.forEach(f { const name f.properties.name; if (extraInfo[name]) { Object.assign(f.properties, extraInfo[name]); } }); fs.writeFileSync(./geojson/拱墅区_扩展.json, JSON.stringify(geoJson));这样改之后tooltip的formatter里就能直接读取params.data上的扩展字段。需要注意Object.assign是浅拷贝嵌套对象会被多个feature共享引用如果后续要修改某个街道的info务必用深拷贝或者逐字段赋值避免修改一个街道影响了其他街道。5.2 叠加高德底图时的坐标系一致性验证ECharts地图经常叠加在高德或百度底图上使用。如果底图切片用的是GCJ-02坐标系而GeoJSON是WGS-84经纬度两个图层会出现约几十到几百米的偏移在街道级边界上视觉错位非常明显。验证方法很简单取GeoJSON里某个街道的center坐标和高德地图上同一地点的标注坐标对比差值如果大于0.005度基本可以确定坐标系不一致。处理方式有两种。一种是在服务端做坐标偏移转换把GeoJSON的每个顶点从WGS-84转换成GCJ-02另一种是用ECharts的geo组件配合高德原生地图。前者会永久修改文件建议保留一份原始WGS-84版本备份。转换工具优先选择已经验证过的现成库不要自己写偏移算法火星坐标转换的坑很多自己实现的精度很难保证。如果底图使用高德官方JavaScript API加载也可以在拿到GeoJSON后临时转一次坐标再注册不落盘但要注意注册时机转完再registerMap顺序反了会渲染错乱。5.3 大文件渲染性能收口街道级GeoJSON的坐标顶点数量比区县级多一个数量级13个区县文件全部注册后ECharts在重绘和缩放时会有可感知的卡顿。最有效的优化是降精度GeoJSON里的坐标往往保留了6到8位小数但街道边界在可视化场景下保留4位小数就够了1e-4度约等于11米精度完全满足大屏展示需要。用工具把坐标四舍五入到4位小数文件体积和解析时间都能下降30%以上function simplifyPrecision(coords, digits 4) { const factor Math.pow(10, digits); if (typeof coords[0] number) { return [Math.round(coords[0] * factor) / factor, Math.round(coords[1] * factor) / factor]; } return coords.map(c simplifyPrecision(c, digits)); } geoJson.features.forEach(f { f.geometry.coordinates simplifyPrecision(f.geometry.coordinates); });这个递归函数处理的是GeoJSON嵌套数组结构最内层是[lng, lat]的number数组中间是坐标串和环外层是Polygon或MultiPolygon的边界列表。判断typeof coords[0] number就能确定是否到达最内层然后对lng和lat分别做四舍五入。跑完这段脚本后建议用JSON.stringify(geoJson).length对比前后的体积差异如果降幅不明显说明原始文件的精度本来就不高可以适当把digits调到3再试一次。渲染层使用animation: false也能显著减少街道数量多时的动画性能消耗。这套加工流程做完这份杭州街道级GeoJSON才算真正达到了可上线交付的状态。本文还有配套的精品资源点击获取
返回列表