The issue is frontend related, and has been spotted on (at least) two different machines:
OS: Linux (Ubuntu) / Windows
Browser: Firefox / Firefox
Version: 94.0 (64 bits) / 78.15.0esr (64 bits)
The bug
I'm working with a few tabs, the last of which displays several interconnected Plotly graphs and components. One of the graphs is considered as "the main graph", and making a selection from this graph results in changes in the other components displayed. When switching to another tab after a selection from the legend has been made and cleared (by double-clicking on an item then double-clicking again), then switching back, a new selection from the legend will not be properly reset.
I’ve included a minimal code example to reproduce the behaviour:
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)
And here are the detailed step to replicate the bug:
run the app as usual
go to tab 2
select any item from the legend (by double-clicking on it)
clear selection (by double-clicking anywhere on the legend again)
go to tab 1
go back to tab 2
select any item from the legend (by double-clicking on it)
clear selection by double-clicking again: the datatable is not updated
optional: select new value from slider
optional: select any item from the legend (by double-clicking on it)
optional: clear selection by double-clicking yet again: this time, it works as intended (from my experiments, the bug only affects the last value where a selection has been made before switching tabs).
Looking a bit into this bug, it appears that the restyleData property from the graph, which is normally used to detect a change in selection from the legend, isn’t updated when the selection is cleared and thus retains the previously selected item.
Expected behavior
I expect the legend selection to be completely cleared and to update the related components.
Screenshots
Hopefully, this gif can help better visualize my problem if running the minimal code is not an option:
Any insight on why this might happen or on how to solve it would be greatly appreciated :)
Keep up the good work!
I have recently run into a bug regarding the legend selection from Plotly graphs in my Dash app.
Environment
The bug
I'm working with a few tabs, the last of which displays several interconnected Plotly graphs and components. One of the graphs is considered as "the main graph", and making a selection from this graph results in changes in the other components displayed. When switching to another tab after a selection from the legend has been made and cleared (by double-clicking on an item then double-clicking again), then switching back, a new selection from the legend will not be properly reset.
I’ve included a minimal code example to reproduce the behaviour:
And here are the detailed step to replicate the bug:
Looking a bit into this bug, it appears that the restyleData property from the graph, which is normally used to detect a change in selection from the legend, isn’t updated when the selection is cleared and thus retains the previously selected item.
Expected behavior I expect the legend selection to be completely cleared and to update the related components.
Screenshots Hopefully, this gif can help better visualize my problem if running the minimal code is not an option:
Any insight on why this might happen or on how to solve it would be greatly appreciated :) Keep up the good work!