GIS项目Web可视化太丑?手把手教你用Streamlit打造高颜值交互界面(含:组件源码)

编程与开发
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

引言:如果你正在做“GIS项目Web可视化太丑?手把手教你用Streamlit打造高颜值交互界面(含:组件源码)”这类项目,通常不是空间分析做不出来,而是成果页面看起来像临时脚本:地图孤零零地放在页面中,参数散落,图表和指标没有层次,领导或客户一看就觉得“不像产品”。本文用一个可复用的 Streamlit GIS 可视化模板,带你把 GeoJSON、Shapefile 或 PostGIS 查询结果做成更像应用的交互界面。

本文适合 GIS 学生、空间数据分析师、WebGIS 初学者和需要快速交付原型的 GIS 工程师。我们不追求替代完整的 Vue、React 或 OpenLayers 工程,而是解决一个很实际的问题:如何用 Python 和 Streamlit 快速做出一个清爽、可交互、便于演示的 GIS Web 可视化页面。

Streamlit GIS可视化与WebGIS交互界面工作流
使用 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 项目通常由四层组成:

  1. 数据层:读取 GeoJSON、Shapefile、GeoPackage、CSV 经纬度点或 PostGIS 数据。
  2. 处理层:使用 GeoPandas 进行坐标系转换、字段清洗、空间筛选、面积或长度计算。
  3. 地图层:使用 Folium、streamlit-folium 或 PyDeck 显示交互地图。
  4. 界面层:使用 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 替换为自己的点数据,并保证包含 nametypedistrict 中的部分字段即可。

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")

如果不确定编码,可以尝试 gbkutf-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.crsgdf.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 可视化就会从“脚本截图”变成“可交付应用”。