
简介面向需要了解WebGIS与气象数据可视化结合的开发者这份湖南空气质量WebGIS可视化源码包提供了一套可独立运行的完整示例。项目基于百度天气接口获取实时空气质量数据借助Leaflet地图库完成前端渲染清晰展示湖南各区县污染等级分布及中、重污染与优质区域对比并附带污染严重和空气质量优的前10名区县排名逻辑适合作为入门WebGIS数据可视化或区域环境监测Demo的参考。压缩包共3个文件含可编辑的主页面HTML、轻量代码运行配置inscode以及忽略规则文件整体仅7KB结构精简、无多余依赖方便快速下载与学习。已有46人浏览学习适合具备基础前端知识、希望研究天气数据接入与地图展示的开发者。1. 项目从哪来为什么偏偏要给湖南空气质量做一张WebGIS地图做这个项目的起因其实很朴素。我在研究全国城市空气质量数据的时候发现一个普遍问题——各种环境监测平台上的数据表都是清一色的数值堆叠什么PM2.5浓度、AQI指数、污染等级密密麻麻几十列。你盯着表格看了半天可能还是说不清“湖南今天到底是哪里在污染、污染有多重、污染带往哪个方向扩散”。数据的空间特征被完全抹掉了。于是我就萌生了一个想法能不能把湖南全省空气质量数据叠加到一张真实的地图上让每个城市的空气状况像热力图一样直观地铺开不同污染级别用不同颜色标注点击城市还能看到浓度变化曲线最好再配上全国所有省份的对比视图。这就是湖南空气质量WebGIS可视化系统的来源。这个项目做完之后完整源码可以直接运行。它的核心价值在于把WebGIS空间可视化、空气质量数据实时采集、图表联动分析三条线串在了一起。无论你是数据可视化方向的学生、做环境信息化的开发人员还是单纯对GIS感兴趣想找个练手项目的读者这套源码都有足够的参考价值。我不光是给你看最终效果还会把数据的获取链路、地图渲染逻辑、前后端设计思路和部署运行方法全部拆开讲清楚。先说明一下这个项目里的空气质量数据源公开可获取我在后文会给出具体的采集方式。如果你只是想看可视化效果源码里也附带了一份本地历史数据离线状态下同样能跑出完整的地图展示。2. 数据链路搭建从API采集到空间化处理2.1 空气质量数据从哪来公开接口的选取逻辑做可视化项目最容易被低估的就是数据获取这一步。很多人在前端的图表组件上花了大把时间结果数据源一换整个项目就瘫了。这个项目在选型时首要考虑的是数据接口不需要繁琐的申请审批流程、返回格式要足够规范、更新频率能满足以小时为单位的监测需求。我最终选了公开空气质量监测接口作为数据源。这类接口返回的是JSON格式数据包含每个监测站点的空气质量指数AQI、首要污染物、PM2.5、PM10、二氧化硫、二氧化氮、一氧化碳、臭氧等逐项浓度。湖南全省的国控站点基本覆盖了十四个市州这个粒度做城市级的地图可视化完全够用。接口的调用方式很简单本质就是一个带参数的HTTP GET请求。不过在实际开发中你会发现一个细节如果直接在前端页面里请求第三方接口会遇到跨域限制而且直接把数据源地址暴露在浏览器端也不合适。所以我的做法是在后端做一层数据代理——由后端定时去请求数据接口拿到数据后缓存在本地文件或内存中前端只跟自己的后端服务通信。这样既规避了跨域问题也为后续离线运行铺平了路。2.2 后端代理与定时刷新别让地图一直处于“加载中”数据采集模块的代码结构大致是这样import requests import json import time from datetime import datetime def fetch_air_quality(): 从公开接口获取全国空气质量数据 url https://example.com/api/air/city/all # 实际项目中替换为真实数据地址 params { app_id: your_app_id, app_secret: your_app_secret, city: 湖南省 } try: resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() return data.get(data, {}).get(cities, []) except Exception as e: print(f[{datetime.now()}] 数据请求失败: {e}) return None def load_to_storage(cities_data): 将数据写入本地JSON作为前端数据源 if not cities_data: return payload { update_time: datetime.now().strftime(%Y-%m-%d %H:%M:%S), cities: cities_data } with open(air_data_cache.json, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse)这段代码的核心思想是请求一次、落盘一次。前端地图组件不再关心具体的接口地址它只需要定时读取这份本地JSON就够了。定时刷新我用的方案是APScheduler设置每30分钟执行一次抓取任务。考虑到空气质量监测本身不是秒级变化的数值30分钟的频率已经能很好地在数据时效性和接口稳定性之间取得平衡。2.3 空间数据关联城市名与经纬度的对齐是地图渲染的基石拿到了城市的空气质量数值还不能直接画地图。WebGIS地图上渲染城市点位必须有每个城市的经纬度坐标。这一步加工看似简单实际上非常容易出问题。我在项目里维护了一张湖南各市州经纬度对照表涵盖了长沙、株洲、湘潭、衡阳、岳阳、常德、张家界、益阳、郴州、永州、怀化、娄底、湘西州等主要城市。数据接口返回的城市名称会与这张表做匹配匹配成功后合并出含空间坐标的记录CITY_LOCATION { 长沙: [112.9388, 28.2282], 株洲: [113.1337, 27.8274], 湘潭: [112.9441, 27.8297], 衡阳: [112.5720, 26.8934], 岳阳: [113.1289, 29.3568], 常德: [111.6985, 29.0317], 张家界: [110.4792, 29.1171], 益阳: [112.3551, 28.5541], 郴州: [113.0148, 25.7705], 永州: [111.6134, 26.4204], 怀化: [110.0016, 27.5698], 娄底: [112.0085, 27.7281], 湘西州: [109.7397, 28.3129], # 更多城市... }这里要提醒一句城市坐标的精度直接影响点位在地图上的落位效果。用市级行政中心坐标就够了不需要精确到站点坐标——因为这个项目做的是城市粒度的对比不是街道级别的空气质量监测。坐标差几公里在地图的全国或全省缩放下几乎看不出来但坐标如果填错到别的城市画出来的地图就会非常奇怪。3. 地图主引擎Leaflet图层设计与AQI分级着色3.1 为什么选Leaflet而不是OpenLayers或Mapbox可视化地图的技术选型我当时认真对比过三个主流方案。OpenLayers功能最全投影坐标系支持最丰富适合做专业GIS分析系统但它的API设计对新手不太友好写起代码来总觉得在做几何运算而不是画地图。Mapbox的视觉效果确实顶尖暗黑底图加高亮数据的组合非常出片但它的个性化底图服务很多涉及商业授权问题部署时容易踩合规坑。Leaflet成了这个项目的最终选择。它的体积非常轻量压缩后只有40KB左右插件生态成熟地图瓦片、标记点、弹窗、热力图都有现成方案而且资源来自公共CDN抄起代码就能直接跑。对于一个空气质量可视化项目来说Leaflet的表达能力已经绰绰有余。3.2 点位与弹窗让地图“会说话”的基础结构地图渲染的核心逻辑分成三步。第一步是初始化地图指定中心点和缩放级别。湖南省的地图视角我设置为以长沙和株洲之间为中点初始缩放级别6这样全省各市州都能完整落入视窗。第二步是在地图上添加城市的圆形标记点。这里用L.circleMarker而不是L.marker原因有两个circleMarker的样式完全由代码控制不需要加载外部图标文件它的半径可以动态绑定到AQI数值上数值越大、圆圈越大视觉上就能直接形成“污染扩散”的冲击感。第三步是配置弹窗内容。点击任意一个城市标记点弹窗里展示该城市当前的AQI数值、污染等级、首要污染物和更新时间和一小时的浓度变化摘要。这个弹窗里的数据是从后端JSON里取出来动态拼接成的HTMLconst popupContent strong${city.name}/strongbr/ AQIspan stylecolor:${aqiColor(city.aqi)};font-weight:bold;${city.aqi}/spanbr/ 等级${city.level}br/ 首要污染物${city.primary}br/ 数据时间${city.time} ; L.circleMarker([city.lat, city.lng], { radius: Math.max(6, Math.sqrt(city.aqi) * 1.8), color: #ffffff, weight: 1, fillColor: aqiColor(city.aqi), fillOpacity: 0.75 }) .addTo(map) .bindPopup(popupContent);3.3 AQI分级配色的核心逻辑空气质量AQI分级是国家标准给定的我们不能自己瞎定义。按照HJ 633-2012技术规定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; }还有一个容易被忽略的细节标记点不要全部叠在地图图层之上最好把其中一个标记点的样式做成“当前选中高亮”——当用户从右侧城市列表点击某个城市时对应标记点放大并置于顶层其他点透明度降低。这种视觉上的主次区分能让地图在信息密度较高时保持可读性。3.4 热力图图层污染分布的另一种表达城市点位图适合精确查询但如果你想知道“湖南整体的污染空间分布趋势”点状图就有点力不从心了。所以我在项目里额外加了热力图图层使用Leaflet的leaflet.heat插件。热力图的定位依据是每个城市的经纬度和AQI数值数值高的地方热量强度就高。核心代码只有几行const heatData cities.map(city [city.lat, city.lng, city.aqi / 300]); const heatLayer L.heatLayer(heatData, { radius: 40, blur: 30, maxZoom: 10 });图层加好之后我用一个右上角的图层切换控件让用户在城市点位图和热力图之间自由切换。两种视图各有优势点位图精确热力图直观。实际在项目演示中热力图用于展示污染带走向和区域整体水平效果尤其好——比如湖南东部和北部重工业城市密集的区域在热力图上会自然形成一片红色聚集区。4. 分析透视图ECharts联动时序与城市对比4.1 从地图到图表两种可视化的互补关系地图把空间维度讲清楚了但时间维度的变化还是空白。某个城市过去24小时空气质量的趋势如何全省哪个城市今天最干净、哪个又垫底这些问题是纯粹的地图无法回答的需要引入传统的数据图表来补位。我选择ECharts作为图表库理由很简单它对时空数据可视化的支持成熟折线图、柱状图、雷达图开箱即用与Leaflet之间的数据联动也容易实现。更关键的是ECharts全面支持中文文档和社区讨论遇到问题搜索解决方案的效率比一般图表库高得多。4.2 全省排行榜让数据立刻分出高下排行榜的设计初衷是做“一目了然”的对比。我在地图右侧设置了一个垂直柱状图展示湖南各市州的实时AQI排名柱子的长度代表AQI数值颜色沿用AQI分级配色。这个排行榜是自动排序的污染最重或最轻的城市会自然排在顶部或底部大大减少了用户在密密麻麻的地图标记点中寻找信息的认知负担。const sortedCities [...cities].sort((a, b) b.aqi - a.aqi); const topList sortedCities.slice(0, 10); option { grid: { left: 90, right: 30, top: 20, bottom: 30 }, xAxis: { type: value, name: AQI }, yAxis: { type: category, data: topList.map(c c.name).reverse() }, series: [{ type: bar, data: topList.map(c ({ value: c.aqi, itemStyle: { color: aqiColor(c.aqi) } })), barWidth: 14, label: { show: true, position: right } }] };我额外给这个排行榜做了一个联动功能点击柱状图上的某个城市地图上的对应标记点会让视野平移过去并打开该城市的弹窗。图表组件之间的双向联动是这个项目体验感提升最明显的一处设计。很多初级项目只会做地图单方向展示加上双向交互之后整个系统的“分析感”完全不同了。4.3 历史趋势把24小时浓缩成一条曲线排行榜提供的是某一瞬间的面面观但空气质量的短时波动才是很多使用者最关心的。我用ECharts折线图展示选中城市最近24小时的AQI变化轨迹每条折线按小时打点X轴是时间Y轴是AQI值。曲线的颜色不再按单一AQI值分级而是分成多段着色——在轻度污染区间和中度污染区间的曲线段颜色不同这样曲线的升降过程本身就带上了健康提示的意味。趋势图的数据来源是历史明细文件。数据抓取模块在每次定时刷新后除了更新实时缓存还会把每个城市的当前AQI追加到历史记录中。这样持续运行一天以后24小时曲线自然成形。如果你想立即看到效果源码里附带的历史数据文件已经预置了近一周的数据记录开箱即用。5. 完整运行清单从下载源码到浏览器出图的每一步5.1 环境准备与项目结构这个项目的运行环境非常亲民只要你电脑上装了Python和Node.js就能跑。我的开发环境是Python 3.9 Node 16不过实测Python 3.6以上都能正常运行Node版本只要不低于12问题也不大。下载源码并解压后你会看到这样一个目录结构air-quality-webgis/ │ ├── backend/ # 后端服务 │ ├── app.py # Flask应用主入口 │ ├── data_fetcher.py # 空气质量数据采集模块 │ ├── scheduler.py # 定时任务模块 │ └── cache/ # 数据缓存目录 │ ├── air_data_cache.json # 实时数据缓存 │ └── history_data.json # 历史趋势数据 │ ├── frontend/ # 前端资源 │ ├── index.html # 主页面 │ ├── css/ │ │ └── style.css │ ├── js/ │ │ ├── map.js # Leaflet地图渲染 │ │ ├── chart.js # ECharts图表 │ │ └── main.js # 业务逻辑入口 │ └── lib/ # 第三方库 │ └── requirements.txt # Python依赖清单5.2 三步启动流程第一步安装后端依赖cd backend pip install -r requirements.txtrequirements.txt里包含flask、requests、apscheduler这几个核心库。如果你是用虚拟环境管理的项目建议先把虚拟环境启用再执行安装。第二步启动后端服务python app.py后端默认在5000端口启动Flask服务。启动成功后会看到两行日志一行表示HTTP服务已监听另一行表示定时任务已挂载。这时候你可以先在浏览器访问http://localhost:5000/api/air测试一下接口能否正常返回JSON格式的空气数据。第三步打开前端页面前端是一个纯静态页面不需要额外的编译构建步骤。直接通过后端提供的页面对外服务访问http://localhost:5000即可看到完整的地图可视化界面。地图加载出来之后如果一切正常你应该能看到湖南地图背景上分布着几十个颜色各异的圆形标记点点开某个城市能查看到实时AQI详情右侧同步显示全省AQI排行榜底部是城市的24小时趋势曲线。5.3 常见启动失败场景及对策我在把源码分享给朋友测试的时候遇到过几个典型的启动报错。这里统一列出来方便你排查端口被占用如果5000端口已被其他程序占用启动会报Address already in use。解决方案是在app.py里把app.run(host0.0.0.0, port5000)改成其他端口号。请求依赖库超时pip install时如果不确定依赖安装是否完整可以在Python交互环境里执行import flask和import requests测试缺哪个补装哪个。地图瓦片加载不出来Leaflet默认使用的是OpenStreetMap的公共瓦片服务偶尔会因为网络原因加载缓慢。如果遇到这种情况可以在map.js里把瓦片地址换成国内可达性更好的底图服务比如高德地图瓦片、天地图瓦片等只需替换一行URL即可。数据接口连接失败如果第三方空气质量接口近期调整或不可达后端日志会打印请求失败的错误信息。源码里已经做了降级处理——当获取不到新数据时会读取项目预置的缓存数据文件作为展示数据保证地图和图表不会白屏。6. 踩过的坑复盘五个容易被忽略的技术细节项目从开发到调试结束前前后后踩了不少坑。挑选五个最有代表性的记录下来如果你在做类似项目应该能少走一些弯路。第一个坑缩放级别设置不当导致地图视觉失衡设置CRS与缩放级别时如果初始缩放级别太小整个湖南省只在地图上占了巴掌大的区域城市标记点挤成一团没法看。实际测试下来缩放级别设置为6配合setView的中心点坐标112.5, 27.3全省视角最舒服。第二个坑JSON字符串编码错误导致中文乱码前后端数据传递过程中最容易翻车的就是中文编码。解决方式很笨也很有效字典在json.dumps时加上ensure_asciiFalse前端在fetch请求时指定response.json()解析不乱码。第三个坑circleMarker的radius值不做非线性映射时会失真有的城市AQI是40有的是220如果直接按AQI线性映射半径AQI值大的城市标记点会大到遮挡周围所有信息。我的解决方案是用平方根映射——radius Math.sqrt(aqi) * 1.8再取最小值6这样视觉上大小差异仍然明显但不会产生荒唐的巨型圆点。为什么用平方根AQI从50涨到200是4倍的关系但人眼感知的圆形面积是r的平方取根号之后视觉上的面积增幅才与数值增幅基本匹配。第四个坑定时任务叠加导致数据文件反复写入APScheduler默认是多线程执行如果抓取接口响应慢上一次任务还没跑完下一次任务又启动了两个任务同时写同一个缓存文件最终结果很可能是旧数据覆盖新数据。解决方式是给调度器加max_instances1参数强制同一个任务执行完一轮之前不允许启动下一轮。第五个坑历史记录无限增长导致JSON文件爆炸空气质量数据每30分钟追加一次跑一个月就是1440条记录如果一直累积不清理JSON文件会越来越大前端解析越来越慢。我在历史数据管理模块里加了滚动清理逻辑只保留最近7天的数据超出部分自动丢弃。对24小时趋势图来说7天的历史数据已经足够支撑演示和分析了。7. 扩展方向与二次开发建议项目跑通之后如果你打算把这个源码扩展到自己的场景里有几个方向我觉得很值得做。把城市粒度细化到监测站点级。目前项目展示的是城市平均空气质量如果你能拿到各个国控监测站点的经纬度和AQI数据就可以把渲染粒度从市级下沉到站点级。地图上的点位会从十几个变成几十个污染源的辨识度会大幅提升。接入更长周期的数据做季节分析。把定时抓取连续运行三个月积累下来的历史数据就能支持做季度趋势分析和污染高发时段分析。配合ECharts的热力图日历组件可以很直观地看到湖南哪个月份空气最差、哪个时段AQI最容易反弹。换成其他省份或全国范围。代码里城市的坐标表和高德瓦片地址都是可以更换的。如果你要把它改成“广东空气质量可视化”或者“全国重点城市空气质量看板”只需要修改城市坐标数据集和地图中心点即可后端数据接口如果返回全国数据前端会自动适配。接入预测模型。数据积累到一定规模后可以用历史AQI序列做简单的时间序列回归比如用线性回归结合当天的气象参数、风速、湿度预测未来几小时的污染趋势然后把预测结果和实测结果画在同一条时间轴上对比。这会让项目的技术含量再上一个台阶。本文还有配套的精品资源点击获取