You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Streamlit切换过滤器时交互式Choropleth地图渲染异常排查

Streamlit + Folium 交互式地图切换筛选器时渲染失败的问题排查与解决

问题背景

使用Streamlit与Folium开发交互式Choropleth地图,支持通过年份、区域下拉框及滑块筛选数据(图1),但切换筛选条件时地图偶尔无法正常渲染(图2),控制台报Uncaught TypeError: Cannot read properties of undefined (reading 'style')错误(图3)。

图1:正常渲染的地图
正常渲染的地图

图2:渲染失败的地图
渲染失败的地图

图3:控制台报错
控制台报错

相关核心代码:

地图展示函数

def display_map(year, region, start, end, _geo_data, data):
    df = filter_data(data, year, region, start, end, 'Happiness Score')
    if df.empty:
        st.warning("No data available for the selected filters.")
        return "", "", ""
    
    myscale = get_scale(df, 'Happiness Score')
    map = display_choropleth_map(_geo_data, df, myscale)
    st_map = st_folium(map, width=700, height=450)
    country = ''
    happiness_rank = ''
    happiness_score = ''
    if st_map['last_active_drawing']:
        properties = st_map['last_active_drawing']['properties']
        country = properties.get('name', '')
        happiness_score = properties.get('happiness_score', '')
        happiness_rank = properties.get('happiness_rank', '')
    else:
        country = df["Country"].iloc[0]
        happiness_rank = df.loc[df["Country"] == country, "Happiness Rank"].iat[0]
        happiness_score = round(df.loc[df["Country"] == country, "Happiness Score"].iat[0],2)
    return country, happiness_rank, happiness_score

Choropleth图层创建函数

@st.cache_resource(hash_funcs={folium.Map: lambda _: None})
def display_choropleth_map(_geo_data, df, myscale):
    x_map = 17.51
    y_map = 22
    map = folium.Map(location=[x_map, y_map],
                     zoom_start=1, tiles=None, scrollWheelZoom=False)
    folium.TileLayer('CartoDB positron', name="Light Map",
                     control=False).add_to(map)

    choropleth = folium.Choropleth(
        geo_data=_geo_data,
        name='Choropleth',
        data=df,
        columns=['Country', 'Happiness Score'],
        key_on="feature.properties.name",
        fill_color='YlGnBu',
        threshold_scale=myscale,
        fill_opacity=1,
        line_opacity=0.2,
        legend_name='Happiness Score',
        smooth_factor=0
    ).add_to(map)

    def style_function(x): return {'fillColor': '#ffffff',
                                   'color': '#000000',
                                   'fillOpacity': 0.1,
                                   'weight': 0.1}

    def highlight_function(x): return {'fillColor': '#000000',
                                       'color': '#000000',
                                       'fillOpacity': 0.50,
                                       'weight': 0.1}

    df_indexed = df.set_index('Country')
    for feature in choropleth.geojson.data['features']:
        country = feature["properties"]['name']
        feature['properties']['happiness_score'] =round(df_indexed.loc[country,
                                                                  'Happiness Score'],2) if country in list(df_indexed.index) else 'N/A'
        feature['properties']['happiness_rank'] = int(
            df_indexed.loc[country, 'Happiness Rank']) if country in list(df_indexed.index) else 'N/A'

    NIL = folium.features.GeoJson(
        choropleth.geojson.data,
        style_function=style_function,
        control=False,
        highlight_function=highlight_function,
        tooltip=folium.features.GeoJsonTooltip(
            fields=['name', 'happiness_rank', 'happiness_score'],
            aliases=['Country: ', 'Happiness Rank', 'Happiness Score'],
            style=(
                "background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px;")
        )
    )
    map.add_child(NIL)
    map.keep_in_front(NIL)
    return map

问题根源分析

  1. 缓存策略错误:display_choropleth_map使用@st.cache_resource缓存Map对象,且通过hash_funcs={folium.Map: lambda _: None}忽略了Map实例的差异,导致筛选条件变化时,缓存返回旧的Map对象,与新DOM结构冲突。
  2. 可变数据污染:直接修改choropleth.geojson.data['features']的属性,而GeoJson数据是可变对象,会导致缓存的副本被污染,后续渲染时数据异常。
  3. 组件复用冲突:Streamlit默认复用相同组件的DOM,当地图数据更新但组件key未变化时,Folium生成的JS无法正确绑定新元素,导致读取style属性时出现undefined错误。

解决办法

1. 优化缓存策略

  • 移除hash_funcs={folium.Map: lambda _: None},让缓存能正确识别Map实例的差异;若测试时问题消失,可根据性能需求保留@st.cache_resource,确保所有影响地图的参数(df、myscale、_geo_data)都参与缓存哈希。
  • 若缓存导致的问题无法彻底解决,可暂时注释掉@st.cache_resource,先保证功能正常,再后续优化性能。

2. 避免修改原始GeoJson数据

创建GeoJson数据的深拷贝,防止污染原始数据:

import copy

# 在创建choropleth后,复制geo数据
geo_data_copy = copy.deepcopy(choropleth.geojson.data)
df_indexed = df.set_index('Country')

for feature in geo_data_copy['features']:
    country = feature["properties"]['name']
    feature['properties']['happiness_score'] = round(df_indexed.loc[country, 'Happiness Score'], 2) if country in df_indexed.index else 'N/A'
    feature['properties']['happiness_rank'] = int(df_indexed.loc[country, 'Happiness Rank']) if country in df_indexed.index else 'N/A'

# 使用拷贝后的GeoJson创建NIL层
NIL = folium.features.GeoJson(
    geo_data_copy,
    style_function=style_function,
    control=False,
    highlight_function=highlight_function,
    tooltip=folium.features.GeoJsonTooltip(
        fields=['name', 'happiness_rank', 'happiness_score'],
        aliases=['Country: ', 'Happiness Rank', 'Happiness Score'],
        style=(
            "background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px;")
    )
)

3. 强制组件重新渲染

在st_folium调用时添加与筛选条件绑定的唯一key,确保每次筛选变化时生成新组件:

st_map = st_folium(map, width=700, height=450, key=f"map_{year}_{region}_{start}_{end}")

4. 加固数据访问逻辑

避免KeyError,使用更安全的方式访问DataFrame数据:

feature['properties']['happiness_score'] = round(df_indexed.get(country, {}).get('Happiness Score', 'N/A'), 2) if country in df_indexed.index else 'N/A'
feature['properties']['happiness_rank'] = int(df_indexed.get(country, {}).get('Happiness Rank', 'N/A')) if country in df_indexed.index else 'N/A'

内容的提问来源于stack exchange,提问作者Nirmal Sankalana

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 18:35:02