Loading indicator for pn.pane.Bokeh

I’m building an application with various Parameterized classes that each typically plot a piece of data and have some controls. One of my classes is creating a plot using bokeh directly. The data is calculated using various algorithms, each with some parameters. The calculation takes at least a few seconds. I wanted to provide feedback to the user that something new is being calculated or loaded. After exploring various options, I have something that works, but I feel like it’s pretty messy and I’m probably not using panel the way it’s intended.

Here’s a toy example that roughly shows what I’m doing:

import time
import panel as pn
import panel.io.loading
import param as pm
import pandas as pd
import numpy as np
import threading
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.server.server import Server


class App(pm.Parameterized):
    y = pm.Integer(label="Y", default=0)

    def __init__(self):
        super().__init__()
        self.source = ColumnDataSource(pd.DataFrame(dict(x=range(10), y=[0]*10)))
        self.bkfigure = self._create_plot()
        self.pane = pn.pane.Bokeh(self.bkfigure)

    def _create_plot(self):
        fig = figure()
        fig.circle(source=self.source, x='x', y='y', size=10)
        return fig

    @pm.depends('y', watch=True)
    def _update_data(self):
        def work():
            def updater():
                self.source.data['y'] = new_y
                self.pane.loading = False

            # long calculation
            time.sleep(2)
            new_y = np.ones(10) * self.y        # result of long calculation

            # pn.state.curdoc is None...
            self.bkfigure.document.add_next_tick_callback(updater)

        self.pane.loading = True
        threading.Thread(target=work).start()


def bkapp(doc):
    app = App()
    panel = pn.Row(app.param, app.pane)
    doc.add_root(panel.get_root())


if __name__ == '__main__':
    port = 8902
    server = Server({'/': bkapp}, num_procs=1, port=port)
    server.start()
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

If I don’t use the thread and add_next_tick_callback, nothing happens, I’m guessing because of the way the callbacks are scheduled it ends up starting and stopping the loading spinner after the calculation is done.

I tried using @pm.depends(pn.state.param.busy), which also didn’t work for the same reason. I can print something and that works just fine, but updating the GUI doesn’t happen until after the calculation.

Before I discovered the loading spinners, I was simply changing the background color of my bokeh figure, but the same problem there.

My main question is really about recommended usage and patterns.

  1. Is there a cleaner way to do this? Would be nice to separate the calculation from the mechanics of showing a busy indicator. If I don’t need a busy indicator, I don’t even need to use a thread for the calculation. I can do the calculation and update the ColumnDataSource right in the callback.
  2. I’m pretty confused about which document I need to call add_next_tick_callback on. In this case I obviously used the document that underlies my bokeh plot, but I wasn’t expecting pn.state.curdoc to be None. The documentation here added to my confusion because there’s a line doc = pn.state.curdoc, but doc doesn’t get used subsequently.

Any thoughts/suggestions would be much appreciated.

EDIT:
In this example (Loading Spinners), they are achieving the same thing without the thread and the spinner displays fine. I can’t figure out what the difference is.

I played around more with the example from awesome-panel (Loading Spinners). The difference seems to be coming from the way the application is launched. When I don’t use the bokeh server directly, but instead use servable() as is done in the example, the thread isn’t necessary.

That brings me to a different question: the reason I’m using the bokeh server is because that allows me to start my code from a debugger (I use PyCharm). Is there any way to get the best of both worlds?

The awesome-panel examples have been great to better understand how to structure my code. My updated example would be something like this (where I could factor out the self.view.loading = True/False part further):

import time
import panel as pn
import param as pm
import pandas as pd
import numpy as np
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
pn.extension(loading_spinner='arc')


class App(pm.Parameterized):
    y = pm.Integer(label="Y", default=0)

    def __init__(self):
        super().__init__()
        self.source = ColumnDataSource(pd.DataFrame(dict(x=range(10), y=[0]*10)))
        self.bkfigure = self._create_plot()
        self.controls = pn.Column(self.param)
        self.plot = pn.pane.Bokeh(self.bkfigure)
        self.view = pn.Row(self.controls, self.plot)

    def _create_plot(self):
        fig = figure()
        fig.circle(source=self.source, x='x', y='y', size=10)
        return fig

    @pm.depends('y', watch=True)
    def _update_data(self):
        self.view.loading = True
        # long calculation
        time.sleep(2)
        new_y = np.ones(10) * self.y  # result of long calculation
        self.source.data['y'] = new_y
        self.view.loading = False


def view():
    app = App()
    return app.view


if __name__.startswith("bokeh"):
    view().servable()

This does what I want (except for not running in the debugger), but I think there may be a bug: when I update the y spinner value by clicking on one of the arrows, the value keeps changing as long as I hover my cursor over the arrow. Seems like the button release isn’t registered because the component is disabled while the loading indicator appears.

Added this for debugging in PyCharm:

if __name__.startswith("bokeh"):
    view().servable()
elif __name__ == '__main__':
    pn.serve(view, port=8904)
1 Like