Advise on queuing multiple server callbacks and whether there is an alternative callback trigger.

Good afternoon all,

I was after some help configuring server side on_change events to trigger when a plot is zoomed. I have a large 2D array of data (5000, >1000000), which I wish to visualise as an image using bokeh. As such, I am resizing the data (skimage.transform.resize) to the bokeh plot width and height. I am trying to use an on_change function to call the resize when the plot is zoomed, panned or reset. I’ve attached a callback to the plot x_range and y_ranges to trigger on start and end changes. But this (understandably) causes the resize callback to be triggered 4 times on each zoom event. It is possible and desirable to be able to trigger a resize event on any subset of the range changes (a zoom may not change the y_range.end for instance), so I cannot simply wait for all on_change events to be calculated.

Example code:

import bokeh.plotting as bk
from bokeh.application.handlers import FunctionHandler
from bokeh.application import Application
from bokeh.models import LinearColorMapper, ColumnDataSource

from skimage.transform import resize
import numpy as np
import xarray as xa
bk.output_notebook()

# Create a simple array image - basically lifted from https://bokeh.pydata.org/en/latest/docs/gallery/image.html
N = 5000
x = np.linspace(0, 10*np.pi, N)
y = np.linspace(0, 10*np.pi, N)
xx, yy = np.meshgrid(x, y)
d = np.sin(xx)*np.cos(yy)

# Using xarray as this is the array handling object of choice in production code
array = xa.DataArray(data = d, coords = (x, y), dims = ('x', 'y'))

class Plot():

    def __init__(self, array, width = 800, height = 800):
        # Probably unecessary, but done for syntactic familiarity
        self.tla = array.to_pandas()

        xrange = self.tla.        index
yrange = self.tla.
        columns
# Configure plot object
        self.p = bk.figure(height = height, width = width,
                           x_range = (xrange.min(), xrange.max()),
                           y_range = (yrange.max(), yrange.min()))

        # Create ColoumnDataSource object with the resized input array, no arguments passed to
        # simplify its use during callback
        self.source = ColumnDataSource(data = {'image': [self.resize()]})

        # Colormapper for image and colorbar control
        self.color_mapper = LinearColorMapper(palette="Viridis256", low=-1, high=1)

        # Create image
        self.p.image('image', source = self.source, color_mapper=self.color_mapper,
                   dh=yrange.max() - yrange.min(), dw=xrange.max() - xrange.min(), x = xrange.min(), y=yrange.max())

        # Add callback to x_range and y_range
        self.p.x_range.on_change('end', self.callback)
        self.p.x_range.on_change('start', self.callback)
        self.p.y_range.on_change('end', self.callback)
        self.p.y_range.on_change('start', self.callback)

    def resize(self):
        # Slice tla frame by x and y range bounds and then resize to fit plot height and width
        return resize(self.tla.loc[self.p.x_range.start:self.p.x_range.end,
                                   self.p.y_range.end:self.p.y_range.start].as_matrix(),
                      [self.p.plot_width, self.p.plot_height],
                      preserve_range = True, mode = 'reflect')

    def modify_doc(self, doc):
        # Add plot to document
        doc.add_root(self.p)

    def callback(self, attr, start, end):

        # Call resize
        data = ColumnDataSource(data = {'image': [self.resize()]}).        data
# Re-assign computed data to ColumnDataSource object
        self.source.data = data
print("Callback calculated @ {0}:{1}, {2}:{3}".format(self.p.x_range.start, self.p.x_range.end,
                                                    self.p.y_range.start, self.p.y_range.end))

# Create Plot
plot = Plot(array)

handler = FunctionHandler(plot.modify_doc)
app = Application(handler)

show(app)

I have also looked at adding an on_event change to the plot object itself, and adding an on_change callback to the tools, but don’t seem to be able to find appropriate triggers for either object.

Any help or advise would be much appreciated!