6

In my Dash-based application, a button triggers a long-running computation. Wouldn't it be nice to display a loading animation while the result is not yet there, and make the button inactive so it is not clicked again before the computation finishes?

I am using Bulma for UI design and wanted to use the button is-loading CSS class for that purpose.

enter image description here

My first idea was to have two callbacks: One triggered by the button click to set the button to is-loading, and one triggered by a change in the output to set it back to normal.

@app.callback(
    Output('enter-button', 'className'),
    [
        Input('graph', 'figure')
    ],
)
def set_trend_enter_button_loading(figure_changed):
    return "button is-large is-primary is-outlined"


@app.callback(
    Output('enter-button', 'className'),
    [
        Input('enter-button', 'n_clicks')
    ],
)
def set_trend_enter_button_loading(n_clicks):
    return "button is-large is-primary is-outlined is-loading"

Apparently it doesn't work that way:

dash.exceptions.CantHaveMultipleOutputs:
You have already assigned a callback to the output
with ID "enter-button" and property "className". An output can only have
a single callback function. Try combining your inputs and
callback functions together into one function.

Any ideas how to make this work?

clstaudt
  • 21,436
  • 45
  • 156
  • 239

4 Answers4

11

I had the same problem last week, and even tried to achieve the disabled button behavior using Javascript, but eventually gave up. I've seen seen this discussed on the plotly forums, and there is clearly a need for this type of functionality, but I don't think it can be achieved easily in the current version.

One thing that is possible though, and is mentioned as a temporary solution by the Dash developer, is adding a global loading screen. In short, you need to add the following CSS to your stylesheet:

@keyframes fadein {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 0.5;
    }
}

._dash-loading-callback {
  font-family: sans-serif;
  padding-top: 50px;
  color: rgb(90, 90, 90);
  
  /* The banner */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  text-align: center;
  cursor: progress;

  opacity: 0;
  background-color: rgb(250, 250, 250);
  /* Delay animation by 1s to prevent flashing 
     and smoothly transition the animation over 0.5s 
   */
  -moz-animation: fadein 0.5s ease-in 1s forwards; /* Firefox */
  -webkit-animation: fadein 0.5s ease-in 1s forwards; /* Safari and Chrome */
  -o-animation: fadein 0.5s ease-in 1s forwards; /* Opera */
    animation: fadein 0.5s ease-in 1s forwards;
}

A few clarifications:

  1. The _dash-loading-callback selector selects a div which is added at the end of the body element each time a callback is made, and is removed when it is finished.
  2. You can add a loading animation by defining a background GIF for _dash-loading-callback.
  3. The animations defined in the above CSS are meant to prevent the loading screen from flickering upon short callbacks. It is set to fade in after one second, so it will only appear for long loading operations. You can of course play with these settings.

Updates 2022

  1. It is mentioned in the comments that the class name has changed from _dash-loading-callback to _dash-loading in newer versions. I didn't get the chance to test this yet.
  2. There seem to be easier and more modern ways of achieving this behavior now, without custom CSS. See for example Dash Loading Spinners.
Shovalt
  • 6,407
  • 2
  • 36
  • 51
  • That seems like an interesting workaround - will try it out. – clstaudt Jan 30 '19 at 14:16
  • It is easy to disable the button on click. Is Dash really not equipped to re-enable it when something in the page content happens? I also don't understand why there can be only one callback for each output. – clstaudt Jan 30 '19 at 14:18
  • 1
    I also don't fully understand the logic, but I'm sure there is a reason. You can set two inputs for a single output though, and the function will get called when either of them changes. Inside the function you can perhaps check which of the two inputs was the trigger (I'm not sure how, discussed here: https://github.com/plotly/dash/issues/291). This will simulate two different callbacks. – Shovalt Jan 30 '19 at 14:28
  • The CSS works great for the purpose, so I'll accept your answer. I have a feeling though that I will need to work out the original problem for Dash to work nicely with CSS frameworks like Bulma. Your comment may contain the answer. – clstaudt Jan 30 '19 at 14:29
  • Adding my penny: as of 2022, I've had to change the selector from `._dash-loading-callback`to `._dash-loading` for this to take effect, – P. Navarro Jul 18 '22 at 15:02
1

Since dash-renderer==0.9.0, a div tag is added to your layout when your app is waiting for a callback. You can put a different css in it as suggested here by Chris: https://community.plot.ly/t/mega-dash-loading-states/5687

Also, a new feature is coming which fit your needs. It is currently in alpha version: https://community.plot.ly/t/loading-states-api-and-a-loading-component-prerelease/16406

1

Loader with custom GIF

Just create a folder named assets in the root of your app directory and include your CSS and JavaScript files in that folder. Dash will automatically serve all of the files that are included in this folder. By default the url to request the assets will be /assets but you can customize this with the assets_url_path argument to dash.Dash.

Create a customer css file under style folder

- app.py
- assets/
    |-- loader.css

custom css

        ._dash-loading-callback {
        position: fixed;
        z-index: 100;
        }

        ._dash-loading-callback::after {
        content: 'Loading';
        font-family: sans-serif;
        padding-top: 50px;
        color: #000;

        -webkit-animation: fadein 0.5s ease-in 1s forwards; /* Safari, Chrome and Opera > 12.1 */
           -moz-animation: fadein 0.5s ease-in 1s forwards; /* Firefox < 16 */
            -ms-animation: fadein 0.5s ease-in 1s forwards; /* Internet Explorer */
             -o-animation: fadein 0.5s ease-in 1s forwards; /* Opera < 12.1 */
                animation: fadein 0.5s ease-in 1s forwards;  
        /* prevent flickering on every callback */
        -webkit-animation-delay: 0.5s;
        animation-delay: 0.5s;

        /* The banner */
        opacity: 0;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(255, 255, 255, 0.5);
        text-align: center;
        cursor: progress;
        z-index: 100000;

        background-image: url(https://www.w3schools.com/html/programming.gif);
        background-position: center center;
        background-repeat: no-repeat;
        }

        @keyframes fadein {
          from { opacity: 0; }
          to   { opacity: 1; }
        }

        /* Firefox < 16 */
        @-moz-keyframes fadein {
          from { opacity: 0; }
          to   { opacity: 1; }
        }

        /* Safari, Chrome and Opera > 12.1 */
        @-webkit-keyframes fadein {
          from { opacity: 0; }
          to   { opacity: 1; }
        }

        /* Internet Explorer */
        @-ms-keyframes fadein {
          from { opacity: 0; }
          to   { opacity: 1; }
        }

        /* Opera < 12.1 */
        @-o-keyframes fadein {
          from { opacity: 0; }
          to   { opacity: 1; }
        }
Anon30
  • 567
  • 1
  • 6
  • 21
1

To add to the answer of Gustave, there is now a Dash-option for loading animations that has come out of alpha and beta. You simply declare a dcc.Loading component and add it to the layout. Then add it as an Output to the callback that is taking a long time and you want the loader to display. Just adding this here, because that's what I went with after some research and testing the other options in this thread.