Python callback for Bokeh RangeTool events?

I would like to replace my current panel FloatSlider with a RangeTool on a Bokeh plot with a callback in the python backend.
I guess it would be possible with a js callback, but wanted to check if there is a python route to take first?

1 Like

Digging into docs and stackoverflow I found the solution:
range_tool.x_range.on_change('start', func)
range_tool.x_range.on_change('end', func)

Coming from matplotlib, it takes some effort to get familiarized with Bokeh structure…

Edit

Here is a working minimal example:

import panel as pn
from bokeh.events import RangesUpdate, Event
from bokeh.models import Range1d, RangeTool
from bokeh.plotting import figure


class App(pn.viewable.Viewer):
    def __init__(self):
        plot = figure(x_range=[0,5])
        range_tool = RangeTool(x_range=Range1d(1,2))
        range_tool.overlay.fill_color = "navy"
        range_tool.overlay.fill_alpha = 0.2
        range_tool.x_range.on_change('start', self._xrange_changed_callback)
        range_tool.x_range.on_change('end', self._xrange_changed_callback)
        range_tool.x_range.on_event(RangesUpdate, self._xrange_update_callback) # Never fires
        plot.add_tools(range_tool)
        plot.toolbar.active_multi = range_tool
        self.bokeh = pn.pane.Bokeh(plot)
        self.msg = pn.widgets.StaticText(name='on_change callback')
        self.msg_event = pn.widgets.StaticText(name='on_event callback')

    def _xrange_changed_callback(self, attr:str, old:float, new:float):
        # In case the range_tool is moved, it will fire two events: one for 'start' and one for 'end'. This needs to
        # be handled in the callback or just accept a redundant execution of the callback
        self.msg.value = f'{attr=}, {old=}, {new=}'

    def _xrange_update_callback(self, event:Event):
        # Unfortunately Range1d never fires RangesUpdate event and this callback is never called :-(
        self.msg_event.value = f'{event.x0=}, {event.x1=}'

    def __panel__(self):
        return pn.Column(self.msg,
                         self.msg_event,
                         self.bokeh)

if __name__ == '__main__':
    app = App()
    app.show()

It would be neat if it was possible to register a RangesUpdate event to the x_range to avoid having to handle redundant triggers when both start and end are changed (by moving the entire range), but it seems this is not implemented as the event is never triggered.

Edit 2

I have added a feature request in github: #12110

1 Like