在Plotly Dash Mapbox中实现几何图层的显示切换
问题描述
我参考了Stack Overflow上的《Plotly Mapbox Markers not rendering (other than circle)》帖子,实现了在Plotly Mapbox上绘制Maki符号,基础功能代码如下:
import dash from dash import Dash, dcc, html, Input, Output import dash_bootstrap_components as dbc import plotly.express as px import plotly.graph_objs as go import numpy as np import requests import svgpath2mpl, shapely.geometry, shapely.affinity from pathlib import Path from zipfile import ZipFile import pandas as pd import geopandas as gpd import json # 下载maki图标 f = Path.cwd().joinpath("maki") if not f.is_dir(): f.mkdir() f = f.joinpath("maki.zip") if not f.exists(): r = requests.get("https://github.com/mapbox/maki/zipball/main") with open(f, "wb") as f: for chunk in r.iter_content(chunk_size=128): f.write(chunk) fz = ZipFile(f) fz.extractall(f.parent) def to_shapely(mpl, simplify=0): p = shapely.geometry.MultiPolygon([shapely.geometry.Polygon(a).simplify(simplify) for a in mpl]) p = shapely.affinity.affine_transform(p,[1, 0, 0, -1, 0, 0],) p = shapely.affinity.affine_transform(p,[1, 0, 0, 1, -p.centroid.x, -p.centroid.y],) return p # 将SVG图标转换为matplotlib几何图形,再转为shapely几何图形 # 把图标存在数据框中方便后续调用... SIMPLIFY=.1 dfi = pd.concat( [ pd.read_xml(sf).assign( name=sf.stem, mpl=lambda d: d["d"].apply( lambda p: svgpath2mpl.parse_path(p).to_polygons() ), shapely=lambda d: d["mpl"].apply(lambda p: to_shapely(p, simplify=SIMPLIFY)), ) for sf in f.parent.glob("**/*.svg") ] ).set_index("name") # 构建可用于Plotly Mapbox图表布局的GeoJSON图层 def marker(df, marker="marker", size=1, color="green", lat=51.379997, lon=-0.406042): m = df.loc[marker, "shapely"] if isinstance(lat, float): gs = gpd.GeoSeries( [shapely.affinity.affine_transform(m, [size, 0, 0, size, lon, lat])] ) elif isinstance(lat, (list, pd.Series, np.ndarray)): gs = gpd.GeoSeries( [ shapely.affinity.affine_transform(m, [size, 0, 0, size, lonm, latm]) for latm, lonm in zip(lat, lon) ] ) return {"source":json.loads(gs.to_json()), "type":"fill", "color":color}
该方法直接绘制符号时运行正常,但我希望添加一个交互组件,让用户可以切换这些Maki符号的显示与隐藏。我尝试将符号添加到单个图层中,通过更新布局来实现切换功能,请问这种实现方式是否可行?我的尝试代码如下:
us_cities = pd.read_csv( 'https://raw.githubusercontent.com/plotly/datasets/master/us-cities-top-1k.csv' ) external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP] app = dash.Dash(__name__, external_stylesheets = external_stylesheets) app.layout = html.Div([ dcc.Checklist( id="symbol_on", options=[{"label": "Symbol", "value": True}], value=[], inline=True ), html.Div([ dcc.Graph(id="the_graph") ]), ]) @app.callback( Output("the_graph", "figure"), Input('symbol_on', 'value') ) def update_graph(symbol_on): fig = go.Figure() scatter = px.scatter_mapbox(data_frame = us_cities, lat = 'lat', lon = 'lon', zoom = 0, hover_data = ['State', 'lat', 'lon'] ) fig.add_traces(list(scatter.select_traces())) fig.update_layout( height = 750, mapbox=dict( style='carto-positron', ), ) star = marker( dfi, "star", size=.1, color="red", lon=[-70, -80, -90], lat=[30, 40, 45] ), airport = marker( dfi, "airport", size=.1, color="green", lon=[-70, -80, -90], lat=[30, 40, 45] ), layers = [] for lyr in symbol_on: layers.append(star) layers.append(airport) fig.update_layout(mapbox={"layers": layers}) return fig if __name__ == '__main__': app.run_server(debug=True, port = 8050)
解答
这种通过更新Mapbox图层来切换符号显示隐藏的方式完全可行,但你的尝试代码存在两个关键问题:
- 变量类型错误:定义
star和airport时末尾多了逗号,导致这两个变量变成元组而非预期的图层字典,Plotly无法解析该格式。 - 图层添加逻辑冗余:Checklist选中时
symbol_on为[True],用循环遍历添加图层完全多余,直接判断列表是否非空即可。
修正后的完整代码如下:
us_cities = pd.read_csv( 'https://raw.githubusercontent.com/plotly/datasets/master/us-cities-top-1k.csv' ) external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP] app = dash.Dash(__name__, external_stylesheets = external_stylesheets) app.layout = html.Div([ dcc.Checklist( id="symbol_on", options=[{"label": "显示符号", "value": "show"}], value=[], inline=True ), html.Div([dcc.Graph(id="the_graph")]), ]) @app.callback( Output("the_graph", "figure"), Input('symbol_on', 'value') ) def update_graph(symbol_on): fig = go.Figure() scatter = px.scatter_mapbox( data_frame = us_cities, lat = 'lat', lon = 'lon', zoom = 0, hover_data = ['State', 'lat', 'lon'] ) fig.add_traces(list(scatter.select_traces())) fig.update_layout( height = 750, mapbox=dict(style='carto-positron') ) # 去掉末尾逗号,确保变量是图层字典 star = marker(dfi, "star", size=.1, color="red", lon=[-70, -80, -90], lat=[30, 40, 45]) airport = marker(dfi, "airport", size=.1, color="green", lon=[-70, -80, -90], lat=[30, 40, 45]) layers = [] # 直接判断是否勾选,简化逻辑 if "show" in symbol_on: layers.extend([star, airport]) fig.update_layout(mapbox={"layers": layers}) return fig if __name__ == '__main__': app.run_server(debug=True, port = 8050)
如果需要更精细的控制(比如单独切换不同类型符号),可以修改Checklist选项为多个值,示例如下:
dcc.Checklist( id="symbol_on", options=[ {"label": "显示星星", "value": "star"}, {"label": "显示机场", "value": "airport"} ], value=[], inline=True )
然后在回调中根据选中的值添加对应图层:
layers = [] if "star" in symbol_on: layers.append(star) if "airport" in symbol_on: layers.append(airport)
内容的提问来源于stack exchange,提问作者Chopin
相关产品推荐
相关产品推荐

