dcc.Loading: Works with Single Callback, but not with Multiple

I have a dashboard in which 3 Data Tables are updated when a new date range is selected from the DateRangePicker. While the data is refreshing for each of the 3 data tables, the ‘default’ loading animation for dcc.Loading is displayed.

I recently added a Button which can update the DateRangePicker with a specific “start_date” and “end_date”. When the Button is clicked, the DateRangePicker updates properly, and the Data Tables refresh.

However, the loading animations do not play when I click the Button to update the DateRangePicker, which then automatically updates the Data Tables.

To be clear, if I use the DateRangePicker, the loading animations still play properly. But if I update the date range by clicking the Button (“Update 5.0”), then no loading animation is played, even though the data refreshes.

Relevant snippets from my code:


DateRangePicker

dcc.DatePickerRange(
                id='my-date-picker-range',
                min_date_allowed=date(2018, 12, 14),
                max_date_allowed=date.today(),
                start_date=date.today() - timedelta(days=7),
                end_date=date.today(),
                display_format='D/M/Y'
            )

Data Table

    dbc.Col(
            html.Div(
                children=[
                    dash_table.DataTable(
                        id='datatable-weapons',
                        columns=[{"name": 'Rank', "id": "ranking"},
                                 {"name": 'Weapons', "id": 'weapon'},
                                 {"name": 'Times Used', "id": 'times_used'},
                                 {"name": '% Used', "id": 'pct_used'}
                        ],
                        style_header={
                                'fontWeight': 'bold',
                        },
                        style_cell={
                            'textAlign':'center',
                        },
                        style_cell_conditional=[
                            {
                                'if': {'column_id': 'weapon'},
                                'textAlign': 'left',
                            }
                        ],
                        style_as_list_view=True,
                        pagination_settings={
                                'current_page': 0,
                                'page_size': PAGE_SIZE
                            },
                        pagination_mode='be'
                    ),
                    dcc.Loading(id="loading-1", children=[html.Div(id="loading-output-2")], type="default")
                ]
            )
        )

Callback for Button click to update DateRangePicker

@app.callback(
    [Output('my-date-picker-range', 'start_date'), Output('my-date-picker-range', 'end_date')],
    [Input('button-update-5', 'n_clicks')])
def update_output(n_clicks):
    if n_clicks > 0:
        start_date = date(2019, 2, 12)
        end_date = date.today()
        return start_date, end_date

Callback for one of the data tables

@app.callback(
    [Output('datatable-weapons', 'data'), Output('weapon-download-link', 'href'), Output('loading-output-1', 'children')],
    [Input('my-date-picker-range', 'start_date'), Input('my-date-picker-range', 'end_date'),
     Input('range-slider', 'value'), Input('prestige-range-slider', 'value'), Input('datatable-weapons', 'pagination_settings')])
def update_graph(begin_dt, end_dt, rank_range, prestige_range, pagination_settings):
    df = pd.read_sql(
        create_sql('weapon', begin_dt, end_dt, rank_range[0], rank_range[1], prestige_range[0], prestige_range[1]), conn)
    df["weapon"] = df["weapon"].astype(str)
    df["times_used"] = df["times_used"].apply(lambda x: "{:,}".format(x))
    df["pct_used"] = df["pct_used"].map('{:,.2f}%'.format)

    csv_string = df.to_csv(index=False, encoding='utf-8')
    csv_string = "data:text/csv;charset=utf-8," + quote(csv_string)

    return df.iloc[
        pagination_settings['current_page']*pagination_settings['page_size']:
        (pagination_settings['current_page'] + 1)*pagination_settings['page_size']
    ].to_dict('rows'), csv_string, ''

@XFaCToZ2 Using the snippets and playing around a bit (a fully functional minimal app really would have been easier to deal with), I got this working as expected with dash==0.40.0. There doesn’t seem to be any inherent problem with dcc.Loading when attached to multiple callbacks.

# -*- coding: utf-8 -*-
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash_table import DataTable

from dash.dependencies import Input, Output, State

import pandas as pd

from datetime import date, datetime, timedelta
import time

url = 'https://github.com/plotly/datasets/raw/master/26k-consumer-complaints.csv'

rawDf = pd.read_csv(url)
df = rawDf.to_dict("rows"),

app = dash.Dash()
app.scripts.config.serve_locally = True

app.layout = html.Div(children=[
    html.Button(
        id='button-update',
        children=['Update']
    ),
    dcc.DatePickerRange(
        id='my-date-picker-range',
        min_date_allowed=date(2018, 12, 14),
        max_date_allowed=date.today(),
        start_date=date.today() - timedelta(days=7),
        end_date=date.today(),
        display_format='D/M/Y'
    ),
    dcc.Loading(
        id="loading-1",
        children=[
            DataTable(
                id='datatable-weapons',
                columns=[{"name": i, "id": i, "type": "numeric", 'format': {'locale': {'group': '.', 'decimal': ','}}} for i in rawDf.columns],
                data=[]
            )
        ]
    )])

@app.callback(
    [Output('my-date-picker-range', 'start_date'), Output('my-date-picker-range', 'end_date')],
    [Input('button-update', 'n_clicks')])
def update_output(n_clicks):
    if n_clicks is not None and n_clicks > 0:
        start_date = date(2019, 2, 12)
        end_date = date.today()
        return start_date, end_date

    return date(2019, 2, 23), date(2019, 2, 27)

@app.callback(
    [Output('datatable-weapons', 'data')],
    [Input('my-date-picker-range', 'start_date'), Input('my-date-picker-range', 'end_date')])
def update_graph(begin_dt, end_dt):
    begin_date = datetime.strptime(begin_dt, '%Y-%m-%d')
    end_date = datetime.strptime(end_dt, '%Y-%m-%d')
    days = (end_date - begin_date).days

    rawDfSlice = rawDf[0:days]
    dfSlice = rawDfSlice.to_dict("rows")

    time.sleep(5)

    return (dfSlice,)

if __name__ == "__main__":
    app.run_server(port=8053)

I think one problem is that the callback uses Output('loading-output-1', 'children') and the snippets contain loading-1 and loading-output-2

1 Like

@Marc-Andre thank you for your reply!

This discrepancy with ‘loading-output-1’ was a copy / pasting mistake on my part.

After testing your code, I figured out that the problem was not with my code, but with my dash version. I previously had dash==0.39.0.

After updating to dash==0.40.0, I noticed that your code AND my code worked properly.

Thank you very much for your time and effort.

1 Like

I do have one additional question though.

If the Button is NOT clicked, I would want to default to using the start_date and end_date specified in dcc.DatePickerRange. However, from your example, it appears that there needs to be a return statement specifying dates. And if I don’t include the “n_clicks is not None” part of the if-statement, then it gives me an error about comparing type None vs. Int.

@app.callback(
    [Output('my-date-picker-range', 'start_date'), Output('my-date-picker-range', 'end_date')],
    [Input('button-update', 'n_clicks')])
def update_output(n_clicks):
    if n_clicks is not None and n_clicks > 0:
        start_date = date(2019, 2, 12)
        end_date = date.today()
        return start_date, end_date

    return date(2019, 2, 23), date(2019, 2, 27)

Is there a way to default to using the start_date and end_date from the dcc.DatePickerRange?

You can do away with the initialization return by doing

if n_clicks is None:
    raise dash.exceptions.PreventDefault

This will prevent Dash from attempting to update the values.

You’ll run into the same issue in update_graph… in that case you will have to use your default values when being_dt and end_dt are None during initialization.

I also found this post, similar to your response: https://community.plotly.com/t/improving-handling-of-aborted-callbacks/7536

Here is the resulting snippet of code:

@app.callback(
    [Output('my-date-picker-range', 'start_date'), Output('my-date-picker-range', 'end_date')],
    [Input('button-update-5', 'n_clicks')])
def update_output(n_clicks):
    if n_clicks is None:
        raise PreventUpdate
    if n_clicks > 0:
        start_date = date(2019, 2, 12)
        end_date = date.today()
        return start_date, end_date

Does this do the same thing as what you mentioned? It appears to be working properly.

My intended functionality is that the data tables will load with the default DataPickerRange dates when the URL is visited, and will only reload with the dates from the Button when it is clicked. It appears to be fine.

Yes. The case discussed there is more general, but it’s the same idea.

1 Like

Thank you for all of your help