Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New features for dcc.Loading #2760

Merged
merged 15 commits into from
Mar 8, 2024
Merged

Conversation

AnnMarieW
Copy link
Collaborator

@AnnMarieW AnnMarieW commented Feb 15, 2024

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. (same as in the Dash Bootstrap Components dbc.Spinner)
  • 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
  • adds display prop to override the loading status. Can set to "show", "hide", or "auto"
  • refactored component to functional instead of a class

Closes:
#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
plotly/dash-core-components#873 - possible with custom_spinner and delay_show
#1541 - can't replicate the issue
plotly/dash-core-components#709 - possible with target_components

Contributor Checklist

  • To Do / Questions

    • 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 [Feature Request] Make dcc.Loading toggle-able attribute for enabled/disabled #2696 . It's currently called "mode" with options "on", "off", "auto".

    • 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 provide loading Indicator during plotly rendering #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

The delay_hide and delay_show props can be used to prevent flickering when the loading spinner shows only for a short time.

This callback runs for 500ms. This example shows how to:

  • prevent the spinner from showing by setting delay_show=700.
  • make the spinner show for a minimum amount of time by setting delay_hide=2000

A good use-case for delay_show is when hoverData is used in a callback. A good demo is this cross filter example in the docs Try wrapping the app layout with 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

Use the target_components prop to specify which component/props can trigger the loading spinner.
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/props enters loading state.

target_components is a dict where the key is the component id, and the value is the prop name | list of prop names | "*"

examples - the following will trigger the loading spinner:

Any prop in the "grid" component

target_components ={"grid": "*"}

Either the grid's rowData or columnDefs

target_components ={"grid": ["rowData", "columnDefs"]}

Only the grid's rowData

target_components ={"grid": "rowData"}

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 built-in spinner component, you can provide your own.

A custom_spinner can be used to remove/hide the spinner. This is a convenient way to remove the spinner when using nested dcc.Loading components.
It's possible to create a component with custom loading messages using any Dash components.

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)


Example 5 display prop

Use the display prop to manually display or hide the loading spinner. Set to "show", "hide" or "auto"(the default)

dcc_loading5

import time

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


app=Dash()
app.layout = html.Div(
    [
        html.Div("Select mode"),
        html.Button("Start", id="loading-button", n_clicks=0),
        dcc.Dropdown(["auto", "show", "hide"], value="auto", id="display", style={"marginBottom": 100}),
        html.Hr(),
        dcc.Loading(
            html.Div("Demo of manually controlling  loading status", id="loading-output"),
            id="loading"


        ),
    ]
)

@app.callback(
    Output("loading", "display"),
    Input("display", "value")
)
def update_display(display):
    return display


@app.callback(
    Output("loading-output", "children"),
    Input("loading-button", "n_clicks"),
)
def load_output(n):
    if n:
        time.sleep(5)
        return f"Updated content {n}"
    return dash.no_update


app.run(debug=True)

@AnnMarieW AnnMarieW marked this pull request as draft February 15, 2024 17:20
@JamesKunstle
Copy link

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

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

@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).

@AnnMarieW
Copy link
Collaborator Author

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

@JamesKunstle
Copy link

JamesKunstle commented Feb 19, 2024

@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.

Comment on lines +52 to +55
const hiddenContainer = mergeRight(
{visibility: 'hidden', position: 'relative'},
overlay_style
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overlay_style to set dcc.Loading to be semi-translucent feels like a really common use case. I wonder if it's worth adding a prop like opacity to specify explicitly with some default styles applied?

This way developers don't need to remember the more complex e.g. overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"} if they just want to make the overlay translucent?

Copy link
Collaborator Author

@AnnMarieW AnnMarieW Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but I expect most people would tweak the opacity and background color depending on the theme (light/dark). Or do know some CSS that would work in most cases?

Copy link
Member

@ndrezn ndrezn Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably pick a neutral grey? I've found #7F8487 is a good middle ground grey that works well in light and dark.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try that - but still want to give the ability for people to use any CSS in overlay_style. Are you suggesting another prop, perhaps that sets {"visibility":"visible", "opacity": .5, "backgroundColor": "#7F8487"}?
Then what if people set both props?

If there are a couple examples in the docs, it might work OK to just leave it as-is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe let's leave as-is for now with good examples in the docs, and see if there is community feedback from there?

* example: `[{"output-container": "children"}]`
*
*/
target_components: PropTypes.arrayOf(PropTypes.object),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this would be better as

objectOf(arrayOf(string))

or perhaps

objectOf(oneOfType([string, arrayOf(string)]))

so you could do something like:

target_components={"my_table": ["data", "columns"], "my_dropdown": "options"}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this, plus it's possible to use a wildcard. This will trigger loading for any prop in the "my_table" component

target_components={"my_table": "*", "my_dropdown": "options"}

@alexcjohnson
Copy link
Collaborator

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"?

@emilykl
Copy link
Contributor

emilykl commented Feb 27, 2024

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.

@JamesKunstle
Copy link

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

@alexcjohnson
Copy link
Collaborator

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

@AnnMarieW
Copy link
Collaborator Author

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

Done

Copy link
Member

@ndrezn ndrezn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃 Super excited about these changes. I think it'd be nice to have a dedicated opacity prop but I understand concerns about color... so I don't feel super strongly about this, as long as it's well-documented.

On that note... is there a corresponding docs PR?

@AnnMarieW
Copy link
Collaborator Author

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

@emilykl
Copy link
Contributor

emilykl commented Mar 4, 2024

Super excited for these!

@AnnMarieW AnnMarieW requested a review from T4rk1n as a code owner March 4, 2024 16:30
@T4rk1n
Copy link
Contributor

T4rk1n commented Mar 4, 2024

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

Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃

@AnnMarieW
Copy link
Collaborator Author

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

@schultz77
Copy link

schultz77 commented Apr 8, 2024

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

@AnnMarieW
Copy link
Collaborator Author

Hi @schultz77

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

@schultz77
Copy link

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

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

Successfully merging this pull request may close these issues.

7 participants