2026/10/21 5:26:51

成都GeoJSON数据可视化实战:从解析到高德地图加载

成都GeoJSON数据可视化实战:从解析到高德地图加载 简介四川省成都市最新GeoJSON数据文件涵盖高新区南区、西区、东区及天府新区主要区域边界面向GIS开发者、前端地图工程师及数据分析人员可用于高德地图等平台的行政区划图层加载、区域可视化与空间查询免去自行爬取和人工维护边界数据的繁琐流程。数据包为rar压缩格式共2个文件包含一个区域zip压缩包和一个核心GeoJSON数据文件整体约1.6MB结构简洁GeoJSON以标准经纬度坐标存储各区域多边形边界兼容主流地图API。已有943人学习下载。解压后可直接读取GeoJSON数据或导入高德地图自定义图层实现区域高亮、边界描边、点击弹出区域名称等交互效果也可结合Python、JavaScript等地理空间库做面积统计、点位归属分析适用于城市规划展示、商业选址分析、交通研究等应用场景。1. 拿到成都 GeoJSON 数据后先认识这份能直接出图的区划边界做城市级数据可视化时最缺的不是图表库而是一份能用、敢用的行政区划边界数据。这份成都 GeoJSON 数据把高新区南区、高新西区、高新东区以及天府新区的边界单独拆了出来配合高德地图的地图加载能力可以直接做区域高亮、地理围栏、统计聚合和自定义样式的可视化底图。我拿到手先做的事不是急着渲染而是把文件结构、坐标系、字段属性一次性核对清楚——后端做接口输出的数据前端不一定能直接用这份数据看着干净但里面藏的坑比想象中多。适合前端工程师做可视化看板、数据开发做区域聚合分析、搞 GIS 的做底图叠加也适合刚接触 GeoJSON 的初学者拿它当练手素材。下面按我自己的落地顺序把从拆文件到出图、再到排错的完整过程捋一遍。2. 拆开数据文件结构、字段与坐标系的三重核对2.1 FeatureCollection 结构与属性字段先看清楚里面装了什么GeoJSON 的标准结构是一个 FeatureCollection里面包含若干 Feature每个 Feature 有 geometry几何形状和 properties属性两个核心部分。这份成都数据里geometry 的 type 主要是 MultiPolygon 或 Polygonproperties 里通常包含 name区县名称、adcode行政区划代码、center中心点经纬度这类字段。先确认属性字段后续按区拆分、按名称检索、按编码关联业务数据都靠它们。字段名类型典型值示例用途nameString高新区南区图例、Hover 提示、筛选adcodeString/Number6 位区划代码关联业务表、按区加载centerArray[104.xx, 30.xx]标注中心点、飞线连线geometry.typeStringMultiPolygon决定渲染方式很多人在拿到数据后直接丢给地图组件properties 字段名不核对等渲染出来发现 name 取不到、adcode 对不上才回去翻文件——这一步省不掉。2.2 用 Python 做一次合法性检查JSON 能解析只是第一步我的习惯是先用一段 Python 脚本把文件过一遍。json.load 只是最基础的检查真正要确认的是 geometry 类型分布、Feature 数量、每个要素的坐标层级是否正常。MultiPolygon 和 Polygon 的处理逻辑完全不同写渲染代码前必须搞清楚数据里到底有哪些类型否则后面写 filter 逻辑时会不知不觉漏掉一半区域。import json with open(chengdu.json, r, encodingutf-8) as f: data json.load(f) # 确认顶层结构 print(type:, data[type]) # 期望 FeatureCollection print(feature 数量:, len(data[features])) # 统计 geometry 类型分布 from collections import Counter geo_types Counter(f[geometry][type] for f in data[features]) print(geometry 类型:, geo_types) # 抽查第一个要素的坐标层级 sample_geom data[features][0][geometry] if sample_geom[type] MultiPolygon: print(坐标层级: MultiPolygon Polygon LineString Position) print(第一块 Polygon 的坐标点数:, len(sample_geom[coordinates][0][0]))这段检查脚本有四个关键输出顶层 type 决定了解析器是否按 FeatureCollection 走feature 数量帮你判断数据是整市合并版还是按区分拆版geometry 类型分布直接决定渲染代码要不要同时兼容 Polygon 和 MultiPolygon坐标层级确认有没有数据被错误简化成 LineString。参数上没太多可调的核心就一个——所有检查都是为了让你在用地图组件时不需要反复打开 JSON 文件去猜结构。2.3 坐标系判定先分清 GCJ-02 与 WGS-84 再谈加载GeoJSON 本身不带坐标系声明你必须靠坐标值自己判断。成都市中心大约在经度 104.0、纬度 30.6 附近但这不是判断依据因为两种坐标系在这个位置只差几百米。真正的判定方法是看坐标精度和偏移特征如果坐标是 104.062345, 30.670345 这种带 6 位小数、且你拿高德坐标拾取器对同一位置发现几乎重合——这是 GCJ-02高德、腾讯地图的默认坐标系。如果坐标跟手机 GPS 记录的轨迹点能对上拿 WGS-84 的地图比如部分海外底图看也基本重合——这是 WGS-84。最粗暴的判定打开高德地图的坐标拾取器选成都任意一个点位读坐标再对比 JSON 里对应位置的坐标差值在 300500 米量级基本可判定为 WGS-84。这份成都 GeoJSON 数据在高德地图场景下使用加载前需要确认坐标系。原理层面的账先算清楚高德底图是 GCJ-02如果你的数据是 WGS-84直接叠加就会出现边界和底图错位一整条街的现象。后面第 4 章会专门给转换脚本这里先记结论——加载前不判定坐标系等于埋雷。3. 接到高德地图上从 chengdu.json 到区域高亮的完整链路3.1 高德地图 JS API 加载GeoJSON 直接出新图高德 JS API 提供了直接解析 GeoJSON 的能力不用你手动把每条 Polygon 转成 AMap.Polygon。常见做法是在初始化 Map 后通过 GeoJSON 类传入数据并设置样式。以下几点必须注意GeoJSON 里的 coordinates 是数组嵌套结构API 内部会做解析但你要确保每个 Feature 的 geometry 类型是地图库认识的 Polygon 或 MultiPolygon样式回调按属性字段动态设置比如按 adcode 前缀匹配不同颜色数据量大时稳妥做法是先做数据瘦身把坐标精度从 6 位小数压到 5 位肉眼无差别渲染性能差很多。// 假设已通过 AMap.plugin(AMap.GeoJSON) 加载插件 const map new AMap.Map(container, { zoom: 10, center: [104.06, 30.67], // 成都中心点 }); const geojson new AMap.GeoJSON({ geoJSON: chengduData, // 从 JSON 文件解析后的对象 getPolygon: function (obj) { // obj 是高德生成的 Polygon 覆盖物这里统一设置样式 const name obj._parent.properties.name; return new AMap.Polygon({ fillColor: name.includes(高新) ? #3388ff : #79b6f2, strokeColor: #ffffff, strokeWeight: 1, fillOpacity: 0.4, }); } }); geojson.setMap(map); map.setFitView(geojson.getOverlays());getPolygon 回调里obj._parent.properties 可以拿到原始 GeoJSON Feature 的属性——不同版本的高德 JS API 这个访问路径略有差异稳妥做法是在加载前把属性挂到上一层避免运行时找不到字段。fillColor 按名称做条件判断高新区的三个片区用同一色系天府新区用另一色系视觉上先把主体分区区分开。setFitView 自动调整视野到所有边界能完整显示的范围省去手动调 zoom 和 center。3.2 Leaflet 备选方案不依赖高德时的加载写法不是所有项目都允许接高德 SDK比如纯离线内网环境。这时候 Leaflet 加高德瓦片是常见替代方案。Leaflet 本身是 WGS-84 友好的但高德瓦片是 GCJ-02 加密坐标直接叠加 WGS-84 数据会出现几百米偏移。这个问题有两个解法一是把 GeoJSON 坐标转成 GCJ-02 再渲染二是用专门适配高德瓦片的 Leaflet 插件让瓦片图层和 GeoJSON 数据统一。我一般用第一种因为数据转换是一次性的而插件维护成本是持续的。const map L.map(map).setView([30.67, 104.06], 10); // 高德地图瓦片GCJ-02 L.tileLayer(https://webrd0{1-4}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}, { subdomains: [1, 2, 3, 4], maxZoom: 18, }).addTo(map); // 已转为 GCJ-02 的 GeoJSON L.geoJSON(gcj02Data, { style: { color: #3388ff, weight: 1, fillOpacity: 0.3, }, onEachFeature: (feature, layer) { layer.bindTooltip(feature.properties.name); } }).addTo(map);瓦片 URL 里的 style8 是路网图style6 是卫星图生产环境按需切换。核心问题是坐标系对齐——数据必须和瓦片处于同一坐标系这条规则在 Leaflet 里比在高德 SDK 里更严格因为高德 SDK 内部可能做了兼容而 Leaflet 完全依赖你自己的数据判断。3.3 交互联动Hover 高亮与按 adcode 区划回传地图数据只有渲染价值是不够的。实际业务里最常见的是鼠标滑过时高亮当前区域、点击时把区域信息回传给业务系统用来做联动筛选或下钻。无论是高德还是 Leaflet核心都是 onEachFeature 这个钩子。这里有一个必须注意的细节你绑定的点击事件拿到的 feature 就是 GeoJSON 里的原始 Feature 对象properties 字段直接从 JSON 里来但如果你在加载前对 properties 做过二次赋值务必在事件回调里重新取一次避免闭包引用到旧对象。onEachFeature: (feature, layer) { const props feature.properties; layer.on(mouseover, () { layer.setStyle({ fillOpacity: 0.7, strokeWeight: 2, }); }); layer.on(mouseout, () { layer.setStyle({ fillOpacity: 0.3, strokeWeight: 1, }); }); layer.on(click, () { // adcode 是区划唯一编码回传给业务系统做筛选 console.log(当前区域:, props.name, adcode:, props.adcode); // 这里可以触发 Vue/React 的全局状态更新 }); }参数上要注意mouseover 里设置的样式会在 mouseout 时被覆盖如果你在点击后也改了样式要单独维护一个“选中态”变量不然移出鼠标后选中高亮会被清掉。adcode 的格式在不同数据源里可能是数字也可能是字符串建议统一转成 Number 或 String 再做比较减少类型引发的隐式 bug。4. 坐标系转换GCJ-02 与 WGS-84 之间差了一个街区4.1 三个坐标系的关系为什么高德要用 GCJ-02WGS-84 是 GPS 设备直接输出的坐标全球统一GCJ-02 是国内地图厂商普遍使用的加密坐标由 WGS-84 经过非线性偏移得到另外还有 BD-09只用于百度系产品。三者关系用一句话概括WGS-84 是地球真身GCJ-02 是加密哈哈镜。高德地图所有产品线统一采用 GCJ-02包括它的 Web 端、移动端和开放平台接口。如果你手里的成都 GeoJSON 是 WGS-84不转换直接撒到高德底图上边界线会在真实位置上平移几百米在特写级别看行政边界压在了隔壁街区上。这种问题在代码层面不报错渲染时也看不出明显异常直到你把数据叠加到卫星图上做比对才暴露——属于典型的数据污染型 bug排查成本比逻辑 bug 高得多。4.2 标准转换脚本WGS-84 转 GCJ-02 的公式与工程取舍网上流传的 WGS-84 转 GCJ-02 算法是逆向工程产物精度在 13 米级别对行政边界可视化来说完全够用。核心公式是固定的区别只在工程封装方式。我自己的习惯是不改公式本体只封装成纯函数输入输出都用数组形式方便直接用在地图组件的数据预处理里。import math def wgs84_to_gcj02(lng, lat): 单个点坐标转换WGS-84 转 GCJ-02高德/腾讯坐标系 返回 (gcj_lng, gcj_lat) a 6378245.0 # 地球长半轴 ee 0.00669342162296594323 # 偏心率平方 def _transform_lat(x, y): ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(y * math.pi) 40.0 * math.sin(y / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(y / 12.0 * math.pi) 320 * math.sin(y * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(x, y): ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * math.sqrt(abs(x)) ret (20.0 * math.sin(6.0 * x * math.pi) 20.0 * math.sin(2.0 * x * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(x * math.pi) 40.0 * math.sin(x / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(x / 12.0 * math.pi) 300.0 * math.sin(x / 30.0 * math.pi)) * 2.0 / 3.0 return ret dlat _transform_lat(lng - 105.0, lat - 35.0) dlng _transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) gcj_lng lng dlng gcj_lat lat dlat return round(gcj_lng, 6), round(gcj_lat, 6) # 批量转换 GeoJSON 中的坐标 def convert_geojson_coords(data): geom data[features][0][geometry] if geom[type] MultiPolygon: for polygon in geom[coordinates]: for line in polygon: for idx, (lng, lat) in enumerate(line): line[idx] wgs84_to_gcj02(lng, lat) return data这段代码里最关键的是两个正弦函数组合它们模拟的是 GCJ-02 的非线性偏移不是简单加减固定值。工程上要注意三点round 到 6 位小数就够高德底图的显示精度到 6 位小数已经是亚米级保留更多位数只会让文件变大批量转换建议把数据先深拷贝一份避免原地修改后不可回退转换函数是逐点转换成都整个数据文件几十万坐标点跑完也就几秒钟不必做并行优化。4.3 转换后验证用坐标拾取器做偏移校验转换有没有生效肉眼是看不出来的。有效做法是拿转换后的数据里某个区域的中心点坐标和高德地图的坐标拾取器在同一位置读取的坐标做比对误差应该在 3 米以内。再激进一点同时加载转换前和转换后的两份数据到高德地图上转换后的边界应该和底图道路网吻合转换前的边界会整体偏移几百米。我一般在验证时还会跑一个自动化检查对所有多边形取第一个点坐标转换前后距离做统计把平均偏移量和最大偏移量打印出来保存到日志里备查。这一步的产出不是一个结论而是一个可复现的验证脚本下次换新数据时直接跑。5. 常见问题避坑加载空白、边界漂移与拉不出区县的排查记录5.1 加载后地图空白多为文件读取和变量作用域问题现象页面打开底图显示正常但行政区域一个都没渲染出来控制台也不报错。原因最常见的是 GeoJSON 数据没正确从 JSON 文件解析成 JavaScript 对象直接传了字符串给地图组件其次是数据在异步回调里还没返回就执行了渲染代码。还有一种隐蔽情况组件加载时数据是空的但后续异步更新时没有触发重新渲染。解决先打印typeof data object确认是对象而非字符串。再检查网络请求是否在渲染后才完成用 Promise 或回调包裹渲染逻辑。最后确认加载成功后的数据是否在 Vue/React 的响应式系统里被正确代理必要时手动拷贝成普通对象。5.2 边界漂移一大截坐标系混用是头号原因现象区域边界能显示但和高德底图的道路网错位明显看起来边界线悬在街道中间有时隔了一条街的距离。原因数据是 WGS-84 坐标系底图是 GCJ-02 坐标系两边坐标叠加时没有统一。这也是市面上 GeoJSON 数据最常见的通病——数据源来自国际开源社区坐标系几乎清一色 WGS-84而国内地图服务商全部用 GCJ-02。解决用第 4 章的转换脚本把坐标统一成 GCJ-02。转换后重新加载再拿拾取器抽样验证。如果源数据自带坐标系声明文件先看声明再决定要不要转不要盲目转。5.3 单个区县拉不出来数据结构和字段匹配的双重坑现象整市数据能加载但点击某个区域时拿不到区域名称或者按名称过滤时永远查不到“高新西区”。原因一是几何类型不统一有的区是 Polygon有的是 MultiPolygon过滤逻辑里只处理了一种二是 name 字段里有不可见字符比如全角空格、零宽空格字符串比较时永远不相等。解决过滤逻辑改为同时判断 Polygon 和 MultiPolygon不要用 geometry.type 做排除条件。字段清洗时用正则去重不可见字符。还可以用 adcode 作为主键name 只做展示不参与逻辑判断。5.4 换数据后样式失效字段名不匹配的翻车现场现象之前跑得好好的渲染代码换了一份新导出的成都 GeoJSON 后区域能出来但全部是默认样式颜色、描边全丢了。原因新旧数据的 properties 字段命名不一致。旧数据叫name新数据叫districtName旧数据 adcode 是数字新数据 adcode 变成了字符串。样式回调里直接取 field取不到就回退默认值。解决加载后第一步先做字段归一化把可能的字段名都映射到统一标准比如data.features.forEach(f f.properties.name f.properties.name || f.properties.districtName)。再把类型统一adcode 全部走 String() 强转。这个过程封装成一个预处理函数每次换数据只改映射表。6. 进阶用法按区划拆分数据与面积校验脚本6.1 按区划代码拆分为每个区单独生成 JSON整份成都 GeoJSON 在生产环境用得最多的是整体渲染但部分业务需要按区加载比如只展示高新西区或者绩效看板按区切换。这时候把整份文件提前拆成多个小文件比运行时过滤更省性能。拆分逻辑不复杂按 adcode 分组写入独立文件即可但要注意输出目录结构设计和命名规范方便后续增量更新。import json import os with open(chengdu.json, r, encodingutf-8) as f: data json.load(f) output_dir districts os.makedirs(output_dir, exist_okTrue) # 按 adcode 分组 from collections import defaultdict groups defaultdict(list) for feature in data[features]: adcode str(feature[properties][adcode]) groups[adcode].append(feature) # 每个区写出独立 JSON for adcode, features in groups.items(): district_data { type: FeatureCollection, features: features } file_name os.path.join(output_dir, f{adcode}.json) with open(file_name, w, encodingutf-8) as f: json.dump(district_data, f, ensure_asciiFalse) print(已输出:, file_name, 要素数:, len(features))拆分脚本的可调参数就一个——按什么字段分组。adcode 是稳定的区划编码推荐用它做文件名按 name 分组容易遇到重名或特殊字符问题。输出文件用 UTF-8 编码是必须的否则属性里的中文名读出来全是乱码。6.2 用边界算面积校验数据完整性的一个实用技巧数据是不是完整、有没有在加工过程中被误删多边形肉眼看不出来但面积能算出来。把拆分后的每个区面积算一遍跟官方统计做对比误差在合理范围内的说明数据完整。注意 GeoJSON 的坐标单位是度直接用面积公式算出的是平方度不是平方公里需要先投影到米制坐标。from shapely.geometry import shape from shapely.ops import transform import math def to_web_mercator(lng, lat): # 伪墨卡托投影把经纬度转成米制坐标 x lng * 20037508.34 / 180.0 y math.log(math.tan((90.0 lat) * math.pi / 360.0)) / (math.pi / 180.0) y y * 20037508.34 / 180.0 return (x, y) def calc_area_km2(feature): geom shape(feature[geometry]) # 用 transform 对每个坐标点做投影 geom_meter transform( lambda x, y: to_web_mercator(x, y), geom ) # area 单位是平方米转成平方公里 return round(geom_meter.area / 1_000_000, 2) # 示例计算整个数据的总面积 total_area sum( calc_area_km2(f) for f in data[features] ) print(总计面积平方公里:, total_area)这个面积计算用的是等距圆柱投影近似在高纬度地区误差大但成都位于北纬 30 度附近投影变形很小结果和真实面积能对上。我第一次跑这个脚本时发现总面积和官方公布的辖区总面积偏差了 2%后来定位到是几个区的边界在矢量化的过程中被简化掉了部分小凸起导致面积变少。从那以后我每次拿到新数据都会强制跑一遍面积校验流程拆分前后各跑一次两边数字对不上就说明拆分逻辑有问题对上了再上线。这套流程看着简陋但帮我挡住了很多次线上翻车希望帮到你。本文还有配套的精品资源点击获取