Temporarily prevent Bokeh update/redraw, and re-enable it later (for Jupyter with ipywidgets)?

I am using Bokeh in Jupyter notebook; and I have several plots in a single figure. I’m basically starting like this:

import bokeh.plotting as bplt

# for the ipython notebook
bplt.output_notebook()

figLchar_p1 = bplt.figure(plot_height=300, tools="pan,wheel_zoom,box_zoom,hover,reset")

traces_l = []
for ich, ichan in enumerate(datachans):
    line = figLchar_p1.line([0], [0], line_width=1, line_color=mycolors[ich])
    traces_l.append(line)
...

… so I just add the (0,0) point at start; then based on a dropdown, I load in data into Pandas dataframe, and then use that to update the plot:

# "update loop"
for ich, ichan in enumerate(datachans):
    traces_l[ich].data_source.data={ 'x': this_ch_data["xdata"], 'y': this_ch_data["ydata"] }

… and this, in principle, works just fine.

However, specifically in my case, I have at least 5 line plots (with also circle markers) with at least 800 points in each; so when I have one plot loaded, and then I load another, I can see the lines changing “one by one” so to speak.

So, I would like to temporarily prevent bokeh plot redraw/update, then change the .data_sources “in peace”, and then finally enable bokeh plot redraw/update again - which would hopefully allow me to visually see “instantly” how all 5 lines change in the plot (and not just one after another).

Is there a way to do this temporary disable and reenable of bokeh plot drawing/rendering, and if so, how can I do it?

Ok, I looked into this a bit more; and there seemingly, two general options for this:

So, maybe I should have been more specific: not that I just want to use Jupyter notebooks, but I also want to use ipywidgets (will update the title soon)

Anyways, I finally wrote a proper runnable example, which will hopefully demonstrate the sluggishness I’m trying to demonstrate:

import bokeh.plotting as bplt
from jupyter_bokeh.widgets import BokehModel
import bokeh.io
import numpy as np
import pandas as pd
import requests
import random
from ipywidgets import widgets, Layout
from IPython.display import display

# for the ipython notebook
bplt.output_notebook()

CSSNAMESLINK="https://gist.githubusercontent.com/ek/913fe6905da054977ab9ebc00fc43470/raw/4294f90b9dee884a1146e40e7fc57ada62eb82a0/html-color-names-keywords-list.txt"
css_response = requests.get(CSSNAMESLINK)
cssnames = css_response.text.split()

# generate simulated data
numvals = 1000 # 10000
#numvals = 10
xcol = np.arange(0, numvals, 1)
df = pd.DataFrame(dtype=np.int64)
for iser in range(0, 3):
    for ix in xcol:
        irow = iser*len(xcol)+ix
        df.at[irow, 0] = int(iser)
        df.at[irow, 1] = int(ix)
        df.at[irow, 2] = int( ix*(iser+2)*0.3 )
df = df.rename(columns={0: "ser", 1: "xcol", 2: "ycol"})
df = df.astype(np.int64) # without this, getting floats

# prepare plot
datachans = df["ser"].unique()
mycolors = random.sample(cssnames, len(datachans))
print(mycolors)

myfig = bplt.figure(plot_height=300, tools="pan,wheel_zoom,box_zoom,hover,reset", sizing_mode="stretch_width")
traces_l = []
for ichan in datachans:
    # use .circle here instead of .line, as it meeds more drawing/is more sluggish
    line = myfig.circle([0,0], [0,0], line_width=2, line_color=mycolors[ichan])
    traces_l.append(line)
myfig.x_range.start = 0 ; myfig.x_range.end = numvals
myfig.y_range.start = 0 ; myfig.y_range.end = numvals

the_slider = widgets.IntSlider(min=0, max=len(datachans), description='Test:')

myfig_model = BokehModel(myfig)
#handle_myfig = bokeh.io.show(myfig, notebook_handle=True) # direct in VBox: TraitError: The 'children' trait of a VBox instance contains an Instance of a TypedTuple which expected a Widget, not the CommsHandle 
#myfig_model = BokehModel(handle_myfig) # AssertionError: isinstance(model, LayoutDOM) 

the_ui = widgets.VBox([the_slider, myfig_model])
#the_ui = widgets.VBox([the_slider, handle_myfig])

def update_plot(newval):
    for ichan in datachans:
        this_ch_data = df[df["ser"]==ichan]
        traces_l[ichan].data_source.data={ 'x': this_ch_data["xcol"].values, 'y': this_ch_data["ycol"].values+newval }

def on_slider_change(change):
    if change['type'] == 'change' and change['name'] == 'value': # prevent multiple hits upon single value change
        update_plot(change['new']*0.1*numvals)
        # "Next tick callbacks only work within the context of a Bokeh server session. This function will no effect when Bokeh outputs to standalone HTML or Jupyter notebook cells."
        #bplt.curdoc().add_next_tick_callback(update_plot)
the_slider.observe(on_slider_change)


display(the_ui)
on_slider_change({'type': 'change', 'name': 'value', 'new': 0 }) # initial call

I am also aware there is also ipywidgets's interact (as described in “Jupyter interactors”) - unfortunately, my UI is a bit more complex than what can be (readably) fit in a single interact call, which is why I feel it’s needed to code the Bokeh plot as a ipywidgets GUI element.

Right, so here are a couple of more examples:

This is an example using ipywidgets.widgets.interactive_output - to me it looks as sluggish as the previous solution:

import bokeh.plotting as bplt
from jupyter_bokeh.widgets import BokehModel
import bokeh.io
import numpy as np
import pandas as pd
import requests
import random
from ipywidgets import widgets, Layout
from IPython.display import display

# for the ipython notebook
bplt.output_notebook()

CSSNAMESLINK="https://gist.githubusercontent.com/ek/913fe6905da054977ab9ebc00fc43470/raw/4294f90b9dee884a1146e40e7fc57ada62eb82a0/html-color-names-keywords-list.txt"
css_response = requests.get(CSSNAMESLINK)
cssnames = css_response.text.split()

# generate simulated data
numvals = 1000 # 10000
#numvals = 10
xcol = np.arange(0, numvals, 1)
df = pd.DataFrame(dtype=np.int64)
for iser in range(0, 3):
    for ix in xcol:
        irow = iser*len(xcol)+ix
        df.at[irow, 0] = int(iser)
        df.at[irow, 1] = int(ix)
        df.at[irow, 2] = int( ix*(iser+2)*0.3 )
df = df.rename(columns={0: "ser", 1: "xcol", 2: "ycol"})
df = df.astype(np.int64) # without this, getting floats

# prepare plot
datachans = df["ser"].unique()
mycolors = random.sample(cssnames, len(datachans))
print(mycolors)

myfig = bplt.figure(plot_height=300, tools="pan,wheel_zoom,box_zoom,hover,reset", sizing_mode="stretch_width")
traces_l = []
for ichan in datachans:
    # use .circle here instead of .line, as it meeds more drawing/is more sluggish
    line = myfig.circle([0,0], [0,0], line_width=2, line_color=mycolors[ichan])
    traces_l.append(line)
myfig.x_range.start = 0 ; myfig.x_range.end = numvals
myfig.y_range.start = 0 ; myfig.y_range.end = numvals

the_slider = widgets.IntSlider(min=0, max=len(datachans), description='Test:')

myfig_model = BokehModel(myfig)

the_ui = widgets.VBox([the_slider, myfig_model])

def update_plot(newval):
    final_newval = newval*0.1*numvals
    for ichan in datachans:
        this_ch_data = df[df["ser"]==ichan]
        traces_l[ichan].data_source.data={ 'x': this_ch_data["xcol"].values, 'y': this_ch_data["ycol"].values+final_newval }
    bokeh.io.push_notebook()


out = widgets.interactive_output(update_plot, {'newval': the_slider})

display(the_ui)
on_slider_change({'type': 'change', 'name': 'value', 'new': 0 }) # initial call

And here is one, that uses bokeh.io.push_notebook and notebook_handle=True - it seems much faster/more responsive – regardless if the_slider.observe is used, or if widgets.interactive_output is used.

import bokeh.plotting as bplt
from jupyter_bokeh.widgets import BokehModel
import bokeh.io
import numpy as np
import pandas as pd
import requests
import random
from ipywidgets import widgets, Layout
from IPython.display import display

# for the ipython notebook
bplt.output_notebook()

CSSNAMESLINK="https://gist.githubusercontent.com/ek/913fe6905da054977ab9ebc00fc43470/raw/4294f90b9dee884a1146e40e7fc57ada62eb82a0/html-color-names-keywords-list.txt"
css_response = requests.get(CSSNAMESLINK)
cssnames = css_response.text.split()

# generate simulated data
numvals = 1000 # 10000
#numvals = 10
xcol = np.arange(0, numvals, 1)
df = pd.DataFrame(dtype=np.int64)
for iser in range(0, 3):
    for ix in xcol:
        irow = iser*len(xcol)+ix
        df.at[irow, 0] = int(iser)
        df.at[irow, 1] = int(ix)
        df.at[irow, 2] = int( ix*(iser+2)*0.3 )
df = df.rename(columns={0: "ser", 1: "xcol", 2: "ycol"})
df = df.astype(np.int64) # without this, getting floats

# prepare plot
datachans = df["ser"].unique()
mycolors = random.sample(cssnames, len(datachans))
print(mycolors)

myfig = bplt.figure(plot_height=300, tools="pan,wheel_zoom,box_zoom,hover,reset", sizing_mode="stretch_width")
traces_l = []
for ichan in datachans:
    # use .circle here instead of .line, as it meeds more drawing/is more sluggish
    line = myfig.circle([0,0], [0,0], line_width=2, line_color=mycolors[ichan])
    traces_l.append(line)
myfig.x_range.start = 0 ; myfig.x_range.end = numvals
myfig.y_range.start = 0 ; myfig.y_range.end = numvals

the_slider = widgets.IntSlider(min=0, max=len(datachans), description='Test:')

the_ui = widgets.VBox([the_slider])

def update_plot(newval):
    final_newval = newval*0.1*numvals
    for ichan in datachans:
        this_ch_data = df[df["ser"]==ichan]
        traces_l[ichan].data_source.data={ 'x': this_ch_data["xcol"].values, 'y': this_ch_data["ycol"].values+final_newval }
    bokeh.io.push_notebook(handle=nbhandle)


def on_slider_change(change):
    if change['type'] == 'change' and change['name'] == 'value': # prevent multiple hits upon single value change
        update_plot(change['new'])
the_slider.observe(on_slider_change)

nbhandle = bokeh.io.show(myfig, notebook_handle=True)

display(the_ui)
the_slider.value = 1 # initial call

EDIT: also discovered, that this works:

import bokeh.plotting as bplt
from jupyter_bokeh.widgets import BokehModel
import bokeh.io
from bokeh.io.notebook import run_notebook_hook
from bokeh.io.state import curstate
....

state = curstate()
nbhandle = run_notebook_hook(state.notebook_type, 'doc', myfig, state, notebook_handle=True)
myfig_model = BokehModel(nbhandle._doc.roots[0])
the_ui = widgets.VBox([the_slider, myfig_model])
display(the_ui)

… however, unfortunately, the call to run_notebook_hook also draws the plot, so then the last display(the_ui) will draw a second plot - otherwise, this could have made BokehModel for ipywidgets work with push_notebook without problems (actually, it does work, its just that two graphs are needlessly updated)

Hi @sdbbs I would say you are definitely pushing the envelope a bit, so it will really require some experimentation. Unfortunately we are in the middle of prepping a release, and I am also coming off a week off work and have a lot to catch up on, so I can’t really dig far into the details just now. A few quickl comments:

  • If you are embedding a Bokeh server app, there is curdoc().hold() and curdoc().unhold() that might be useful, There is an example in the repo (code grep for “unhold”)

  • If you are using push_notebook you may just have to try to patch e.g. run_notebook_hook to suit your needs. Then any changes you discover could be the basis of a feature request.

Many thanks for the feedback, @Bryan:

No worries - I very much appreciate the feedback; and great to know what the status on this kind of plot drawing is.

Much appreciated! I am not embedding a Bokeh server app currently, but hold and unhold look very promising.

So far, it seems I will manage in my task with push_notebook without embedding into BokehModel for ipywidgets - it seems that bokeh.io.show plot output can interleave more or less fine with UI widgets output of display(...) (that is, I can have widgets before, then output plot, then output some more widgets, and have them all talk to each other).

UItimately, wrapping in BokehModel would allow additional leveraging of some of the layout options of ipywidgets in Jupyter notebooks - but for now, I can live without it.

Thanks for a great library!

I got to another solution - basically, copied in some code to generate a handle, without having to instantiate a plot - and then I can use BokehModel; it seems to work, but it sorta seems sluggish (so quite possibly, I might have missed something in my code copying); but at least, this is an example with both handle and BokehModel (for ipywidgets):

import bokeh.plotting as bplt
from jupyter_bokeh.widgets import BokehModel
import bokeh.io
import numpy as np
import pandas as pd
import requests
import random
from ipywidgets import widgets, Layout
from IPython.display import display, HTML
from bokeh.io.notebook import run_notebook_hook
from bokeh.io.state import curstate
import bokeh.embed.notebook

CSSNAMESLINK="https://gist.githubusercontent.com/ek/913fe6905da054977ab9ebc00fc43470/raw/4294f90b9dee884a1146e40e7fc57ada62eb82a0/html-color-names-keywords-list.txt"
css_response = requests.get(CSSNAMESLINK)
cssnames = css_response.text.split()

# generate simulated data
numvals = 1000 # 10000
#numvals = 10
xcol = np.arange(0, numvals, 1)
df = pd.DataFrame(dtype=np.int64)
for iser in range(0, 3):
    for ix in xcol:
        irow = iser*len(xcol)+ix
        df.at[irow, 0] = int(iser)
        df.at[irow, 1] = int(ix)
        df.at[irow, 2] = int( ix*(iser+2)*0.3 )
df = df.rename(columns={0: "ser", 1: "xcol", 2: "ycol"})
df = df.astype(np.int64) # without this, getting floats

# prepare plot
datachans = df["ser"].unique()
mycolors = random.sample(cssnames, len(datachans))
print(mycolors)

# for the ipython notebook
bplt.output_notebook()

myfig = bplt.figure(plot_height=300, tools="pan,wheel_zoom,box_zoom,hover,reset", sizing_mode="stretch_width")
traces_l = []
for ichan in datachans:
    # use .circle here instead of .line, as it meeds more drawing/is more sluggish
    line = myfig.circle([0,0], [0,0], line_width=2, line_color=mycolors[ichan])
    traces_l.append(line)

myfig.x_range.start = 0 ; myfig.x_range.end = numvals
myfig.y_range.start = 0 ; myfig.y_range.end = numvals

def get_bokeh_handle(infigure):
    # as in def show_doc:
    state = curstate()
    comms_target = bokeh.util.serialization.make_id()
    (script, div, cell_doc) = bokeh.embed.notebook.notebook_content(infigure, comms_target)
    thehandle = bokeh.io.notebook.CommsHandle(bokeh.io.notebook.get_comms(comms_target), cell_doc)
    state.document.on_change_dispatch_to(thehandle)
    state.last_comms_handle = thehandle
    return thehandle

#myfig_model = BokehModel(myfig) # can go with this, then get_bokeh_handle after; or:
nbhandle = get_bokeh_handle(myfig)
myfig_model = BokehModel(nbhandle._doc.roots[0])

def update_plot(newval):
    final_newval = newval*0.1*numvals
    for ichan in datachans:
        this_ch_data = df[df["ser"]==ichan]
        traces_l[ichan].data_source.data={ 'x': this_ch_data["xcol"].values, 'y': this_ch_data["ycol"].values+final_newval }
    bokeh.io.push_notebook(handle=nbhandle)

the_slider = widgets.IntSlider(min=0, max=len(datachans), description='Test:')

def on_slider_change(change):
    if change['type'] == 'change' and change['name'] == 'value': # prevent multiple hits upon single value change
        update_plot(change['new'])
the_slider.observe(on_slider_change)

the_hbox = widgets.HBox([myfig_model, widgets.HTML("<div style='background-color: yellow;'>aa</div>", layout=Layout(width='40%'))])
the_ui = widgets.VBox([the_slider, the_hbox])
display(the_ui)

the_slider.value = 1 # initial call