This PR adds features to `dcc.Loading`
- adds `delay_show` and `delay_hide` pro…ps 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:
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
https://github.com/plotly/dash-core-components/issues/709 - possible with `target_components`
## 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.
- [x] Add ability to manually trigger loading as requested in #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 https://github.com/plotly/dash/issues/2690
- [x] I have run the tests locally and they passed.
- [x] I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR
- [x] 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
--------------------------------
<br />
## 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](https://dash.plotly.com/interactive-graphing#update-graphs-on-hover) Try wrapping the app layout with `dcc.Loading([<example app layout> ], delay_show=500)`
![dcc_loading1](https://github.com/plotly/dash/assets/72614349/7fe82e46-3ba0-4d46-872a-40926c9bdbeb)
```python
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)
```
--------------------------------
<br />
## 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](https://github.com/plotly/dash/assets/72614349/ca1c1dfa-b1d4-4a8b-a7b7-abcc2500af85)
```python
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)
```
--------------------------------
<br />
## Example 3 Styling with `overlay_style` prop
### Default: content is hidden while loading
![dcc_loading3b](https://github.com/plotly/dash/assets/72614349/3b63cab0-623c-4916-a621-268e98512335)
### Styled with `overlay_style={"visibility":"visible", "opacity": .5, "backgroundColor": "white"}`
This keeps the content visible while loading and adds opacity
![dcc_loading3a](https://github.com/plotly/dash/assets/72614349/618920f9-b2c1-436e-8faa-9f95d2f78bca)
```python
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)
```
--------------------------------
<br />
## 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:
```python
custom_spinner=html.H2(["My Custom Spinner", dbc.Spinner(color="danger")])
```
![dcc_loading4](https://github.com/plotly/dash/assets/72614349/d4c7cd66-1768-44f9-9a07-23ebcbf96ee1)
```python
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)
```
--------------------------------
<br />
## 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](https://github.com/plotly/dash/assets/72614349/edc7a247-55c8-47a1-b39c-a0160d05860f)
```python
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)
```