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中仍保留之前选中的项,状态和图表实际展示不一致。
解决思路
- 新增标签页切换事件监听,当用户切回Tab2时,强制重置
displayed_legends存储的状态,主动拉取当前图表的所有图例可见状态做同步,避免本地缓存和图表实际状态不一致 - 修改图例状态更新回调,除了监听
restyleData之外,额外把标签页当前值作为输入,当Tab切换到tab2时,主动校验当前图表的图例状态和存储的displayed_legends是否一致,不一致就做一次同步更新 - 临时规避方案可以关闭标签页的组件状态缓存,将Tab2的内容改成回调动态渲染,每次切到Tab2时重新渲染图表组件,从根源避免缓存带来的状态不一致问题
内容的提问来源于stack exchange,提问作者cons
相关产品推荐
相关产品推荐

