Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

dcc.Store modified_timestamp property causing infinite loop in callback #408

Closed
kolaworld opened this issue Dec 6, 2018 · 5 comments
Closed

Comments

@kolaworld
Copy link

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.

@T4rk1n
Copy link
Contributor

T4rk1n commented Dec 7, 2018

Can you list the versions used ?

@kolaworld
Copy link
Author

No problem:

  • dash==0.30.0
  • dash-html-components==0.13.2
  • dash-core-components==0.40.2
  • dash-table==3.1.8
  • dash-auth==1.2.0
  • dash-renderer==0.15.0

@mkhorton
Copy link

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
Copy link
Contributor

mmartinsky commented 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_update

@alexcjohnson
Copy link
Collaborator

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 children callbacks 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 out PreventUpdate and/or no_update are your best bet. But the infinite loop described in this issue is now fixed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants