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
问题根源分析
- 缓存策略错误:
display_choropleth_map使用@st.cache_resource缓存Map对象,且通过hash_funcs={folium.Map: lambda _: None}忽略了Map实例的差异,导致筛选条件变化时,缓存返回旧的Map对象,与新DOM结构冲突。 - 可变数据污染:直接修改
choropleth.geojson.data['features']的属性,而GeoJson数据是可变对象,会导致缓存的副本被污染,后续渲染时数据异常。 - 组件复用冲突: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
相关产品推荐
相关产品推荐

