Small example on a loading Div?

Hi,

I was wondering if anyone would mind sharing a small example of adding a loading spinner/animation when a button is being clicked. I have created a dashboard and the user clicks a button to load the data, and it takes a few seconds. I would just want a loading spinner in the middle. What is the best way to do this? If anyone has a small example I can check out to help me do something similar, I would really appreciate it:)

Hi @Zana

I do most of my development using bokeh models and layouts. On occasion, I find it useful to pull in Holoviz panel for some convenience behaviors related to dynamic layouts or additional widgets.

Holoviz panel includes a loading spinner widget. See LoadingSpinner — Panel 0.12.6 documentation.

Holoviz panel plays very nicely with bokeh and its server is the bokeh server, so working with the two is typically straightforward. In my experience, I have never had to rewrite my original bokeh code – excepting a few lines of superficial changes – to leverage the power of both tools together.

1 Like

Hi,

Thanks for the reply. I might try Panel instead. I have a small example below if anyone wants to try and see if they can get it working. Not sure why the spinner is not spinning when I increase time.sleep seconds. If you try you see it disappears as soon as it starts spinning.

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

spinner_text = """
<!-- https://www.w3schools.com/howto/howto_css_loader.asp -->
<div class="loader">
<style scoped>
.loader {
    border: 16px solid #f3f3f3; /* Light grey */
    border-top: 16px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
} 
</style>
</div>
"""

loadData = Button(width = 200, label="Load Data", button_type="success")
div_spinner = Div(text="",width=120,height=120)
#div_spinner = Div(text=spinner_text,width=120,height=120)

layout = column(div_spinner, loadData)

def dataLoader():
    div_spinner.text = spinner_text
    print('Loading data..')
    time.sleep(2)
    hideSpinner()

def hideSpinner():
    print('Done')
    div_spinner.text = ""
        
loadData.on_click(dataLoader)
#curdoc().on_event(DocumentReady, hideSpinner)

curdoc().add_root(layout)
1 Like

I played with your example and (as usual these days it seems) I have not been able to solve the problem, but I’ve been able kinda isolate the problem further. I broke up your functions into even more granular components, changed the function to alter the Div’s visibility instead of the text property, and added a CustomJS to a) see if it’s a classic case of explicitly triggering change.emit(), and/or b) see when the changes to the visibility property are “felt” on the JS side:

from bokeh.models import Div, Button, CustomJS
from bokeh.events import DocumentReady
from bokeh.layouts import column
from bokeh.io import curdoc
from bokeh.document import Document
import time

spinner_text = """
<!-- https://www.w3schools.com/howto/howto_css_loader.asp -->
<div class="loader">
<style scoped>
.loader {
    border: 16px solid #f3f3f3; /* Light grey */
    border-top: 16px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
} 
</style>
</div>
"""

loadData = Button(width = 200, label="Load Data", button_type="success")
# div_spinner = Div(text="",width=120,height=120)
div_spinner = Div(text=spinner_text,width=120,height=120,visible=False)

layout = column(div_spinner, loadData)

cb = CustomJS(args=dict(div_spinner=div_spinner)
              ,code='''
              console.log('cb triggered!')
              div_spinner.change.emit()''')
div_spinner.js_on_change('visible',cb)
def dataLoader():
    print('loading')
    # div_spinner.text = spinner_text
    div_spinner.visible = True

def doingStuff():
    print('doing stuff')
    for i in range(10000):
        print(i)

def hideSpinner():
    print('Done')
    # div_spinner.text = ""
    div_spinner.visible=False
        
loadData.on_click(dataLoader)
loadData.on_click(doingStuff)
loadData.on_click(hideSpinner)
# curdoc().on_event('document_ready', hideSpinner)

curdoc().add_root(layout)

loader

It seems to me that do_stuff is being executed first, then both the load and hide spinner functions actually happen, hence the loader “blinking” right as the counter finishes.

I don’t know why the order of operations is occurring like this → I feel like you’re tantalizingly close.

EDIT: Ha, the frame rate on my gif only managed to capture only one “blink” of the loader (i click three times in the gif), but trust me, it blinks every time you click the button.

Hi, thanks for looking into it. I was working with examples posted here on the discourse for older versions of bokeh. The new version (I am using 2.4.1) the events triggered in callbacks happens at the end of the callback, so the function makes the spinner blink in and out of view at the end of the callback. Same that is happening for you. Bryan posted a solution to the question I had a year ago: Can't get spinning loader to work. What am I doing wrong?

Maybe its best to animate the spinner first (when user clicks button) and then have the callback stop the animator after it has loaded the data when the document is idle?

I will try some more with the code you provided:D

Ok, managed to solve it! Used the add_next_tick_callback inside the dataloader function:D

# -- coding: utf-8 --
"""
Created on Thu Jan 21 11:55:35 2021

@author: zana.kajolli
"""
from bokeh.models import Div, Button, CustomJS
from bokeh.events import DocumentReady
from bokeh.layouts import column
from bokeh.io import curdoc
from bokeh.document import Document
import time

spinner_text = """
<!-- https://www.w3schools.com/howto/howto_css_loader.asp -->
<div class="loader">
<style scoped>
.loader {
    border: 16px solid #f3f3f3; /* Light grey */
    border-top: 16px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
} 
</style>
</div>
"""

doc = curdoc()


loadData = Button(width = 200, label="Load Data", button_type="success")
# div_spinner = Div(text="",width=120,height=120)
div_spinner = Div(text=spinner_text,width=120,height=120,visible=False)

layout = column(div_spinner, loadData)

cb = CustomJS(args=dict(div_spinner=div_spinner)
              ,code='''
              console.log('cb triggered!')
              div_spinner.change.emit()''')
              
div_spinner.js_on_change('visible',cb)

def dataLoader():
    print('loading')
    # div_spinner.text = spinner_text
    div_spinner.visible = True
    doc.add_next_tick_callback(doingStuff)

def doingStuff():
    print('doing stuff')
    time.sleep(5)
    print('Done')
    div_spinner.visible=False
    
#def hideSpinner():
#    print('Done')
#    # div_spinner.text = ""
#    div_spinner.visible=False
        
loadData.on_click(dataLoader)
#loadData.on_click(doingStuff)
#loadData.on_click(hideSpinner)
# curdoc().on_event('document_ready', hideSpinner)

doc.add_root(layout)
3 Likes

Sweet, this is super useful for me and I’m sure others :smiley: .

This actually looks ripe for application via a decorator function.

Now time for me to try and implement a standalone JS-side version of this… I’ll report back if successful…

1 Like

Here is the bit to make it work as a decorator. partial is from functools import partial

div_spinner.js_on_change('visible',cb)

 def show_spinner(status):
     div_spinner.visible = status
 
 def busy(func):
     def wrapper():
         doc.add_next_tick_callback(partial(show_spinner,True))
         doc.add_next_tick_callback(func)
         doc.add_next_tick_callback(partial(show_spinner,False))
 
     return wrapper
 
 @busy
 def doingStuff():
     time.sleep(5)
 
 loadData.on_click(doingStuff)
 
 doc.add_root(layout)

And below is to also disable the button while the stuff is getting done:

div_spinner.js_on_change('visible',cb)

def show_hide_spinner():
    div_spinner.visible = not div_spinner.visible

def busy(button=None):
    def busy_decorator(func):
        def wrapper():
            doc.add_next_tick_callback(show_hide_spinner)
            if button is not None:
                doc.add_next_tick_callback(partial(enable_disable_button,button))
            doc.add_next_tick_callback(func)
            doc.add_next_tick_callback(show_hide_spinner)
            if button is not None:
                doc.add_next_tick_callback(partial(enable_disable_button,button))
        return wrapper
    return busy_decorator

def enable_disable_button(button):
    button.disabled = not button.disabled

@busy(loadData)
def doingStuff():
    time.sleep(5)
    
loadData.on_click(doingStuff)

doc.add_root(layout)
2 Likes

Awesome Sebastien, thanks for the help on this problem. This is very useful as I would want to use the spinner in different tabs and plots in the dashboard. I can just pull this function when I need it to show a loading spinner.