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.
- 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. - 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 expectingpn.state.curdoc
to beNone
. The documentation here added to my confusion because there’s a linedoc = pn.state.curdoc
, butdoc
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.