Repository navigation
dcc.Store modified_timestamp property causing infinite loop in callback #408
Description
I discovered what appears to be a bug after trying and failing to adapt the "Generic Crossfilter Recipe" found here https://dash.plot.ly/interactive-graphing such that the x and y axes of the scatter plots can be set dynamically with dcc.Dropdown components.
Here is the code to reproduce the infinite loop caused by dcc.Store's modified_timestamp property being used as an input in callbacks to update my scatter plots. Warning, running the below will cause an infinite loop so please be sure to stop the run and exit your tab/browser after running:
import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
from dash.dependencies import Input, Output, State
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
np.random.seed(0)
df = pd.DataFrame({
'Column {}'.format(i): np.random.rand(30) + i*10
for i in range(6)})
app.layout = html.Div([
dcc.Store(id='data_store', storage_type='session'),
# html.Div(
# dcc.Dropdown(id='g1x', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[0])
# ),
# html.Div(
# dcc.Dropdown(id='g1y', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[1])
# ),
# html.Div(
# dcc.Dropdown(id='g2x', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[2])
# ),
# html.Div(
# dcc.Dropdown(id='g2y', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[3])
# ),
# html.Div(
# dcc.Dropdown(id='g3x', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[4])
# ),
# html.Div(
# dcc.Dropdown(id='g3y', options=[{'label': i, 'value': i} for i in df.columns], value=df.columns[5])
# ),
html.Div(
dcc.Graph(
id='g1',
config={'displayModeBar': False}
), className='four columns'
),
html.Div(
dcc.Graph(
id='g2',
config={'displayModeBar': False}
), className='four columns'),
html.Div(
dcc.Graph(
id='g3',
config={'displayModeBar': False}
), className='four columns'),
], className='row')
@app.callback(Output('data_store', 'data'),
[Input('g1', 'selectedData'), Input('g2', 'selectedData'), Input('g3', 'selectedData')],
[State('data_store', 'data')])
def update_data_store(g1_sel_data, g2_sel_data, g3_sel_data, prev_data):
print('update_data_store - A')
if not g1_sel_data and not g2_sel_data and not g3_sel_data:
return
print('update_data_store - B')
data = prev_data if prev_data else {'g1': None, 'g2': None, 'g3': None, 'sel_data': None}
if data['g1'] is not g1_sel_data:
data['sel_data'] = g1_sel_data
data['g1'] = g1_sel_data
if data['g2'] is not g2_sel_data:
data['sel_data'] = g2_sel_data
data['g2'] = g2_sel_data
if data['g3'] is not g3_sel_data:
data['sel_data'] = g3_sel_data
data['g3'] = g3_sel_data
print('update_data_store - C', data)
return data
@app.callback(Output('g1', 'figure'),
[#Input('g1x', 'value'), Input('g1y', 'value'),
Input('data_store', 'modified_timestamp')], [State('data_store', 'data')])
def update_figure_1(ts, stored_data):
print('UPDATE_FIGURE_1 - A', ts)
if not ts: raise dash.exceptions.PreventUpdate
print('UPDATE_FIGURE_1 - B', stored_data)
x = 'Column 0'
y = 'Column 1'
selected_data = stored_data['sel_data'] if stored_data else None
selected_points = get_selected_points(df.index, selected_data)
return get_figure(x, y, selected_points, selected_data, 'g1', stored_data)
@app.callback(Output('g2', 'figure'),
[#Input('g2x', 'value'), Input('g2y', 'value'),
Input('data_store', 'modified_timestamp')], [State('data_store', 'data')])
def update_figure_2(ts, stored_data):
if not ts: raise dash.exceptions.PreventUpdate
x = 'Column 2'
y = 'Column 3'
selected_data = stored_data['sel_data'] if stored_data else None
selected_points = get_selected_points(df.index, selected_data)
return get_figure(x, y, selected_points, selected_data, 'g2', stored_data)
@app.callback(Output('g3', 'figure'),
[#Input('g3x', 'value'), Input('g3y', 'value')])
Input('data_store', 'modified_timestamp')], [State('data_store', 'data')])
def update_figure_3(ts, stored_data):
if not ts: raise dash.exceptions.PreventUpdate
x = 'Column 4'
y = 'Column 5'
selected_data = stored_data['sel_data'] if stored_data else None
selected_points = get_selected_points(df.index, selected_data)
return get_figure(x, y, selected_points, selected_data, 'g3', stored_data)
def get_selected_points(df_index, selected_data):
selected_points = df_index
if selected_data is not None:
selected_index = [
p['customdata'] for p in selected_data['points']
]
if len(selected_index) > 0:
selected_points = np.intersect1d(selected_points, selected_index)
return selected_points
def get_figure(x, y, selected_points, selected_data, graph_id, stored_data):
figure = {
'data': [
{
'x': df[x],
'y': df[y],
'text': df.index,
'textposition': 'top',
'selectedpoints': selected_points,
'customdata': df.index,
'type': 'scatter',
'mode': 'markers+text',
'marker': {
'color': 'rgba(0, 116, 217, 0.7)',
'size': 12,
'line': {
'color': 'rgb(0, 116, 217)',
'width': 0.5
}
},
'textfont': {
'color': 'rgba(30, 30, 30, 1)'
},
'unselected': {
'marker': {
'opacity': 0.3,
},
'textfont': {
# make text transparent when not selected
'color': 'rgba(0, 0, 0, 0)'
}
}
},
],
'layout': {
'margin': {'l': 15, 'r': 0, 'b': 15, 't': 5},
'dragmode': 'select',
'hovermode': 'closest',
'showlegend': False
}
}
# Display a rectangle to highlight the previously selected region
shape = {
'type': 'rect',
'line': {
'width': 1,
'dash': 'dot',
'color': 'darkgrey'
}
}
if selected_data and selected_data is stored_data[graph_id] and selected_data['range']:
figure['layout']['shapes'] = [dict({
'x0': selected_data['range']['x'][0],
'x1': selected_data['range']['x'][1],
'y0': selected_data['range']['y'][0],
'y1': selected_data['range']['y'][1]
}, **shape)]
else:
figure['layout']['shapes'] = [dict({
'type': 'rect',
'x0': np.min(df[x]),
'x1': np.max(df[x]),
'y0': np.min(df[y]),
'y1': np.max(df[y])
}, **shape)]
return figure
if __name__ == '__main__':
app.run_server(debug=True)
Besides the infinite loops, I've also noticed that the 'selectedData' property of dcc.Graph does not always trigger a callback when passed as an input. Additionally, the 'data' property of dcc.Store also doesn't trigger call backs when passed as an input.
Activity
T4rk1n commentedon Dec 7, 2018
Can you list the versions used ?
kolaworld commentedon Dec 7, 2018
No problem:
mkhorton commentedon Jan 16, 2019
I can't find a minimal example (apologies), but I think I've also encountered this, trying to use dcc.Store following the same recipe caused my Dash app to stop rendering until I removed the Store from my layout completely. No error was shown. This was on Safari 12.0, dash==0.35.1, dash-core-components==0.42.1, dash-html-components==0.13.4, dash-renderer==0.16.1, running the app in debug mode.
mmartinsky commentedon May 23, 2019
Having the same issue, detailed it plotly/dash#745
EDIT: For anyone else having this issue, check out plotly/dash#680 for
dash.no_updatealexcjohnson commentedon May 2, 2020
FWIW in Dash v1.11 we substantially reworked callback handling, and one of the effects of this is that it's harder to generate infinite loops. It's not impossible, in particular
childrencallbacks like @mmartinsky describes in plotly/dash#745 can still generate loops that we currently have no way to detect or break, so as he correctly points outPreventUpdateand/orno_updateare your best bet. But the infinite loop described in this issue is now fixed.