湖南空气质量WebGIS可视化:Leaflet地图渲染与数据链路实践 简介这是一份基于百度天气接口与Leaflet技术的湖南空气质量WebGIS可视化可运行源码适合WebGIS初学者、前端开发者及环境数据展示爱好者参考。资源完整实现了从天气数据获取到地图图层渲染的轻量级流程包含湖南省内中、重污染地区与优质地区的空间分布展示以及污染严重和空气质量优的前10名区县排名可直观用于区域空气质量对比与可视化方案验证。压缩包共3个文件以index.html为主页面搭配.inscode运行配置与.gitignore版本管理说明整体仅7KB结构精简便于快速部署和二次修改。目前已46人学习对于想了解百度天气接口调用、Leaflet地图集成及省级空气质量可视化实现思路的开发者是一份值得直接运行的示例代码。1. 湖南空气质量WebGIS可视化把监测数据落到网页地图上的完整方案当同事把“这周湖南空气质量明显变差的区域是哪里”这个问题扔过来时Excel 做不出空间化的答案。湖南空气质量WebGIS可视化这套可运行源码解决的就是这类问题把散落在国控站点、地市站点的 AQI 与六项污染物数据按经纬度投影到一张网页地图上用气泡颜色和大小直观反映空间分布再靠点击弹窗展示单点浓度。整套方案以轻量地图引擎加一个本地服务为主浏览器打开就能用放到服务器上就能给内网同事访问。适合要把监测数据变成可视化页面的开发者、环保业务人员以及做数据展示场景的在校学生。2. 湖南空气质量WebGIS的架构拆分从数据文件到网页地图的完整链路一份可运行的空气质量 WebGIS 源码本质上是四件事监测数据怎么存、地图边界从哪来、数据如何通过接口出去、页面怎么渲染。浏览器里的 index.html 只负责最后一环中间任何一环断掉页面上都看不出问题出在哪。所以拿到源码后先别急着跑把这条链路捋清楚后面排错会快很多。2.1 监测数据怎么组织站点、坐标与AQI字段的最小约定数据文件的组织方式决定了后面所有代码的写法。常见的做法是把监测站信息存成 JSON 数组每条记录代表某个站点在某个时次的监测结果。我见过不同版本的源码字段名略有出入但以下这些字段是绕不开的字段类型示例值说明station_idstringA101站点唯一编号前端弹窗和图表联动都靠它namestring湘北-A站展示名称建议带方位或行政区前缀lonnumber112.13经度十进制度数WGS84 坐标latnumber28.22纬度同上aqinumber85综合空气质量指数pm25number62PM2.5 浓度按项目需要可扩展其他污染物timestring2026-01-15 10:00监测时次推荐本地时间字符串精确到分钟经纬度要存十进制度数不要存度分秒格式地图库默认都认小数。精度上保留小数点后 5 位大约对应 1 米的分辨率对国控站点展示完全够用如果数据里坐标只有 3 位小数站点位置会明显毛糙甚至跨街区偏移拿到数据先做一次抽样核对。时间字段建议直接存“本地时间字符串”格式要严格到空格都对齐因为后面做时间维度切换时用到的往往是字符串匹配而不是日期解析格式不一致会查不出数据。坐标系是这个环节最大的暗坑。站点数据可能来自不同数据平台有的给 GPS 坐标也就是 WGS84有的给国内地图坐标也就是 GCJ-02。两种坐标系在湖南区域大概有几百米的差异混装后会呈现“一部分点正常、一部分点偏移”的诡异现象。组织数据阶段就要确认每个字段的坐标系来源并在文件头部注释写明后续统一换算。2.2 地图选型为什么可运行源码里通常选Leaflet而不是3D引擎地图渲染层有多个选择但这类站点级可视化最常见的是 Leaflet其次是 OpenLayers再往上才是 Cesium 这类三维引擎。引擎适合场景上手成本本项目取舍Leaflet点线面展示、专题图、几十个图层以内低熟悉 API 后半小时能出图首选体积小 API 直接OpenLayers多投影切换、复杂几何运算、大量图层管理中高API 偏底层功能过剩Cesium三维地形、大场景时空连续高需要补 WebGL 概念杀鸡不用牛刀站点的空间分布用圆形标记就够表达不涉及三维视角、地形加载和连续轨迹动画Cesium 的立体效果在这里没有用武之地。OpenLayers 的强项在于投影切换和复杂空间运算而本项目只有一种投影、一类点数据用 Leaflet 能让代码量少一大截。图表部分用 ECharts 承担排名表和单站污染走势图它和地图之间通过点击事件传递站点编号联动两个库各管各的不混写。底图部分常见做法是直接用开源瓦片服务的 URL 模板代码量最小。国内访问这类瓦片服务时快时慢这属于网络环境问题不是代码缺陷要彻底解决可以改用离线瓦片包。如果你换了国内地图厂商的瓦片就必须注意 GCJ-02 与 WGS84 的偏移问题——底图是加密坐标站点数据是原始坐标两者叠加位置对不上这就是后面避坑章节要展开的经典现象。还有一个常见疑问是为什么不用 WebSocket 做实时推送。这类空气质量可视化场景的数据更新粒度通常是小时级甚至天级用户重新打开页面拉一次最新数据就够了实时推送把架构复杂度抬高却没有带来体验收益。可运行源码里维持 HTTP 轮询是足够且合理的。2.3 一套常见目录结构拿到源码后先看哪几个关键文件下面是一套常见的源码目录约定不一定每个包都长这样但大概率是这个骨架project/ ├─ public/ # 前端页面文件 │ └─ index.html # 页面入口含地图初始化、图层渲染代码 ├─ data/ # 本地数据文件 │ ├─ hunan_city.geo.json # 湖南省地市级边界GeoJSON 格式 │ └─ latest.json # 最新一期监测站点数据 ├─ server.js # 本地 HTTP 服务入口 └─ package.json # 依赖声明拿到源码先做三件事第一打开 package.json 看 dependencies确认是不是只有 express 这类轻量依赖有没有明显不相关的重组件第二看 data 目录边界文件和站点数据文件是否真实存在各有多大第三看入口文件注释或配套说明里写的启动命令。如果 data 目录是空的通常说明数据需要单独下载或由接口动态写入这时候服务能起来但页面空转不代表程序坏了。另外要确认边界文件的粒度。如果里面是街道甚至村级边界页面也许还能跑但拖动缩放会明显卡顿这个坑在第 4 章详细展开。省级空气质量可视化用市级边界足够既保留空间辨识度又不拖累交互性能。边界文件和站点数据文件统一放在 data 目录便于后续替换与增量更新。3. 把数据画上湖南地图核心代码与必调参数这一章把前端渲染拆成四步初始化地图、加载湖南边界、绘制站点、绑定弹窗最后用本地服务把整套串起来。代码是可直接运行的示例参数按湖南区域的常规范围预设你可以直接复制到自己的页面里改。3.1 初始化地图与加载湖南边界两个JSON缺一不可先初始化地图画布并加载湖南地市级边界。这一步的前提是 data 目录里存在边界 GeoJSON 文件没有它地图上就只有底图没有省界参考。// 初始化地图中心点取湖南地理中心附近缩放级别选 7 能看全全省 const map L.map(map).setView([28.2, 111.7], 7); // 底图使用开源瓦片服务其投影坐标系为 WGS84 L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { maxZoom: 18 }).addTo(map); // 加载湖南地市级边界文件放在 data/hunan_city.geo.json fetch(data/hunan_city.geo.json) .then(res res.json()) .then(geo { L.geoJSON(geo, { style: { color: #555, weight: 1.2, fill: false } }).addTo(map); });setView 的第一个参数是 [纬度, 经度] 数组不要写反这是新手最常见的低级错误。湖南的经纬度范围大致是东经 108 到 114 度、北纬 24 到 30 度中心点取 28.2, 111.7 再配缩放级别 7正好铺满整个视口。底图 URL 模板里的{s}、{z}、{x}、{y}占位符由 Leaflet 自动替换不需要你处理。style 对象里把 fill 设为 false只保留描边能省去边界内部的填充渲染让站点气泡更突出。3.2 AQI分级着色颜色映射与气泡半径的代码写法AQI 按国标数值分成六级每一级有约定俗成的标识色。可视化项目里最常见的翻车方式是把颜色写成线性渐变导致轻微污染和中度污染在图上色阶接近阈值处体现不出等级跳跃。正确做法是分段函数。AQI 范围等级颜色0 - 50优#00e40051 - 100良#ffff00101 - 150轻度污染#ff7e00151 - 200中度污染#ff0000201 - 300重度污染#99004c300 以上严重污染#7e0023对应代码// 按国标分级映射颜色逐级拦截边界条件用 function aqiColor(aqi) { if (aqi 50) return #00e400; // 优 if (aqi 100) return #ffff00; // 良 if (aqi 150) return #ff7e00; // 轻度污染 if (aqi 200) return #ff0000; // 中度污染 if (aqi 300) return #99004c; // 重度污染 return #7e0023; // 严重污染 } // 气泡半径线性映射后封顶防止数值悬殊导致图面失衡 function aqiRadius(aqi) { return Math.min(6 aqi / 60, 20); }颜色函数的判断顺序是从小到大拦截aqi50 会命中第一个分支返回绿色aqi51 落入第二个分支判断链不会出现重叠区间。半径函数里 6 是保底像素20 是上限这样 AQI 为 30 和 AQI 为 300 的站点在图上大小差异明显但不至于一眼看成两类数据。若站点密度高可把系数 aqi / 60 调低到 aqi / 80并同步降低封顶值避免大面积气泡互相遮盖。3.3 站点弹窗与时间切换让地图能“讲”出数据变化站点绘制用圆形标记而不是图标标记原因是圆形标记的颜色和半径可以动态绑定数值而图标标记换色要维护一堆图片。用图层组统一管理站点切换时间时批量清理重建。// 统一管理站点图层的图层组便于整体清空与重建 const stationLayer L.layerGroup().addTo(map); function addStationMarker(s) { L.circleMarker([s.lat, s.lon], { radius: aqiRadius(s.aqi), color: #ffffff, weight: 1, fillColor: aqiColor(s.aqi), fillOpacity: 0.8 }) .bindPopup( b${s.name}/bbrAQI: ${s.aqi}brPM2.5: ${s.pm25}br时间: ${s.time} ) .addTo(stationLayer); } // 时间维度切换重建站点图层几十个站点时性能足够 function renderTime(datetime) { stationLayer.clearLayers(); const records allRecords.filter(r r.time datetime); records.forEach(addStationMarker); } timeSelect.addEventListener(change, e renderTime(e.target.value));使用 L.layerGroup 管理站点图层切换时先 clearLayers 再逐个重建能避免旧时次的点残留在地图上。fillOpacity 取 0.8 在站点密集时既看清重度污染点的实色又不至于完全盖住边界线。时间字段的匹配是全等匹配2026-01-15 10:00和2026-01-15 10:00带个空格就是两条数据一旦切换后站点消失先检查时间字符串格式是否完全一致。站点数量超过几百个时重建图层的做法会有一点性能压力但大多数空气质量可视化场景远到不了这个量级。3.4 起一个本地服务跑通整套源码避免双击HTML的跨域坑很多同学拿到源码第一反应是双击 index.html结果页面能显示底图却没有站点数据。原因是浏览器在 file:// 协议下禁止 fetch 本地文件。底图能显示是因为它走的是远程瓦片 URL而本地 JSON 文件属于跨域读取。解决方式就是起一个本地 HTTP 服务这也是可运行源码里通常会带 server.js 的原因。const express require(express); const path require(path); const fs require(fs); const app express(); const PORT 8080; // 前端静态文件目录 app.use(express.static(path.join(__dirname, public))); // 数据接口读取 data/latest.json 返回给前端 app.get(/api/aqi, (req, res) { const json JSON.parse(fs.readFileSync(./data/latest.json, utf-8)); res.json(json); }); // 监听 0.0.0.0 使局域网内其他机器也能访问 app.listen(PORT, 0.0.0.0, () { console.log(服务已启动: http://localhost:${PORT}); });这个服务只做两件事托管 public 目录下的静态文件以及提供 /api/aqi 数据接口。启动流程是先npm install express再node server.js浏览器访问 http://localhost:8080。如果你把端口改成其他值注意前端代码里请求地址也要跟着改否则接口 404。需要给局域网同事演示时监听地址必须写成 0.0.0.0这样本机 IP 加端口才能被外部访问默认监听 localhost 外面是进不来的。4. 湖南空气质量可视化源码的避坑清单坐标、边界、跨域与性能源码跑通只是第一步显示对才是硬道理。地图这类应用出错时往往不报错只是位置不对、颜色不对、加载慢排查起来全靠经验。下面五条是我在实际运行这类源码过程中遇到的典型问题每一条都按现象、原因、解决的顺序拆开讲。4.1 站点位置整体偏移几十到几百米现象所有站点都落在底图上的错误位置偏移方向一致单个点看不出问题整体看就像地图平移了一段距离。我之前遇到的一个典型场景是某站点明明在路东侧气泡却跑到路西侧的街对面。原因站点数据来自国内地图生态坐标是 GCJ-02 加密坐标而 OSM 底图是 WGS84 坐标。两者在湖南区域存在几百米的系统性偏移。数据源没声明坐标系或声明了但没做转换就直接画点。解决先确认数据坐标来源再决定是否转换。如果确认是 GCJ-02就需要在绘制前把经纬度换算成 WGS84常见做法是引入一个小型坐标转换函数库调用转换方法批量处理。不要自己套用网上流传的固定偏移公式那些近似参数在不同纬度误差不一致。转换完成后抽样两三个站点与卫星底图或路网对照确认没有转反GCJ-02 转 WGS84 和反向操作容易搞混。4.2 边界跑到图外甚至变形扭曲现象底图显示正常但湖南边界轮廓整体偏移或旋转严重时跑到地图边缘外有的边界还和底图上的省界线明显不重合像是叠了一层错位的蒙版。原因边界 GeoJSON 本身不是标准 WGS84 坐标。有些数据源导出时保留的是地方坐标系或加密坐标系叠加到 WGS84 底图上自然错位。另一种情况是 GeoJSON 里混入了投影参数而前端没有做任何投影转换。解决优先更换数据源用 WGS84 的省市级 GeoJSON。换完数据后可以用 mapshaper 打开文件检查坐标范围正常应该在东经 108 到 114、北纬 24 到 30 之间如果出现几百上千的数值大概率不是经纬度坐标。不要尝试在前端做投影转换那会让问题复杂化直接换数据文件是最快的出路。4.3 双击HTML打开页面有地图却没有站点数据现象用浏览器直接打开 index.html底图和边界正常显示但站点气泡缺失。打开开发者工具的控制台能看到红色的 Failed to fetch 报错。从同事那里拷来的源码自己跑就是少数据很让人怀疑是代码被改坏了。原因浏览器在 file:// 协议下对本地文件读取有严格限制fetch 请求指向本地 JSON 会被浏览器安全策略拒绝。远程底图不受影响因为它本身是一次正常的 HTTP 请求所以页面看起来“部分正常”。解决按 3.4 的方式起本地 HTTP 服务通过 http://localhost 访问页面这是推荐做法。如果只想临时看效果也可以把数据直接内联成 JavaScript 变量写进页面双击就能打开缺点是一次性数据更新要重新生成 HTML不适合做后续的每日更新版本。4.4 地图缩放卡顿拖动像掉帧现象页面加载完成后地图能显示但鼠标拖动时边界和站点明显迟滞缩放手感很差CPU 占用率飙升。边界线条复杂的地方卡顿尤其明显。原因边界 GeoJSON 包含的顶点数过多。省级可视化如果用了乡镇级甚至村级边界顶点数轻松上万每次缩放重绘都要重新计算和绘制这么多点浏览器扛不住。很多下载到的公开边界数据原始精度极高直接使用就会踩这个坑。解决在数据源头做简化。mapshaper 是处理 GeoJSON 简化最顺手的工具命令行一条就能出结果# 简化边界保留约15%的顶点肉眼几乎看不出变形 npx mapshaper hunan_raw.geo.json -simplify 15% -o dist/hunan_city.geo.jsonsimplify 后面的 15% 表示保留约 15% 的顶点适合省级轮廓展示如果只关注地市局部可以进一步降到 10%文件更小加载更快。简化后再对比一下和原始边界在重点地市的形状差异只要市级轮廓没丢角就没问题。此外前端绘制时把 fill 设为 false只画描边也能减轻渲染负担。4.5 AQI颜色和官方发布不一致现象某天某个站点 AQI 为 120属于轻度污染地图上却显示深红色看起来像重度污染区域。当天官方 App 里显示的是橙色两边颜色对不上业务人员质疑数据出了错。原因颜色映射写成了线性渐变比如把 AQI 从 0 到 300 均匀映射到红黄绿导致阈值附近没有等级跳变轻度污染和中度污染在视觉上区分不开。也有些代码把国标色值记混轻度污染用了黄色中度用了橙色整个色阶后移了一档。解决用 3.2 的分段函数且色值严格按国标约定。轻度污染就是 #ff7e00 橙色中度才是 #ff0000 红色。判断边界时注意整数比较AQI100 属于良AQI101 属于轻度污染代码里 100和 150的连续判断天然满足这个边界不要改成否则整条色阶在临界点都会错位。5. 从静态Demo到每日更新给这套源码接上真实数据源源码跑通只是开始真正让它有价值的是数据自动更新。手工下载数据再替换 JSON 的方式坚持不了几天可运行源码要落地成日常可用的小系统至少得把定时拉取、归档、部署这三件事做到位。5.1 定时拉取接口数据并归档常见做法是在服务端加一个定时任务每天拉取一次最新监测数据写入两个文件一个是供前端读取的 latest.json一个是按日期命名的归档文件。const cron require(node-cron); const endpoint https://你的数据服务地址/api/air/hourly; // 每天0点执行拉取最新数据写入最新文件并归档 cron.schedule(0 0 * * *, async () { const res await fetch(endpoint); const json await res.json(); const day new Date().toISOString().slice(0, 10); fs.writeFileSync(./data/${day}.json, JSON.stringify(json)); fs.writeFileSync(./data/latest.json, JSON.stringify(json)); });cron 表达式0 0 * * *表示每天零点执行前两位分别是分和时后面分别是日、月、周。Node.js 18 以上可以直接用全局 fetch旧版本需要引入 node-fetch。归档文件按日期命名后前端时间轴可以直接把文件目录列表映射成可选择的日期形成历史回看。注意服务器时区要调整到东八区否则归档文件名和本地时间对不上。5.2 前端加一个时间轴回看归档文件有了前端要做的事情很简单把日期列表渲染成下拉框或滑块切换时把 5.1 生成的归档文件作为数据源传入 3.3 里的 renderTime 函数。这里的核心约束是归档数据和最新数据的字段结构必须完全一致时间字段格式也要统一。如果某一天拉取失败归档目录里会缺一个文件前端要有兜底提示比如“该日数据缺失”而不是静默空白。5.3 部署成常驻服务本地跑通后要部署到服务器用 PM2 守护 Node 进程是最省事的方案。启动命令是pm2 start server.js --name air-map然后执行pm2 save和pm2 startup让服务开机自启这样即使服务器重启地图服务也会自动回来。内网用户通过本机 IP 加端口访问3.4 里的监听地址 0.0.0.0 在这时就起作用了。我第一次部署这类地图服务时忘记给数据接口设置缓存控制头第二天打开页面看到的还是浏览器缓存的旧数据排查了一上午才意识到是缓存问题。后来养成了习惯凡是每天更新的数据接口要么在响应头显式设置 Cache-Control: no-cache要么在请求 URL 后加时间戳参数。这套源码本身不复杂但把更新链路跑顺、把缓存和时区这些细节管好才算真正能用起来。希望帮到你。本文还有配套的精品资源点击获取