Show loading sign during calculations

Hello,
I would like to know if there are easy ways to show the user some icons during heavy calculations.

I have some old code which worked in Bokeh 1.0.2 with Python 2.7. There at the start of a callback function the layout was changed to show the loading sign and after the calculations at the end of this same function, the figure with the calculated plot was shown.
However, in Bokeh 1.4.0 with Python 3.7, this does not work any more. It seems like changes to the layout are only set at the end of a function.

Here is a minimal example, to show what I mean:

Click here to see the code

style.html

includes the CSS for the loading sign, taken from https://loading.io/css/

<style>
    .lds-dual-ring {
        display: block;
        width: 50%;
        margin: auto;
        padding: 10px;
    }

    .lds-dual-ring:after {
        content: " ";
        display: block;
        width: 46px;
        height: 46px;
        margin: auto;
        border-radius: 50%;
        border: 5px solid rgb(0, 101, 189);
        border-color: rgb(0, 101, 189) transparent rgb(0, 101, 189) transparent;
        animation: lds-dual-ring 1.2s linear infinite;
    }

    @keyframes lds-dual-ring {
        0% {
            transform: rotate(0deg);
        }

        100% {
            transform: rotate(360deg);
        }
    }
</style>

main.py

The upper button switches between the loading symbol and the figure. This works.
The lower button starts a “heavy calculation” (here the program just sleeps for 5 seconds, but it has the same effect). In the console you can see by the prints, that everything should be set at the correct time. But only when the function finishes, you can see the loading sign for the fraction of a second. Actually, when starting the callback, the loading sign should show and at the end switch back to the figure.

from bokeh.io import curdoc
from bokeh.models import Div, Button, ColumnDataSource
from bokeh.plotting import figure
from bokeh.layouts import column, layout
from os.path import dirname, join
import time


def change_pic():
    if b.label=="change to load screen":
        My_Layout.children[0].children[2] = loading
        b.label = "change to figure"
    else:
        My_Layout.children[0].children[2] = f
        b.label = "change to load screen"

def fake_comp():
    My_Layout.children[0].children[2] = loading
    print("set loading")

    time.sleep(5) # mock calculation
    
    My_Layout.children[0].children[2] = f
    print("set figure")


style_file = join(dirname(__file__), "style.html")
d = Div(text=open(style_file).read(), render_as_text=False)

loading = column(Div(text="<div class=\"lds-dual-ring\"></div>", render_as_text=False, width=650, height=100))

f = figure(x_range=(0,5), y_range=(0,5))


b = Button(label="change to load screen")
b.on_click(change_pic)

c_b = Button(label="expensive comp")
c_b.on_click(fake_comp)


My_Layout = layout([column(d, b, f, c_b)])

curdoc().add_root(My_Layout)

Three ideas that I’ve already tired:

  • Using the visible attribute and setting it to False/True instead of manipulating the layout directly.
  • I have also tried the same with just a Label and changing text, but this yields the same result.
  • Another idea was to add a mock slider and change the layout in a different function if its value has changed, but this workaround doesn’t work either…

Regarding built-in spinners, there was already some discussion on Github:
#3393, #8823

I’m not sure the behavior here has ever been defined clearly. It probably should be, and then maintained under test, some day. But as you mention, currently Bokeh state only synchronizes with the browser when the callback ends . If you want to do some update, then alot of blocking work, then another update, you will need to split things up so the first callback completes immediately then schedules the rest of the work to happen after the return. The simplest way is with add_next_tick_callback :

from time import sleep
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button, Div

d = Div(text="start")

b = Button()

def work():
    sleep(2)
    d.text = "end"

def cb():
    d.text = "middle"
    curdoc().add_next_tick_callback(work)

b.on_click(cb)

curdoc().add_root(column(d, b))
1 Like

Thank you, this solves my problem and works very well :slight_smile:

1 Like