dash icon indicating copy to clipboard operation
dash copied to clipboard

New features for dcc.Loading

Open AnnMarieW opened this issue 1 year ago • 2 comments

This PR adds features to dcc.Loading

  • adds delay_show and delay_hide props to prevent flickering when the loading spinner shows only for a short time
  • adds overlay_style prop so you can do things like make children visible during loading and add opacity.
  • adds target_components prop to specify which component/props can trigger the loading spinner
  • adds custom_spinner prop so you can provide your own spinner rather than using the built-in options
  • refactored component to functional instead of a class

Closes: https://github.com/plotly/dash/issues/951 #2147 will be fixed by using delay_show #1922 will be fixed by using target_components #1802 will be fixed by using delay_show #2422 will be fixed by using delay_show #879 will be possible by using custom_spinner and overlay_style #736. can't replicate the issue

Closes from the old dcc repo https://github.com/plotly/dash-core-components/issues/873 - possible with custom_spinner and delay_show https://github.com/plotly/dash/issues/1541 - can't replicate the issue

Contributor Checklist

  • [ ] To Do / Questions

    • [x] Controlling the visibility of the component being loaded with the opacity and background color of the spinner would be a breaking change. Probably need to find a better way to make it possible to add opacity to the component . Update: added overlay_style prop.

    • [ ] Add ability to manually trigger loading as requested in #2696

    • [ ] Remove the show_initially prop? This was from the dbc library but it seems to have no effect. The spinner will briefly flash on page load, even when show_initially=False

    • [ ] Would setting a delay_hide time solve the issue where there is a lag between the callback finishing and a figure rendering with large data sets? Or might that time be too variable? If this is a solution, I'd need to update the delay_hide timer because it currently sets a minimum time for the timer to display rather than extending the display time. See https://github.com/plotly/dash/issues/2690

  • [ ] I have run the tests locally and they passed.

  • [ ] I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR

  • [ ] I have added entry in the CHANGELOG.md

  • [ ] If this PR needs a follow-up in dash docs, community thread, I have mentioned the relevant URLS as follows

    • [ ] this GitHub #PR number updates the dash docs
    • [ ] here is the show and tell thread in Plotly Dash community

Example 1 delay_hide and delay_show prop

This callback runs for 500ms. This example shows

  • How to add a delay between when the loading starts and when the spinner is displayed. In this example, the callback takes 500ms, so we set the delay_show=600 so the spinner is not displayed. This prevents annoying flashes for callbacks with very fast loading times such as figure updating with hoverData .
  • How to set a minimum time for the spinner to be displayed once loading starts - in this case 2000ms

A good demo for updating with hoverData is to use this example in the docs Try it with adding dcc.Loading([<example app layout> ], delay_show=500)

dcc_loading1


import time

import dash
from dash import Dash, Input, Output, State, html, dcc

app=Dash()
app.layout = html.Div(
    [
        html.Button("Load", id="loading-button", n_clicks=0),
        html.Div("delay_show (ms)"),
        dcc.Input(type="number", value=0, id="delay-show", debounce=True),
        html.Div("delay_hide (ms)"),
        dcc.Input(type="number", value=0, id="delay-hide", debounce=True),
        html.Hr(),
        dcc.Loading(html.Div(id="loading-output"), id="loading"),
    ]
)


@app.callback(
    Output("loading-output", "children"),
    Input("loading-button", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(.5)
        return f"Output loaded {n} times"
    return "Output not reloaded yet"

@app.callback(
    Output("loading", "delay_show"),
    Output("loading", "delay_hide"),
    Input("delay-show", "value"),
    Input("delay-hide", "value")
)
def update_delay_show_hide(show, hide):
    if show is None or hide is None:
        return dash.no_update
    return int(show), int(hide)

app.run(debug=True)

Example 2 target_components prop

Demo of how to overrides default Loading behavior if target_components is set. By default, Loading fires when any child element enters loading state. This makes loading opt-in: Loading animation only enabled when one of target components enters loading state. dcc_loading2

import time

import dash
from dash import Dash, Input, Output, State, html, dcc

app=Dash()
app.layout = html.Div(
    [
        html.Button("Load div 1", id="loading-button1", n_clicks=0),
        html.Button("Load div 2", id="loading-button2", n_clicks=0),
        html.Hr(),
        dcc.Loading([
            html.Div(id="loading-output1"),
            html.Div(id="loading-output2"),
        ], target_components=[{"loading-output1": "children"}]),
    ]
)


@app.callback(
    Output("loading-output1", "children"),
    Input("loading-button1", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(2)
        return f"Output loaded {n} times.  This callback triggers the loading spinner"
    return "Callback 1 output not reloaded yet"



@app.callback(
    Output("loading-output2", "children"),
    Input("loading-button2", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(.5)
        return f"Output loaded {n} times.  No  loading spinner"
    return "Callback 2 output not reloaded yet"

app.run(debug=True)

Example 3 Styling with overlay_style prop

Default: content is hidden while loading

dcc_loading3b

Styled with overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"}

This keeps the content visible while loading and adds opacity

dcc_loading3a

import time

import dash
from dash import Dash, Input, Output, State, html, dcc
import plotly.express as px
data_canada = px.data.gapminder().query("country == 'Canada'")
app=Dash()
app.layout = html.Div(
    [
        html.Button("Start", id="loading-button", n_clicks=0),
        html.Hr(),
        dcc.Loading(
            [dcc.Graph(id="loading-output", figure=px.line(data_canada, x="year", y="pop"))],
            overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"},
            color="red"
        ),
    ]
)


@app.callback(
    Output("loading-output", "figure"),
    Input("loading-button", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(1)
        return px.bar(data_canada, x="year", y="pop")
    return dash.no_update


app.run(debug=True)

Example 4 custom_spinner prop

Instead of using one of the provided spinner component, you can provide your own.

A custom_spinner can be used to remove/hide the spinner. This is nice when using nested dcc.Loading components. It's possible to create a component with just text or other Dash components and no spinner.

This example uses:

custom_spinner=html.H2(["My Custom Spinner", dbc.Spinner(color="danger")])

dcc_loading4

import time

import dash
from dash import Dash, Input, Output, State, html, dcc
import plotly.express as px
import dash_bootstrap_components as dbc

data_canada = px.data.gapminder().query("country == 'Canada'")

app=Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = dbc.Container(
    [
        dbc.Button("Start", id="loading-button", n_clicks=0),
        html.Hr(),
        dcc.Loading(
            [dcc.Graph(id="loading-output", figure=px.line(data_canada, x="year", y="pop"))],
            overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"},
            custom_spinner=html.H2(["My Custom Spinner", dbc.Spinner(color="danger")])
        ),
    ]
)


@app.callback(
    Output("loading-output", "figure"),
    Input("loading-button", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(1)
        return px.bar(data_canada, x="year", y="pop")
    return dash.no_update


app.run(debug=True)




AnnMarieW avatar Feb 15 '24 17:02 AnnMarieW

@AnnMarieW Could you give an example of when target_component is used?

delay_show and delay_hide seem reasonable and the implementation makes sense to me.

JamesKunstle avatar Feb 15 '24 18:02 JamesKunstle

@AnnMarieW This is really exciting, the loading opacity update is really clever, and the demonstrations are really helpful to understand the scope of these updates.

JamesKunstle avatar Feb 16 '24 15:02 JamesKunstle

@AnnMarieW re: 'mode' prop-

What you implemented matches the Loading behavior that I'd like to have.

I'd consider changing the name, however. 'mode' seems like a execution context rather than 'on or off.' I'd perhaps use 'mode' to be 'auto' or 'manual' and 'manual' would disable targeted loading state and consideration of the loading state of children, and instead would rely on another prop like 'setLoading' or something.

This would clarify that the Loading component is either driven by state that can be inferred from the renderer (those components whose state are waiting for backend information) or directly specified by the user (indirectly related components, like a search option that should be put into a loading state while something else is executing).

JamesKunstle avatar Feb 19 '24 19:02 JamesKunstle

@JamesKunstle I'm not sure what you mean re the mode prop. Can you clarify or supply some code?

AnnMarieW avatar Feb 19 '24 19:02 AnnMarieW

@AnnMarieW Yes certainly:

So mode prop could just be manual-mode and be a boolean, True or False.

When not in manual-mode, the Loading component behaves exactly as it does now, changing as an effect of other components waiting on a response from the backend.

In manual mode, the Loading component would honor the additional prop set-loading (naming not final), a boolean that would be under full manual control from the user.

        if (manual_mode) {
            setShowSpinner(set_loading);
            return;
        }

Here's a concrete example:

Imagine that I have a dcc.Interval component that fires every 5 seconds, making a request to the backend to check whether an asynchronous resource is available. Another component, like a Dropdown, indirectly relies on the availability of that resource, so it should be in a forced Loading state that stays enabled even when the dcc.Interval is waiting to fire again.

If I could use manual mode, a final successful return from the callback that handles the Interval logic could disable loading for the Dropdown.

This way, Loading becomes a developer-controllable state that is managed by application logic rather than by the dash renderer.

JamesKunstle avatar Feb 19 '24 22:02 JamesKunstle

re: mode - agreed that the name is a little ambiguous, but I do like having a single prop with three values, rather than a second prop that only applies to one of the values of the other.

What about borrowing from CSS: display: "show" | "hide" | "auto"?

alexcjohnson avatar Feb 26 '24 23:02 alexcjohnson

Re: api for enabling/disabling, what feels most intuitive to me is disabled for consistency with dcc.Interval and the input components, and also it's a more transparent name -- doesn't lend itself super easily to 3 values though.

emilykl avatar Feb 27 '24 16:02 emilykl

Re: api for enabling/disabling, what feels most intuitive to me is disabled for consistency with dcc.Interval and the input components, and also it's a more transparent name -- doesn't lend itself super easily to 3 values though.

That, for me, feels ergonomically less nice for the same reason that it does for dcc.Interval - negating 'disabled' in my head to set the correct boolean value takes a few more cycles than I want, and I normally create global variables to solve it, like "INTERVAL_DISABLED" = True, "INTERVAL_ENABLED" = False, etc.

re: mode - agreed that the name is a little ambiguous, but I do like having a single prop with three values, rather than a second prop that only applies to one of the values of the other.

What about borrowing from CSS: display: "show" | "hide" | "auto"?

Passing a string parameter for 'display' seems reasonable

JamesKunstle avatar Feb 28 '24 15:02 JamesKunstle

Ok great, let’s go with display: "show" | "hide" | "auto"

alexcjohnson avatar Feb 29 '24 03:02 alexcjohnson

Ok great, let’s go with display: "show" | "hide" | "auto"

Done

AnnMarieW avatar Feb 29 '24 13:02 AnnMarieW

I can work on the docs PR next. Note that I can't merge PRs here, so can you do that for me?

AnnMarieW avatar Mar 04 '24 16:03 AnnMarieW

Super excited for these!

emilykl avatar Mar 04 '24 16:03 emilykl

I can work on the docs PR next. Note that I can't merge PRs here, so can you do that for me?

Can we hold on merging while I work on a fix for #2775

T4rk1n avatar Mar 04 '24 16:03 T4rk1n

Here's the PR for the docs https://github.com/plotly/ddk-dash-docs/pull/2570

AnnMarieW avatar Mar 21 '24 18:03 AnnMarieW

Hello AnnMarieW,

thank you very much for implementing the features delay_show and delay_hide to dcc.Loading. Do I have to update dash to have access to it?

Thank You very much in Advance

TypeError: The dcc.Loading component (version 2.14.1) received an unexpected keyword argument: delay_show Allowed arguments: children, className, color, debug, fullscreen, id, loading_state, parent_className, parent_style, style, type

dash==2.14.1 dash-bootstrap-components==1.5.0 dash-core-components==2.0.0

After updating dash: TypeError: The dcc.Loading component (version 2.16.1) received an unexpected keyword argument: delay_show Allowed arguments: children, className, color, debug, fullscreen, id, loading_state, parent_className, parent_style, style, type

schultz77 avatar Apr 08 '24 07:04 schultz77

Hi @schultz77

Thanks! I’m looking forward to it too 🙂. This will be available in the next release (Dash 2.17)

AnnMarieW avatar Apr 08 '24 12:04 AnnMarieW

Ok! Thank You for your quick reply. In the meantime I'll keep using dbc.Spinner...

schultz77 avatar Apr 08 '24 13:04 schultz77