Update a Bokeh figure after adding a glyph to it

I work in a Jupyter notebook.

I am not very familiar how Bokeh and Panel work internally so it might be an obvious question…

After creating and displaying a figure in Bokeh as a Panel pane I want to add a glyph to it. Adding the glyph happens in a new Jupyter notebook cell and so the above figure does not get updated. The figure only shows the added glyphs if I trigger the figure to display again. Ideally, I would like to have the original figure being updated automatically.

In short, how do you tell Panel/Bokeh that the figure has been modified and so need a refresh/redraw/update?

Here is an example of cell #1 in the notebook:

import numpy as np

import panel as pn; pn.extension()
import bokeh as bk
from bokeh import plotting

image = np.random.random((256, 256))

figure_args = {}
figure_args['tools'] = "pan,wheel_zoom,box_zoom,save,reset"
figure_args['active_scroll'] = "wheel_zoom"
figure_args['match_aspect'] = True
figure_args['sizing_mode'] = 'stretch_both'

fig = plotting.figure(**figure_args)
fig.toolbar.logo = None

color_mapper = bk.models.LinearColorMapper(palette=bk.palettes.Viridis256, low=0, high=1)
fig.image(image=[image], x=[0], y=[0], dw=[256], dh=[256], color_mapper=color_mapper)

panel = pn.Pane(fig, sizing_mode="fixed")
panel

This cell displays correctly the figure.

Then in cell#2:

data = {}
data['x'] = [100, 200]
data['y'] = [100, 200]

source = bk.models.ColumnDataSource(data)
glyph = bk.models.markers.Circle(x="x", y="y", radius=20, line_color="#3288bd", fill_color="red", line_width=3)
fig.add_glyph(source, glyph)

does not update the output of cell #1. To be able to see both the image and the glyph I have to execute panel in a third cell.

I guess there is a method to call to fire the update but I could not find it…

Unless you embed a Bokeh server app, things do not automatically update, or propagate from the “Python side” to the “JS side”, in notebooks. You will have to explicitly ask for a synchronization using push_notebook. There are several example notebooks you can refer to and run.

As a word of up-front advice, I would try to avoid adding glyphs later. Instead I would try to prepare a plot up front with all the glyphs it might ever need to show, possibly setting the data for some of them to be empty to start. Then later, update the data for the glyphs. Bokeh is highly optimized and tuned for updating data sources. Anytime a problem can be framed as a data source update (rather than adding/subtracting glyphs), it is almost always preferable.

2 Likes

Thanks, I’ll try to keep update-only advice.

push_notebook seems to only work when using show or output_notebook. In m case, I am using Panel pn.Pane(fig) to display the Bokeh figure. Any idea how I can update it?

I don’t, I’m afraid. Anything Panel related is a question for the Holoviews/Pyviz team. I will point them at this thread.

1 Like

Note that I also tried to initialize the figure with empty glyph and empty data source before displaying the pane containing the figure.

Unfortunately, update the existing data sources does not update the figure and I still need to re-run the command to display the panel in order to see the glyphs getting updated.

Why not using the recommended panel way to update a bokeh panel?
https://panel.pyviz.org/reference/panes/Bokeh.html#gallery-bokeh

data = {}
data['x'] = [100, 200]
data['y'] = [100, 200]

source = bk.models.ColumnDataSource(data)
glyph = bk.models.markers.Circle(x="x", y="y", radius=20, line_color="#3288bd", fill_color="red", line_width=3)
fig.add_glyph(source, glyph)
panel.param.trigger('object')

What 'object' is supposed to be? In my case, I have a param.Parameterized class that own some parameter widgets and my Bokeh figure. So the update of the figure fig.add_glyph(source, glyph) is not related at all with Parameterized.

Updating a color mapper high and low attributes that is binded to the figure, correctly update the Bokeh figure display.

From your example you never talk about a Parameterized instance
If you run the two cells you have described + the command I gave you the figure update

It’s because it’s a very simplified example (maybe too much…).

panel.param.trigger('object') seems to do the job. Thanks for the help.

To close this issue I want to share why the toy example is perfectly working while my real-case code wasn’t correctly updated.

Consider the below code:

import numpy as np

import panel as pn; pn.extension()
import bokeh as bk
from bokeh import plotting

image = np.random.random((256, 256))

figure_args = {}
figure_args['tools'] = "pan,wheel_zoom,box_zoom,save,reset"
figure_args['active_scroll'] = "wheel_zoom"
figure_args['match_aspect'] = True
figure_args['sizing_mode'] = 'stretch_both'

fig = plotting.figure(**figure_args)
fig.toolbar.logo = None

color_mapper = bk.models.LinearColorMapper(palette=bk.palettes.Viridis256, low=0, high=1)
fig.image(image=[image], x=[0], y=[0], dw=[256], dh=[256], color_mapper=color_mapper, alpha=0.6)

n = 10
data = {}
data['x'] = np.random.randint(0, 256, size=(n,))
data['y'] = np.random.randint(0, 256, size=(n,))

source = bk.models.ColumnDataSource(data)
glyph = bk.models.markers.Circle(x="x", y="y", radius=5, line_color="#3288bd", fill_color="red", line_width=3)

index_filter = bk.models.IndexFilter([0, 1, 2, 3, 4, 5])
view = bk.models.CDSView(source=source, filters=[index_filter])

renderer = fig.add_glyph(source, glyph, view=view)

fig_pane = pn.Pane(fig, sizing_mode="fixed")

panel = pn.Row(pn.Column(fig_pane))
panel

This time I have attached a CDSView to the data source.

Originally I was trying to do the following:

renderer.view.filters[0].indices = [6, 7]
fig_pane.param.trigger('object')

And the figure wasn’t updating.

If instead I now instantiate a new filter object, it works:

renderer.view.filters[0] = bk.models.IndexFilter([6, 7, 2, 3])
fig_pane.param.trigger('object')

So I guess some attributes are not monitored internally (such as indices for filters) while other are (such as filters in a view).

1 Like

I think you can update the issue you opened on panel as a bug since if you open your javascript console you’ll see :

default.js:924 Exception opening new comm
(anonymous) @ default.js:924
rejected @ default.js:7
Promise.then (async)
step @ default.js:8
(anonymous) @ default.js:9
push.YC29.__awaiter @ default.js:5
_handleCommOpen @ default.js:909
(anonymous) @ default.js:1016
(anonymous) @ default.js:9
push.YC29.__awaiter @ default.js:5
_handleMessage @ default.js:973
(anonymous) @ default.js:127
Promise.then (async)
DefaultKernel._onWSMessage @ default.js:124
default.js:132 Error: Object '72903231605e4f66bffbbd49e9ed1972' not found in registry
    at default.js:1472
    at new Promise (<anonymous>)
    at Object.loadObject (default.js:1451)
    at DefaultKernel.<anonymous> (default.js:917)
    at Generator.next (<anonymous>)
    at default.js:9
    at new Promise (<anonymous>)
    at push.YC29.__awaiter (default.js:5)
    at DefaultKernel._handleCommOpen (default.js:909)
    at DefaultKernel.<anonymous> (default.js:1016)

I don’t see any error running the code on my last message (using Jupyter lab).