✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
📊 Dash 2.0 is Arriving. Register here.

Infinite loop with dcc.Store?

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.

1 Like