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

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)
  • 代码运行示例
    example code in action
  • 预期实现效果:例如删除加拿大分区下的第2张图表,不影响其他国家的任意图表。
    What i want to trigger

已尝试的方案

  • 尝试了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的匹配规则实现删除即可。

修改点说明

  1. 移除原布局中无用的del-me-please div,以及display_dropdowns回调中对应的处理逻辑
  2. 新增删除回调,通过MATCH绑定删除按钮的parent字段和对应国家区域的index字段,直接过滤要删除的图表元素
  3. 修复原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
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 11:21:00