Plotly Dash中如何通过模式匹配回调删除/修改指定父级元素
我修改了Dash社区的示例,用于实现模式匹配回调的所谓“三级”依赖。
我修改了哪些内容?
- 针对每个选中的国家,你可以在新增的
chart-area中点击add-chart添加图表。 - 为每个新增的图表添加了
delete删除按钮。
核心问题
如何从指定区域删除特定的图表?
原有代码
import os import dash import dash_bootstrap_components as dbc import dash_core_components as dcc import dash_html_components as html import plotly.express as px from dash.dependencies import Input, Output, State, MATCH, ALL from flask import Flask df = px.data.gapminder() mine_dir = os.path.abspath(os.path.dirname(__file__)) mine_dir = os.path.join(mine_dir, 'instance') srv = Flask(__name__, instance_path=mine_dir) external_js = ["http://code.jquery.com/jquery-3.3.1.min.js", "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"] app = dash.Dash(__name__, suppress_callback_exceptions=True, server=srv, external_stylesheets=[dbc.themes.CERULEAN], external_scripts=external_js) app.layout = html.Div([ html.Div(children=[ dcc.Dropdown( options=[{ 'label': i, 'value': i } for i in df.country.unique()], value='Canada', id='country', style={'display': 'inline-block', 'width': 200} ), html.Button( 'Add Chart area ', id='add-chart-area', n_clicks=0, style={'display': 'inline-block'} ), html.Div(id='del-me-please', children=[]) # attempt to add `independent` div element and store here ids to be removed by another callback. ]), html.Div(id='container', children=[]) ]) def create_figure(column_x, column_y, country): chart_type = px.line if column_x == 'year' else px.scatter return chart_type( df.query("country == '{}'".format(country)), x=column_x, y=column_y, ) \ .update_layout( title='{} {} vs {}'.format(country, column_x, column_y), margin_l=10, margin_r=0, margin_b=30) \ .update_xaxes(title_text='').update_yaxes(title_text='') @app.callback( Output('container', 'children'), Input('add-chart-area', 'n_clicks'), Input('del-me-please', 'children'), State('container', 'children'), State('country', 'value')) def display_dropdowns(n_clicks, to_del, children, country): if n_clicks: new_element = html.Details(id={ 'type': 'dynamic-details', 'index': country }, style={"marginLeft": "5px", "marginTop": "10px", 'width': '23%', 'display': 'inline-block', 'outline': 'thin lightgrey solid', 'padding': 10}, open=True, children=[ html.Summary(children=f"dynamic-details for country : {country}"), html.Div(children=[ html.Button( 'Add Chart', id={ 'type': 'add-chart', 'index': country }, n_clicks=0, style={'display': 'inline-block'}, className="btn btn-primary btn-xs" ), ], style={"marginLeft": "10px"}) ]) children.append(new_element) if to_del: res = [] for kid in children: if kid == to_del: continue else: res.append(kid) return res return children @app.callback( Output({'type': 'dynamic-details', 'index': MATCH}, 'children'), Input({'type': 'add-chart', 'index': MATCH}, 'n_clicks'), State({'type': 'dynamic-details', 'index': MATCH}, 'children'), State({'type': 'dynamic-details', 'index': MATCH}, 'id'), ) def display_charts(n_clicks, children, id): if not n_clicks: return children country = id.get('index') default_column_x = 'year' default_column_y = 'gdpPercap' new_element = html.Div(id={ 'type': 'chart-div-area', 'index': country + str(n_clicks), 'parent': country }, style={'width': '23%', 'display': 'inline-block', 'outline': 'thin lightgrey solid', 'padding': 10}, children=[ html.Button( 'Delete chart', id={ 'type': 'dell-chart', 'index': country + str(n_clicks), 'parent': country }, n_clicks=0, style={'display': 'inline-block'}, className="btn btn-danger btn-xs" ), html.Div(id={ 'type': 'deller_hidden_div', 'index': country + str(n_clicks), 'parent': country # attempt ot add 3-d level index. let call it `parent` }, children=[]), dcc.Graph( id={ 'type': 'dynamic-output', 'index': country + str(n_clicks), 'parent': country }, style={'height': 300}, figure=create_figure(default_column_x, default_column_y, country) ), dcc.Dropdown( id={ 'type': 'dynamic-dropdown-x', 'index': country + str(n_clicks), 'parent': country }, options=[{'label': i, 'value': i} for i in df.columns], value=default_column_x ), dcc.Dropdown( id={ 'type': 'dynamic-dropdown-y', 'index': country + str(n_clicks), 'parent': country }, options=[{'label': i, 'value': i} for i in df.columns], value=default_column_y ), ] ) children.append(new_element) return children @app.callback( Output({'type': 'dynamic-output', 'index': MATCH}, 'figure'), Input({'type': 'dynamic-dropdown-x', 'index': MATCH}, 'value'), Input({'type': 'dynamic-dropdown-y', 'index': MATCH}, 'value'), Input('country', 'value'), ) def display_output(column_x, column_y, country): return create_figure(column_x, column_y, country) @app.callback( Output('del-me-please', 'children'), Input({'type': 'dell-chart', 'index': ALL, 'parent': ALL}, 'n_clicks'), State({'type': 'dell-chart', 'index': ALL, 'parent': ALL}, 'id'), ) def my_shiny_remover(n_clicks, id_): # SOME MAGIC HERE res = [] if not n_clicks: return res parent = id_.get('parent') """ try to create div with elements I want to delete in separate div element and index of "parent" div. At this approach, I get that element does not exist. """ new_el = html.Div(id={'type': 'need-to-del', 'index': parent}, children=id_) res.append(new_el) return res if __name__ == '__main__': app.run_server(debug=True)
- 代码运行示例

- 预期实现效果:例如删除加拿大分区下的第2张图表,不影响其他国家的任意图表。

已尝试的方案
- 尝试了
MATCH、ALL模式的不同组合,均报错,报错信息如下:
del-me-please.children State 0 ({"index":MATCH,"parent":MATCH,"type":"dell-chart"}.id) has MATCH or ALLSMALLER on key(s) index, parent where Output 0 (del-me-please.children) does not have a MATCH wildcard. Inputs and State do not need every MATCH from the Output(s), but they cannot have extras beyond the Output(s).
- 尝试新增名为
parent的三级索引存储父级div的index,仍然出现同类报错,或提示“Output should have MATCH...”。 - 尝试在基础布局中新增独立
Div存储待删除元素ID,但输入的MATCH要求输出也带MATCH通配符,独立divdel-me-please的ID无匹配模式,实现失败。 - 尝试在动态div内新增独立的模式匹配div(示例中为
deller_hidden_div),在删除回调中追加index等于父级元素(示例中为选中国家)的div,仍未实现需求。
解决方案
报错本质是Dash模式匹配的规则限制:如果回调的输入/状态参数使用了MATCH通配符,那么回调的输出id必须包含相同的MATCH字段,全局普通id的div无法匹配该规则,因此不需要用del-me-please做中转,直接通过三级id的匹配规则实现删除即可。
修改点说明
- 移除原布局中无用的
del-me-pleasediv,以及display_dropdowns回调中对应的处理逻辑 - 新增删除回调,通过
MATCH绑定删除按钮的parent字段和对应国家区域的index字段,直接过滤要删除的图表元素 - 修复原
display_output回调的逻辑问题:原代码直接读取全局选中的国家值,会导致所有历史图表的数据源随当前选中国家变化,改为从图表id的parent字段读取所属国家,保证数据独立性
完整可运行修改后代码
import os import dash import dash_bootstrap_components as dbc import dash_core_components as dcc import dash_html_components as html import plotly.express as px from dash.dependencies import Input, Output, State, MATCH, ALL from flask import Flask df = px.data.gapminder() mine_dir = os.path.abspath(os.path.dirname(__file__)) mine_dir = os.path.join(mine_dir, 'instance') srv = Flask(__name__, instance_path=mine_dir) external_js = ["http://code.jquery.com/jquery-3.3.1.min.js", "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"] app = dash.Dash(__name__, suppress_callback_exceptions=True, server=srv, external_stylesheets=[dbc.themes.CERULEAN], external_scripts=external_js) app.layout = html.Div([ html.Div(children=[ dcc.Dropdown( options=[{ 'label': i, 'value': i } for i in df.country.unique()], value='Canada', id='country', style={'display': 'inline-block', 'width': 200} ), html.Button( 'Add Chart area ', id='add-chart-area', n_clicks=0, style={'display': 'inline-block'} ) ]), html.Div(id='container', children=[]) ]) def create_figure(column_x, column_y, country): chart_type = px.line if column_x == 'year' else px.scatter return chart_type( df.query("country == '{}'".format(country)), x=column_x, y=column_y, ) \ .update_layout( title='{} {} vs {}'.format(country, column_x, column_y), margin_l=10, margin_r=0, margin_b=30) \ .update_xaxes(title_text='').update_yaxes(title_text='') @app.callback( Output('container', 'children'), Input('add-chart-area', 'n_clicks'), State('container', 'children'), State('country', 'value')) def display_dropdowns(n_clicks, children, country): if n_clicks: new_element = html.Details(id={ 'type': 'dynamic-details', 'index': country }, style={"marginLeft": "5px", "marginTop": "10px", 'width': '23%', 'display': 'inline-block', 'outline': 'thin lightgrey solid', 'padding': 10}, open=True, children=[ html.Summary(children=f"dynamic-details for country : {country}"), html.Div(children=[ html.Button( 'Add Chart', id={ 'type': 'add-chart', 'index': country }, n_clicks=0, style={'display': 'inline-block'}, className="btn btn-primary btn-xs" ), ], style={"marginLeft": "10px"}) ]) children.append(new_element) return children @app.callback( Output({'type': 'dynamic-details', 'index': MATCH}, 'children'), Input({'type': 'add-chart', 'index': MATCH}, 'n_clicks'), State({'type': 'dynamic-details', 'index': MATCH}, 'children'), State({'type': 'dynamic-details', 'index': MATCH}, 'id'), ) def display_charts(n_clicks, children, id): if not n_clicks: return children country = id.get('index') default_column_x = 'year' default_column_y = 'gdpPercap' new_element = html.Div(id={ 'type': 'chart-div-area', 'index': country + str(n_clicks), 'parent': country }, style={'width': '23%', 'display': 'inline-block', 'outline': 'thin lightgrey solid', 'padding': 10}, children=[ html.Button( 'Delete chart', id={ 'type': 'dell-chart', 'index': country + str(n_clicks), 'parent': country }, n_clicks=0, style={'display': 'inline-block'}, className="btn btn-danger btn-xs" ), dcc.Graph( id={ 'type': 'dynamic-output', 'index': country + str(n_clicks), 'parent': country }, style={'height': 300}, figure=create_figure(default_column_x, default_column_y, country) ), dcc.Dropdown( id={ 'type': 'dynamic-dropdown-x', 'index': country + str(n_clicks), 'parent': country }, options=[{'label': i, 'value': i} for i in df.columns], value=default_column_x ), dcc.Dropdown( id={ 'type': 'dynamic-dropdown-y', 'index': country + str(n_clicks), 'parent': country }, options=[{'label': i, 'value': i} for i in df.columns], value=default_column_y ), ] ) children.append(new_element) return children @app.callback( Output({'type': 'dynamic-output', 'index': MATCH}, 'figure'), Input({'type': 'dynamic-dropdown-x', 'index': MATCH}, 'value'), Input({'type': 'dynamic-dropdown-y', 'index': MATCH}, 'value'), State({'type': 'dynamic-output', 'index': MATCH}, 'id'), ) def display_output(column_x, column_y, chart_id): country = chart_id['parent'] return create_figure(column_x, column_y, country) # 新增删除回调 @app.callback( Output({'type': 'dynamic-details', 'index': MATCH}, 'children'), Input({'type': 'dell-chart', 'parent': MATCH}, 'n_clicks'), State({'type': 'dynamic-details', 'index': MATCH}, 'children'), State({'type': 'dell-chart', 'parent': MATCH}, 'id'), prevent_initial_call=True ) def delete_chart(n_clicks_list, children, dell_chart_ids): # 找到被点击的删除按钮对应的图表id to_delete_idx = None for i, n_click in enumerate(n_clicks_list): if n_click > 0: to_delete_idx = dell_chart_ids[i]['index'] break if not to_delete_idx: return children # 过滤掉要删除的图表元素 new_children = [] for child in children: if isinstance(child, dict) and 'props' in child: child_id = child['props'].get('id', {}) if isinstance(child_id, dict) and child_id.get('type') == 'chart-div-area' and child_id.get('index') == to_delete_idx: continue new_children.append(child) return new_children if __name__ == '__main__': app.run_server(debug=True
相关产品推荐
相关产品推荐

