GIS项目Web可视化太丑?手把手教你用Streamlit打造高颜值交互界面(含:组件源码)
引言:如果你正在做“GIS项目Web可视化太丑?手把手教你用Streamlit打造高颜值交互界面(含:组件源码)”这类项目,通常不是空间分析做不出来,而是成果页面看起来像临时脚本:地图孤零零地放在页面中,参数散落,图表和指标没有层次,领导或客户一看就觉得“不像产品”。本文用一个可复用的 Streamlit GIS 可视化模板,带你把 GeoJSON、Shapefile 或 PostGIS 查询结果做成更像应用的交互界面。
本文适合 GIS 学生、空间数据分析师、WebGIS 初学者和需要快速交付原型的 GIS 工程师。我们不追求替代完整的 Vue、React 或 OpenLayers 工程,而是解决一个很实际的问题:如何用 Python 和 Streamlit 快速做出一个清爽、可交互、便于演示的 GIS Web 可视化页面。

背景:为什么很多 GIS 项目 Web 可视化看起来很“临时”
背景:很多 GIS 项目在分析阶段做得很扎实,但到展示阶段会出现几个典型问题:
- 地图能显示,但页面缺少标题、说明、指标卡片和交互逻辑。
- 图层颜色随意,分类方式不清楚,用户不知道重点在哪里。
- 筛选条件写在脚本里,无法让非技术用户自己选择区域、类型或时间。
- 地图、表格、统计图彼此割裂,不能形成一个完整的业务看板。
- WebGIS 前端框架学习成本较高,短期内很难从零搭建完整项目。
Streamlit 的优势在于:它允许 GIS 人员继续使用熟悉的 Python 技术栈,例如 GeoPandas、Pandas、Folium、PyDeck、Plotly,同时用很少的代码完成页面布局、按钮、下拉框、指标卡片和交互地图。
对于“快速汇报、课程作业、项目原型、内部数据看板”这几类场景,Streamlit GIS 可视化非常适合。它不是最强的 WebGIS 生产框架,但很适合把 GIS 分析结果快速包装成一个能看的、能点的、能筛选的界面。
原理:Streamlit GIS 可视化的核心思路
原理:Streamlit 的运行方式可以理解为“用户每操作一次,脚本重新执行一次”。这和传统前端框架不同,但对 Python GIS 用户反而更友好。你只需要按顺序写代码:读取数据、筛选数据、计算指标、生成地图、显示图表,Streamlit 会自动把这些内容渲染成网页。
一个实用的 Streamlit GIS 项目通常由四层组成:
- 数据层:读取 GeoJSON、Shapefile、GeoPackage、CSV 经纬度点或 PostGIS 数据。
- 处理层:使用 GeoPandas 进行坐标系转换、字段清洗、空间筛选、面积或长度计算。
- 地图层:使用 Folium、streamlit-folium 或 PyDeck 显示交互地图。
- 界面层:使用 Streamlit 的 sidebar、columns、metric、tabs、selectbox、slider 组织页面。
在 GIS Web 可视化中,最容易被忽略的是坐标系。多数 Web 地图底图使用 Web Mercator,也就是 EPSG:3857;而 GeoJSON 常用 EPSG:4326。Folium 和 Leaflet 通常接收 WGS84 经纬度坐标,即 EPSG:4326。如果你的数据是 CGCS2000 高斯投影、地方坐标系或米制投影坐标,必须先确认并转换,否则地图可能出现偏移、飞到海里或完全不显示。
步骤:搭建一个高颜值 Streamlit GIS 交互界面
步骤:下面用一个“城市设施点位可视化看板”作为示例。你可以把数据替换为 POI、监测站、地块、管网、行政区、灾害点、巡检记录等 GIS 项目数据。
1. 准备 Python 环境
建议使用虚拟环境,避免和已有 GIS 环境冲突。以下依赖适合大多数 Streamlit GIS 可视化项目:
pip install streamlit geopandas pandas folium streamlit-folium plotly shapely pyproj
如果你需要读取 FileGDB、部分 Shapefile 编码或 PostGIS,可能还需要安装 GDAL、Fiona、SQLAlchemy、psycopg2 等库。初学阶段建议优先使用 GeoJSON 或 GeoPackage,环境问题更少。
2. 推荐项目目录结构
一个清晰的目录能让项目更像正式应用,而不是临时脚本:
streamlit-gis-dashboard/
├── app.py
├── data/
│ └── facilities.geojson
├── assets/
│ └── style.css
└── requirements.txt
其中 app.py 是主程序,data 存放空间数据,assets/style.css 用于定制界面样式,requirements.txt 用于部署。
3. 读取 GIS 数据并统一坐标系
下面的代码会读取 GeoJSON,并确保数据转换到 EPSG:4326,方便 Folium 显示。
import streamlit as st
import geopandas as gpd
import pandas as pd
st.set_page_config(
page_title="GIS设施点位可视化看板",
page_icon="🗺️",
layout="wide"
)
@st.cache_data
def load_data(path):
gdf = gpd.read_file(path)
if gdf.crs is None:
st.warning("数据没有定义坐标系,请先确认原始坐标系。这里暂按 EPSG:4326 处理。")
gdf = gdf.set_crs(epsg=4326)
if gdf.crs.to_epsg() != 4326:
gdf = gdf.to_crs(epsg=4326)
return gdf
gdf = load_data("data/facilities.geojson")
@st.cache_data 用于缓存数据读取结果。GIS 数据稍大时,这一步很重要,否则每次筛选都会重新读取文件,页面会变慢。
4. 添加页面标题、说明和指标卡片
好看的 GIS Web 页面不是只靠地图,而是靠信息层次。建议先给用户一个明确的页面标题,再用指标卡片说明数据规模和当前筛选结果。
st.title("城市设施点位 GIS 可视化看板")
st.caption("基于 Streamlit + GeoPandas + Folium 构建的交互式 GIS 展示界面")
total_count = len(gdf)
type_count = gdf["type"].nunique() if "type" in gdf.columns else 0
col1, col2, col3 = st.columns(3)
col1.metric("设施总数", f"{total_count:,}")
col2.metric("设施类型", f"{type_count}")
col3.metric("数据坐标系", "EPSG:4326")
如果你的数据包含面积、人口、监测值或风险等级,也可以把这些指标做成卡片。例如“总面积”“高风险点数量”“平均污染浓度”“本月新增巡检记录”等。
5. 设计侧边栏筛选组件
Streamlit 的侧边栏非常适合放筛选条件。GIS 项目中常见筛选包括行政区、类型、时间、等级、状态和数值范围。
st.sidebar.header("筛选条件")
filtered_gdf = gdf.copy()
if "type" in gdf.columns:
type_options = sorted(gdf["type"].dropna().unique().tolist())
selected_types = st.sidebar.multiselect(
"设施类型",
options=type_options,
default=type_options
)
filtered_gdf = filtered_gdf[filtered_gdf["type"].isin(selected_types)]
if "district" in gdf.columns:
district_options = sorted(gdf["district"].dropna().unique().tolist())
selected_district = st.sidebar.selectbox(
"行政区",
options=["全部"] + district_options
)
if selected_district != "全部":
filtered_gdf = filtered_gdf[filtered_gdf["district"] == selected_district]
st.sidebar.write(f"当前筛选结果:{len(filtered_gdf)} 条")
这里的关键不是组件本身,而是让筛选逻辑和地图、表格、图表联动。用户选择某个行政区后,地图和统计图都应该同步变化。
6. 使用 Folium 生成交互地图
Folium 基于 Leaflet,适合展示点、线、面数据,也适合 GIS 初学者快速上手。配合 streamlit-folium 可以嵌入 Streamlit 页面。
import folium
from streamlit_folium import st_folium
def create_map(data):
if data.empty:
m = folium.Map(location=[35.0, 105.0], zoom_start=4, tiles="CartoDB positron")
return m
center = [
data.geometry.centroid.y.mean(),
data.geometry.centroid.x.mean()
]
m = folium.Map(
location=center,
zoom_start=11,
tiles="CartoDB positron"
)
color_map = {
"学校": "#2E86DE",
"医院": "#E74C3C",
"公园": "#27AE60",
"交通": "#F39C12"
}
for _, row in data.iterrows():
geom = row.geometry
if geom.geom_type == "Point":
f_type = row.get("type", "其他")
color = color_map.get(f_type, "#7F8C8D")
popup_html = f"""
<b>名称:</b>{row.get("name", "未命名")}<br>
<b>类型:</b>{f_type}<br>
<b>行政区:</b>{row.get("district", "未知")}
"""
folium.CircleMarker(
location=[geom.y, geom.x],
radius=7,
color=color,
fill=True,
fill_color=color,
fill_opacity=0.75,
popup=folium.Popup(popup_html, max_width=260)
).add_to(m)
return m
map_obj = create_map(filtered_gdf)
st_folium(map_obj, width=None, height=560)
为了让页面更“高级”,底图可以优先选择 CartoDB positron 这类浅色底图。它比默认 OpenStreetMap 更干净,适合叠加专题数据。
7. 增加统计图和属性表
地图解决空间位置问题,图表解决数量对比问题,表格解决明细查看问题。三者组合起来,才像一个完整的 GIS 可视化界面。
import plotly.express as px
left, right = st.columns([1, 1])
with left:
st.subheader("类型统计")
if "type" in filtered_gdf.columns and not filtered_gdf.empty:
chart_df = filtered_gdf["type"].value_counts().reset_index()
chart_df.columns = ["type", "count"]
fig = px.bar(
chart_df,
x="type",
y="count",
text="count",
color="type",
title="不同设施类型数量"
)
fig.update_layout(showlegend=False, height=360)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("当前数据缺少 type 字段或筛选结果为空。")
with right:
st.subheader("属性明细")
show_cols = [c for c in ["name", "type", "district"] if c in filtered_gdf.columns]
if show_cols:
st.dataframe(filtered_gdf[show_cols], use_container_width=True, height=360)
else:
st.dataframe(filtered_gdf.drop(columns="geometry"), use_container_width=True, height=360)
如果你做的是面数据,比如地块、行政区或生态红线,可以把统计图改为面积汇总;如果你做的是时间序列监测点,可以用折线图展示变化趋势。
8. 添加自定义 CSS,让界面更像产品
Streamlit 默认界面已经可用,但稍微加一点 CSS,页面质感会明显提升。下面是一个简洁的组件样式示例:
def load_css():
st.markdown(
"""
<style>
.block-container {
padding-top: 2rem;
padding-bottom: 2rem;
}
div[data-testid="stMetric"] {
background-color: #ffffff;
border: 1px solid #e6eaf0;
padding: 16px;
border-radius: 14px;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
}
div[data-testid="stSidebar"] {
background-color: #f8fafc;
}
h1, h2, h3 {
color: #0f172a;
}
</style>
""",
unsafe_allow_html=True
)
load_css()
注意:Streamlit 自定义 CSS 依赖页面结构,版本变化可能导致部分选择器失效。因此 CSS 只建议用于视觉增强,不要把核心功能建立在 CSS hack 上。
9. 完整 app.py 组件源码
下面给出一个可运行的精简版组件源码。你只需要把 data/facilities.geojson 替换为自己的点数据,并保证包含 name、type、district 中的部分字段即可。
import streamlit as st
import geopandas as gpd
import folium
import plotly.express as px
from streamlit_folium import st_folium
st.set_page_config(
page_title="GIS设施点位可视化看板",
page_icon="🗺️",
layout="wide"
)
def load_css():
st.markdown(
"""
<style>
.block-container {
padding-top: 2rem;
padding-bottom: 2rem;
}
div[data-testid="stMetric"] {
background-color: #ffffff;
border: 1px solid #e6eaf0;
padding: 16px;
border-radius: 14px;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
}
div[data-testid="stSidebar"] {
background-color: #f8fafc;
}
</style>
""",
unsafe_allow_html=True
)
@st.cache_data
def load_data(path):
gdf = gpd.read_file(path)
if gdf.crs is None:
gdf = gdf.set_crs(epsg=4326)
if gdf.crs.to_epsg() != 4326:
gdf = gdf.to_crs(epsg=4326)
return gdf
def create_map(data):
if data.empty:
return folium.Map(location=[35.0, 105.0], zoom_start=4, tiles="CartoDB positron")
center = [
data.geometry.centroid.y.mean(),
data.geometry.centroid.x.mean()
]
m = folium.Map(location=center, zoom_start=11, tiles="CartoDB positron")
color_map = {
"学校": "#2E86DE",
"医院": "#E74C3C",
"公园": "#27AE60",
"交通": "#F39C12"
}
for _, row in data.iterrows():
geom = row.geometry
if geom.geom_type != "Point":
continue
f_type = row.get("type", "其他")
color = color_map.get(f_type, "#7F8C8D")
popup_html = f"""
<b>名称:</b>{row.get("name", "未命名")}<br>
<b>类型:</b>{f_type}<br>
<b>行政区:</b>{row.get("district", "未知")}
"""
folium.CircleMarker(
location=[geom.y, geom.x],
radius=7,
color=color,
fill=True,
fill_color=color,
fill_opacity=0.75,
popup=folium.Popup(popup_html, max_width=260)
).add_to(m)
return m
load_css()
st.title("城市设施点位 GIS 可视化看板")
st.caption("Streamlit + GeoPandas + Folium 快速构建高颜值 GIS 交互界面")
gdf = load_data("data/facilities.geojson")
filtered_gdf = gdf.copy()
st.sidebar.header("筛选条件")
if "type" in gdf.columns:
type_options = sorted(gdf["type"].dropna().unique().tolist())
selected_types = st.sidebar.multiselect("设施类型", type_options, default=type_options)
filtered_gdf = filtered_gdf[filtered_gdf["type"].isin(selected_types)]
if "district" in gdf.columns:
district_options = sorted(gdf["district"].dropna().unique().tolist())
selected_district = st.sidebar.selectbox("行政区", ["全部"] + district_options)
if selected_district != "全部":
filtered_gdf = filtered_gdf[filtered_gdf["district"] == selected_district]
col1, col2, col3 = st.columns(3)
col1.metric("原始数据量", f"{len(gdf):,}")
col2.metric("筛选后数量", f"{len(filtered_gdf):,}")
col3.metric("几何类型", gdf.geometry.geom_type.mode()[0])
st.subheader("交互地图")
st_folium(create_map(filtered_gdf), width=None, height=560)
left, right = st.columns([1, 1])
with left:
st.subheader("类型统计")
if "type" in filtered_gdf.columns and not filtered_gdf.empty:
chart_df = filtered_gdf["type"].value_counts().reset_index()
chart_df.columns = ["type", "count"]
fig = px.bar(chart_df, x="type", y="count", text="count", color="type")
fig.update_layout(showlegend=False, height=360)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("暂无可统计字段。")
with right:
st.subheader("属性表")
display_gdf = filtered_gdf.drop(columns="geometry")
st.dataframe(display_gdf, use_container_width=True, height=360)
运行方式如下:
streamlit run app.py
运行后,浏览器会打开本地页面。你可以在侧边栏筛选类型和行政区,地图、统计图和表格会同步更新。
常见坑:Streamlit GIS 可视化最容易出问题的地方
常见坑:Streamlit GIS 项目看似简单,但 GIS 数据本身复杂,以下问题非常常见。
1. 地图不显示或点位跑偏
优先检查坐标系。Folium 中点位经纬度顺序是 [纬度, 经度],也就是 [y, x]。GeoPandas 的 Point 坐标是 x, y,对应经度和纬度。很多点位跑偏,就是把顺序写反了。
- 确认
gdf.crs是否为空。 - 确认是否需要
to_crs(epsg=4326)。 - 确认 Folium 的 location 是否写成
[geom.y, geom.x]。
2. Shapefile 中文字段乱码
Shapefile 对中文编码并不友好。如果出现乱码,建议优先转换为 GeoPackage 或 GeoJSON。也可以在读取时尝试指定编码,但不同数据来源不一定稳定。
gdf = gpd.read_file("data/facilities.shp", encoding="utf-8")
如果不确定编码,可以尝试 gbk、utf-8,但长期项目建议减少 Shapefile 作为中间格式。
3. 数据量一大页面就卡
Folium 适合中小规模交互展示,不适合一次性渲染几十万点。数据量大时可以考虑:
- 先按行政区、类型或时间筛选,再渲染地图。
- 对点数据做抽样展示。
- 使用聚合图层,例如 MarkerCluster。
- 使用 PyDeck 或矢量瓦片方案展示大规模点线面。
- 把复杂空间计算提前离线处理,不要每次页面交互都计算。
4. 面数据中心点计算报警告
如果你在 EPSG:4326 下直接计算面积、长度或中心点,GeoPandas 可能会提示结果不准确。原因是经纬度坐标单位是度,不是米。正确做法是先投影到适合本地的米制坐标系,再进行面积、长度等计算。
projected = gdf.to_crs(epsg=3857)
gdf["area_m2"] = projected.geometry.area
注意:EPSG:3857 适合 Web 显示,不一定适合精确面积计算。正式项目应选择当地合适的投影坐标系。
方法比较:Streamlit、Dash、传统 WebGIS 框架怎么选
方法比较:如果你的目标是快速做 GIS 可视化演示,Streamlit 很合适;如果要做复杂前端系统,就要谨慎选择。
| 方案 | 适合场景 | 优点 | 限制 |
|---|---|---|---|
| Streamlit + Folium | GIS 原型、课程作业、内部看板、数据分析成果展示 | 上手快,Python 友好,界面组件丰富 | 复杂前端交互能力有限,大数据地图性能有限 |
| Streamlit + PyDeck | 大规模点数据、热力效果、三维感可视化 | 视觉效果好,适合点云和大规模点渲染 | GIS 图层控制不如专业 WebGIS 框架灵活 |
| Dash + Plotly | 数据看板、统计图交互、企业分析应用 | 图表交互强,结构更工程化 | 开发复杂度高于 Streamlit |
| Vue/React + OpenLayers | 正式 WebGIS 系统、图层管理、空间查询、业务平台 | 可控性强,工程化能力强 | 前端学习成本高,开发周期更长 |
| Leaflet 原生开发 | 轻量级 Web 地图、嵌入式地图页面 | 生态成熟,插件丰富 | 需要写 JavaScript,和 Python 分析流程衔接较弱 |
简单判断:如果你要在一天内把 GIS 分析结果做成可交互页面,选 Streamlit;如果你要做长期维护的业务系统,选 OpenLayers、Leaflet 或 Cesium 等更专业的 WebGIS 技术栈。
检查清单:发布前先检查这 12 项
检查清单:在把 Streamlit GIS 可视化页面交给老师、客户或领导之前,建议逐项检查。
- 数据是否有正确坐标系,是否统一到地图组件需要的坐标系。
- 地图中心点和缩放级别是否合适,打开页面后是否能直接看到数据。
- 专题颜色是否有业务含义,不要随机使用过多颜色。
- 侧边栏筛选是否会导致空数据,空数据时是否有提示。
- 指标卡片是否能解释当前页面的核心信息。
- 地图弹窗是否包含名称、类型、行政区等必要字段。
- 图表是否与当前筛选结果联动。
- 属性表是否隐藏了无意义字段,例如内部编号、临时字段或过长 WKT。
- 数据量较大时是否使用缓存、筛选、抽样或聚合。
- 页面标题、说明文字和图例是否让非 GIS 用户也能理解。
- 部署环境是否写好
requirements.txt。 - 敏感坐标、涉密数据或内部属性是否已经脱敏。
FAQ:Streamlit GIS Web 可视化常见问题
FAQ:下面整理几个使用 Streamlit 做 GIS Web 可视化时最常被问到的问题。
Q1:Streamlit 能不能做真正的 WebGIS 系统?
可以做轻量级 GIS Web 应用和原型系统,但不建议把它当成完整 WebGIS 平台的替代品。Streamlit 更适合展示、筛选、统计和分析结果交互。如果你需要复杂图层树、绘制编辑、权限体系、空间查询服务、瓦片服务管理,建议使用 OpenLayers、Leaflet、Cesium 或完整前后端架构。
Q2:Streamlit GIS 可视化支持 Shapefile 吗?
支持,只要 GeoPandas 能读取即可。但 Shapefile 容易遇到中文编码、字段名长度、多个附属文件丢失等问题。实际项目中更推荐使用 GeoPackage 或 GeoJSON。GeoPackage 更适合保存较复杂的 GIS 数据,GeoJSON 更适合 Web 展示和接口传输。
Q3:为什么我的 Folium 地图点位没有显示?
常见原因有三个:第一,数据坐标系不是 EPSG:4326;第二,经纬度顺序写反;第三,几何类型不是 Point,而代码只处理了点数据。可以先打印 gdf.crs、gdf.geometry.geom_type.value_counts() 和前几条坐标检查。
Q4:如何让 Streamlit 页面更好看?
优先从布局和信息层次入手,而不是只改颜色。建议使用宽屏布局、指标卡片、左右分栏、侧边栏筛选、浅色底图、统一配色和简短说明。必要时再用少量 CSS 调整间距、卡片边框和阴影。
Q5:数据量很大时还适合用 Streamlit + Folium 吗?
不太适合直接渲染大规模数据。几千到几万条以内可以通过筛选、缓存和聚合勉强处理,几十万级以上建议考虑 PostGIS 后端查询、矢量瓦片、PyDeck 或专业 WebGIS 前端方案。Streamlit 可以作为查询和展示入口,但不要把所有数据一次性塞进浏览器。
Q6:Streamlit 项目如何部署给别人访问?
可以部署到 Streamlit Community Cloud、服务器、Docker 或内网环境。部署前需要准备 requirements.txt,并确认数据路径、字体、依赖库和服务器访问权限。涉及内部 GIS 数据时,优先选择内网部署或加访问控制。
结论:用 Streamlit 快速补齐 GIS 项目的展示短板
结论:GIS 项目 Web 可视化太丑,很多时候不是分析能力不足,而是缺少一个清晰的界面组织方法。Streamlit 的价值在于让 GIS 人员用 Python 快速完成“地图 + 筛选 + 指标 + 图表 + 表格”的完整闭环。
如果你只是想把空间分析成果快速做成演示页面,Streamlit GIS 可视化是非常高效的选择;如果项目进入长期业务系统阶段,再迁移到 OpenLayers、Leaflet、Cesium 或完整前后端架构也不迟。建议你先用本文的组件源码跑通一个最小版本,再逐步加入自己的业务字段、专题样式和空间分析结果。
最后记住一个原则:高颜值 GIS 界面不是堆特效,而是让用户更快看懂空间问题。地图负责位置,图表负责对比,指标负责总结,筛选负责交互。把这四件事组织好,你的 GIS Web 可视化就会从“脚本截图”变成“可交付应用”。