How to cause the screen to redraw from within an event handler?

Hi

Many thanks for Bokeh!

I have successfully drawn a screen within a vscode notebook (using a Panel pane.Bokeh(column) where column is a Bokeh column). I have a python event handler there, which also works. It’s code is below, essentially every time the plot is tapped it generates an event and I do actually get the event!

However the screen does not refresh, so when I change data based on the tap - there is no change to the screen.

As you can see below, I tried several things, but they do not have the desired effect:

  1. I attempted to add a change.emit, however the Glyph and the ColumnDataSource do not have a change property. Is there some other place, accessible from within an event handler, where I can use change.emit() ? Or perhaps there is a new way to emit a redraw directive?
  2. I tried to use Panel’s pn.io.push_notebook(pane) but it simply has no effect.
  3. I tried to use Panel’s pane.param.trigger('object'), again to no effect

How do I get Bokeh to refresh the plot from inside the event handler? Thanks!

        def tap_callback(event):
            feature_index = floor(event.y)
            feature_name = event.model.y_range.factors[feature_index]
            new_value = ... # [details removed to avoid confusion]
            
            # making the change show up in the plot
            for r in event.model.renderers:
                if hasattr(r, 'glyph'):
                    r.data_source.data['most important'][-1-feature_index] = new_value
                    if hasattr(r.data_source, 'change'):
                        print('here I am')
                        r.data_source.change.emit()
                    if hasattr(r.glyph, 'change'):
                        print('here I am')
                        r.glyph.change.emit()
            
            print('will this work?')
            self.pane.param.trigger('object')
            pn.io.push_notebook(self.pane)

Versions:
Bokeh 3.1.1
vscode 1.79.2
Panel 1.1.1
jupyter_bokeh 3.0.7

Hi @gpn Panel/Holoviews is a separate project run by other people, and they have added some additional levels of eventing and control that are not developed by the Bokeh team (e.g. anything “param” related is not part of Bokeh). I’d suggesting starting this question on the Holiviz Discourse: https://discourse.holoviz.org

Thank you @Bryan - I have done so, here.

One follow up question though: can you confirm that the code above for r.data_source.change.emit() and/or r.glyph.change.emit() should normally work? That is pure Bokeh code so I assume you are best placed to answer that bit of my question.

Thank you
GPN

@gpn change.emit() etc are only useful on the JS side (e.g. in a CustomJS callback). In fact, AFAIK they don’t exist at all on the Python side. The very best way to signal an update for most common situations, on either the Python or the JS side, is to do a full new-dict assignment to a source.data:

source.data = entirely_new_data_dict

Bokeh will always automatically notice this and trigger redraws. No change.emit is needed either, that is only required for “in-place” updates where you reach inside and modify the contents of individual columns directly.

Edit: clarifying “entirely new data dict” I mean a new, different object, which might contain all the same data or columns as the old dict object, that’s fine.

@Bryan Thanks for that advice. The solution, as you said, is by doing a complete recreation of the ColumnDataSource and then setting the source, like so:

        def tap_callback(event):
            feature_index = floor(event.y)
            feature_name = event.model.y_range.factors[feature_index]
            new_value = ... # ommitted irrelevant details

            # create new dict of all the data
            new_data = {}
            for r in event.model.renderers:
                if hasattr(r, 'glyph'):
                    for c in r.data_source.data:
                        new_data[c] = r.data_source.data[c]
                    new_data['relevant column name'][-1-feature_index] = new_value
                    r.data_source.data = new_data
                    break

By the way, a copy also works, like so:

        def tap_callback(event):
            feature_index = floor(event.y)
            feature_name = event.model.y_range.factors[feature_index]
            new_value = ... # ommitted irrelevant details

            # create new dict of all the data
            new_data = {}
            for r in event.model.renderers:
                if hasattr(r, 'glyph'):
                    new_data = r.data_source.data.copy()
                    new_data['relevant column name'][-1-feature_index] = new_value
                    r.data_source.data = new_data
                    break

I hope someone else benefits from this solution.
Many thanks
GPN

FYI I think new_data = dict(r.data_source.data) would accomplish the same thing without a loop.

a deep copy also works, like so

dict.copy is a shallow copy, i.e. identical to dict(other_dict)

In [5]: dict.copy?
Docstring: D.copy() -> a shallow copy of D
Type:      method_descriptor

which is good, a shallow copy is all that is required.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.