Layout won't update after curdoc().unhold()

In the following code, I change the source of a plot renderer in a callback.
For performance reasons (which don’t play a role in this minimal example), I first remove the plot from the layout, apply the change and then put the plot back to the layout.
This works fine, but as expected I get a blinking which I would like to avoid, so I tried to use curdoc().hold() - curdoc().unhold().
Then, the update is not shown after completion of the callback.
Also, I get a JavaScript console warning reference already known 'p1002', P1002 being the id of the plot.

What can I do to make sure the plots are updated after these steps?

from bokeh.io import curdoc
from bokeh.plotting import figure, show
from bokeh.layouts import Column
from bokeh.models import Button, ColumnDataSource

button = Button(label="Click")

p = figure(width=400, height=400, title="Title")

small_list = list(range(1000))
small_dict = dict(x=small_list, y=small_list)
source = ColumnDataSource(small_dict)

big_list = list(range(50000)) # <- Increase this number if the change is too quick on your machine
big_dict = dict(x=big_list, y=big_list)

p.circle(source=source)

def callback():
    curdoc().hold() # <- Toggle
    layout.children = [button]
    p.title.text="New title"
    p.renderers[0].data_source.data = big_dict
    layout.children.append(p)
    curdoc().unhold() # <- Toggle

button.on_click(callback)

layout = Column(button, p)

curdoc().add_root(layout)

Here is an explanation why I am doing this:
I have an app in which the user can add or remove as many plots as he wants.
In each plot, the user can add or remove as many renderers as he wants.

Each time a line is added from user, several things happen in the background, among other:

  • adding a line glyph with p.line(...
  • adding a circle glyph with p.circle(...
  • adding two additional lines to display tolerances
  • link tolerance lines visible attribute with ref_line.js_link('visible', tol_line, 'visible')
  • adding legend items with p.legend.items.append(LegendItem(...
  • for each line, adding hover inspection with p.add_tools(HoverTool(...

So for each line added by the user, I have about ten calls out of the ones mentioned above.

Additionaly, the user can work with categories, in which case the number of lines is automatically multiplied by the number of categorical items, a typical number would be five, so fifty of the mentioned calls.
This number will be potentized by the number of plots.

I observed that the mentioned calls are done within 1 ms if the plots are not in the layout, but each call takes about 70 ms if the plots are in the layout.
In the latter case, a user action can lead to an update time of dozens of seconds, which is a bad user experience.

Therefore, I tried to first remove the plots from the layout, perform the update in a reasonable time, then add the plots back to the layout.

Since I nether know up front how many plots the user will want to display nor how many lines, it is difficult to define at start a given number of plots and curves an just use the visible attribute to toggle them in/out.
I am using curdoc().hold() - curdoc().unhold() to avoid blinking.

Does anyone have ideas of a better approach for solving my problem?


My system:
Python version : 3.12.0 | packaged by conda-forge | (main, Oct 3 2023, 08:26:13) [MSC v.1935 64 bit (AMD64)]
IPython version : 8.17.2
Tornado version : 6.3.3
Bokeh version : 3.3.0
BokehJS static path : C:\Users\Blaise\miniconda3\envs\servenv312\Lib\site-packages\bokeh\server\static
node.js version : (not installed)
npm version : (not installed)
Operating system : Windows-10-10.0.19045-SP0

Is the use of lists rather than numpy arrays representative of your actual code? If so, I would first recommend switching to using arrays, since Bokeh can transmit numpy arrays much more efficiently using a binary transmission protocol.

Recgarding unhold and layout, possibly just a bug. TBH when I added hold and unhold the only scenarios I had in mind were ones to update data for a plot, so it’s entirely possible there are capability gaps.

@Icoti Have you tried to use hold_render for the figure and not changing/updating the layout object?

def callback():
    #curdoc().hold() # <- Toggle
    #layout.children = [button]
    p.hold_render = True
    p.title.text="New title"
    p.renderers[0].data_source.data = big_dict
    p.hold_render = False
    #layout.children.append(p)
    #curdoc().unhold() # <- Toggle

@Jonas_Grave_Kristens Unfortunately, .hold_render = True doesn’t shorten the updating time in my application.

@Bryan I was not aware that numpy arrays were more effectively transmitted than lists. I tested this and there is really a big difference. Thanks for the hint.

Unfortunately, this doesn’t help in my actual code since I use ColumnDataSource(pandas.dataframe) which already automatically produces numpy arrays.

I didn’t manage to produce a minimal reproductible example showing the runtime difference when the plots are / are not in the layout yet. I will keep working on it.

In the meantime, I posted an issue regarding the unhold an layout problem:

1 Like