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

Plotly/Dash图表图例选择切换标签后无法正确清除问题求助

Dash多标签页下Plotly图例选择触发失效问题

问题描述

开发Dash应用时遇到Plotly图表图例选择相关bug:应用包含多个标签页,最后一个标签页内置了多个联动的Plotly图表与组件,其中主图表的图例选择操作会触发其他展示组件更新。若先完成图例选择+清除操作(双击选中某图例项后再次双击清除),切换到其他标签页再切回,后续新的图例选择清除操作就无法正常触发重置。

可复现问题的最小代码示例

import json
import random
import pandas as pd
pd.options.plotting.backend = "plotly"

import dash
from dash.dependencies import Input, Output, State
import dash_table as dt
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css',
                        "https://codepen.io/chriddyp/pen/brPBPO.css"]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

# -------------------- DATA --------------------

plot_df = pd.DataFrame({
        'x': [1,2,3,4,5],
        'y': [1,2,3,4,5],
        'text': ['a', 'b', 'c', 'd', 'e'],
        'label': [-1, -1, -1, -1, -1]
    })

# -------------------- LAYOUTS --------------------
tab1_layout = html.Div(
            [
                html.H1('Tab 1'),
                html.P('Please navigate to tab 2.')
            ]
)

tab2_layout = html.Div(
            [
                html.Div([html.H1('Tab 2')]),

                # Slider
                html.Div(
                    [
                        html.H3(
                            children=['Slider'],
                        ),

                        dcc.Slider(
                            id="slider",
                            min=2,
                            max=5,
                            step=1,
                            value=2,
                            marks={
                                2: "2",
                                5: "5",
                            },
                            disabled=False,
                        ),
                    ]
                ),

                # Graph
                html.Div(
                    [
                        html.H3(
                            children=['Graph'],
                        ),

                        dcc.Graph(
                            id="graph",
                            config={
                                'modeBarButtonsToRemove': ['autoScale2d', 'zoomIn2d', 'zoomOut2d', 'resetScale2d', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleSpikelines'],
                                'displaylogo': False
                                }
                        ),
                    ]
                ),

                # DataTable
                html.Div(
                    [
                        html.H3(
                            children=['DataTable'],
                        ),

                        dt.DataTable(
                            id='data_table',
                            sort_action="native",
                            page_action="native",
                            page_current= 0,
                            page_size= 100,
                            style_as_list_view=True
                        ),
                    ]
                ),
            ],
            style={
                'width':'50%',
            },
        )

app.layout = html.Div(
    [   
        dcc.Store(id='store_data', data=plot_df.to_json()),
        dcc.Store(id='displayed_legends'),
        dcc.Tabs(
            id='main_tabs', 
            value='tab1',
            children=[
                dcc.Tab(id="tab_1", label='1. Tab 1', value='tab1', children=[tab1_layout]),
                dcc.Tab(id="tab_2", label='2. Tab 2', value='tab2', children=[tab2_layout])
            ],
        ),
    ]
)

# -------------------- CALLBACKS --------------------

@app.callback(
[Output('graph', 'figure'),
Output('store_data', 'data')],

Input('slider', 'value'),

State('store_data', 'data')
)
def update_graph_from_slider(n, data_json) :
    plot_df = pd.read_json(data_json)

    labels_int = list(range(0, n))
    k = len(plot_df) - len(labels_int)
    if k > 0:
        labels_int += random.choices(range(0, n), k=k)
    labels = [str(e) for e in labels_int]

    plot_df['label'] = labels

    plot_df = plot_df.sort_values(by='label', ascending=True)
    plot_fig = plot_df.plot.scatter(x='x', y='y', hover_data={'x': False, 'y': False, 'label': True, 'text': True} ,color='label')

    return plot_fig, plot_df.to_json()

@app.callback(
    Output('displayed_legends', 'data'),
    Input('graph', 'restyleData'),
    State('displayed_legends', 'data')
)
def update_selected_legend_items(restyleEvent, displayedLegendsItems):
    if displayedLegendsItems == None:
        displayedLegendsItems = {}
    if restyleEvent != None:
        states = restyleEvent[0]['visible']
        labels = restyleEvent[1]
        for index in range(len(restyleEvent[0]['visible'])):
            if states[index] == 'legendonly':#legend item has been deactivated
                displayedLegendsItems[labels[index]] = False
            elif states[index] == True:#legend item has been activated
                displayedLegendsItems[labels[index]]  = True
    return displayedLegendsItems

@app.callback(
[Output('data_table', 'columns'),
Output('data_table', 'data'),],

[Input('graph', 'selectedData'),
Input('graph', 'relayoutData'),
Input('displayed_legends', 'data')],

State('store_data', 'data'),
)
def display_selected_data(selectedData, relayoutData, selectedTopics, data_json):
    res = get_filtered_nodes(data_json, selectedTopics, selectedData)
    df_table = pd.DataFrame(res)
    visibility = {}
    changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
    if 'graph' in changed_id:
        if 'relayoutData' in changed_id:
            relayoutDataKeys = relayoutData.keys()
            # if we zoomed, auto zoomed or rescaled,don't update the list
            if 'dragmode' in relayoutDataKeys or 'xaxis.range[0]' in relayoutDataKeys or 'xaxis.autorange' in relayoutDataKeys:
                res = dash.no_update
    if res != dash.no_update and len(res) == 0:
        visibility = {'display':'none'}

    return  [{"name": i, "id": i} for i in df_table.columns], df_table.to_dict('records')

# -------------------- FUNCTIONS --------------------
def get_filtered_nodes(data, selectedTopics, selectedData):
    res = []
    selectedTopicsKeys = selectedTopics.keys()

    if selectedData != None:#There is an active selection
        pointsList = json.loads(json.dumps(selectedData))
        if pointsList != None and len(pointsList['points']) > 0:#Some points are selected
            for point in pointsList['points']:
                text = point['customdata'][1]
                label = point['customdata'][0]
                if label not in selectedTopicsKeys or selectedTopics[label] == True:
                    res.append({'text': text, 'label': label})
    elif data != None:#No active selection
        dataArray = json.loads(data)
        textArray = dataArray['text']
        if 'label' in dataArray :
            labelsArray = dataArray['label']

        for key in textArray.keys():
            if labelsArray[key] not in selectedTopicsKeys or selectedTopics[labelsArray[key]] == True:
                res.append({'text': textArray[key], 'label': labelsArray[key]})
    return res


if __name__ == "__main__":
    app.run_server(debug=True)

环境依赖版本

  • Python == 3.9
  • pandas==1.3.3
  • dash==1.20.0
  • dash-core-components==1.16.0
  • dash-bootstrap-components==0.12.2
  • dash-html-components==1.1.3
  • dash-table==4.11.3

复现步骤

  • 正常启动应用
  • 进入标签页2
  • 双击选中任意图例项
  • 再次双击图例任意位置清除选择
  • 切换到标签页1
  • 切回标签页2
  • 双击选中任意图例项
  • 再次双击清除选择:DataTable未同步更新
  • 可选步骤:调整滑块到新值
  • 可选步骤:双击选中任意图例项后再次双击清除,此时功能恢复正常。该bug仅影响切换标签前完成过选择操作的滑块值。

原因定位

正常用于监听图例选择变化的图表restyleData属性,在清除选择操作时未触发更新,dcc.Store中仍保留之前选中的项,状态和图表实际展示不一致。

解决思路

  1. 新增标签页切换事件监听,当用户切回Tab2时,强制重置displayed_legends存储的状态,主动拉取当前图表的所有图例可见状态做同步,避免本地缓存和图表实际状态不一致
  2. 修改图例状态更新回调,除了监听restyleData之外,额外把标签页当前值作为输入,当Tab切换到tab2时,主动校验当前图表的图例状态和存储的displayed_legends是否一致,不一致就做一次同步更新
  3. 临时规避方案可以关闭标签页的组件状态缓存,将Tab2的内容改成回调动态渲染,每次切到Tab2时重新渲染图表组件,从根源避免缓存带来的状态不一致问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 09:09:13