dash icon indicating copy to clipboard operation
dash copied to clipboard

Loading states on chained callbacks (grandchildren)

Open chriddyp opened this issue 4 years ago • 4 comments

Currently, loading states are only triggered on the output if its immediate callback is being rendered.

Ideally, there would be a way to allow a chain of callbacks to trigger the loading states all the way down.

This would be useful in the "cache & signal" approach to sharing data between callbacks in https://dash.plotly.com/sharing-data-between-callbacks.

A current workaround is to have the intermediate callbacks output to "dummy" properties of the final outputs. Example:

import dash
from dash.dependencies import Input, Output
import dash_design_kit as ddk
import dash_html_components as html
import dash_core_components as dcc
import time

app = dash.Dash(__name__)

app.layout = html.Div([
     dcc.Input(id='input'),
     dcc.Store(id='data'),
     html.Div(id='output'),
     dcc.Loading(dcc.Graph(id='graph-1')),
     dcc.Loading(dcc.Graph(id='graph-2'))
])

@app.callback(
    Output('data', 'data'),
    Output('output', 'children'),
    Output('graph-1', 'className'),  # dummy output just to trigger the loading state on `graph-1`
    Output('graph-2', 'className'),  # dummy output just to trigger the loading state on `graph-2`
    Input('input', 'value')
)
def update_data(value):
    time.sleep(2)
    return value, value, dash.no_update, dash.no_update

@app.callback(Output('graph-1', 'figure'), Input('data', 'data'))
def display_graph_1(value):
    time.sleep(3)
    return {'layout': {'title': value}}

@app.callback(Output('graph-2', 'figure'), Input('data', 'data'))
def display_graph_2(value):
    time.sleep(3)
    return {'layout': {'title': value}}
    
if __name__ == '__main__':
    app.run_server(debug=True)

chriddyp avatar Jan 26 '21 02:01 chriddyp

Thanks for the hack to extend the loading functionality to other callbacks @chriddyp

I agree that having a proper way of doing this would be a great addition to the library.

Adding an extra prop to Loading to specify Inputs that need to be followed up might be a possibility?

For example, using your code, if the Loading function would accept a prop with a list of tuples to followup, as in followup=[('data', 'data')], it would mean that Loading would also have to wait for the callbacks where ('data', 'data') is an Output

import dash
from dash.dependencies import Input, Output
import dash_design_kit as ddk
import dash_html_components as html
import dash_core_components as dcc
import time

app = dash.Dash(__name__)

app.layout = html.Div([
     dcc.Input(id='input'),
     dcc.Store(id='data'),
     html.Div(id='output'),
     dcc.Loading(dcc.Graph(id='graph-1'), followup=[('data', 'data')]),
     dcc.Loading(dcc.Graph(id='graph-2'), followup=[('data', 'data')])
])

@app.callback(
    Output('data', 'data'),
    Output('output', 'children'),
    Input('input', 'value')
)
def update_data(value):
    time.sleep(2)
    return value, value, dash.no_update, dash.no_update

@app.callback(Output('graph-1', 'figure'), Input('data', 'data'))
def display_graph_1(value):
    time.sleep(3)
    return {'layout': {'title': value}}

@app.callback(Output('graph-2', 'figure'), Input('data', 'data'))
def display_graph_2(value):
    time.sleep(3)
    return {'layout': {'title': value}}
    
if __name__ == '__main__':
    app.run_server(debug=True)

Otherwise, a simpler way could be to consider this prop as a bool. If followup=True, it would extend to all the callbacks that have any of the Input values as Output values. Not sure if this generic approach would cover all cases though.

prl900 avatar Jul 31 '21 11:07 prl900

followup - That's a nice API, I like that. Thanks for the suggestion!

chriddyp avatar Aug 05 '21 22:08 chriddyp

This would be especially handy for long callback chains, where the hack requires a different dummy variable for each callback in the chain

joelalgee avatar Aug 24 '22 11:08 joelalgee

I know this is an old topic, but I'm working on updating dcc.Loading in PR #2760 and wanted to see if this was still an issue.

I tried the code above and without the intermediate callbacks output to "dummy" properties of the final outputs, and it seemed to work fine. Both loading spinners appear.
Should this be closed now?


import dash
from dash import Input, Output, dcc, html
import time

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Input(id='input'),
    dcc.Store(id='data'),
    html.Div(id='output'),
    dcc.Loading(dcc.Graph(id='graph-1')),
    dcc.Loading(dcc.Graph(id='graph-2'))
])


@app.callback(
    Output('data', 'data'),
    Output('output', 'children'),
  #  Output('graph-1', 'className'),  # dummy output just to trigger the loading state on `graph-1`
  #  Output('graph-2', 'className'),  # dummy output just to trigger the loading state on `graph-2`
    Input('input', 'value')
)
def update_data(value):
    time.sleep(2)
    return value, value #, dash.no_update, dash.no_update


@app.callback(Output('graph-1', 'figure'), Input('data', 'data'))
def display_graph_1(value):
    time.sleep(3)
    return {'layout': {'title': value}}


@app.callback(Output('graph-2', 'figure'), Input('data', 'data'))
def display_graph_2(value):
    time.sleep(3)
    return {'layout': {'title': value}}


if __name__ == '__main__':
    app.run_server(debug=True)

AnnMarieW avatar Feb 16 '24 23:02 AnnMarieW

This works for me as well, @AnnMarieW . Closing the issue. If someone reproduces the past erroneous behavior, we can reöpen.

Coding-with-Adam avatar Feb 20 '24 17:02 Coding-with-Adam