Upgrade to Bokeh v1.4 needs custom model registration?

Hi,

Some time ago I created a custom class for plotting images. The code is shown below (apologies for the length but it’s quicker to show it all than try to cut it down). Up to Bokeh 1.3.4 it worked fine but I just upgraded to 1.4 and the example fails with the following error in the browser console:

Error: Model ‘ColourMap.Column’ does not exist. This could be due to a widget or a custom model not being registered before first usage.

Any advice on how to fix this would be much appreciated!

The custom class:

'''ColourMap class definition'''

from bokeh.plotting import Figure

from bokeh.models import ColumnDataSource, Plot, ColorBar, HoverTool
from bokeh.models.mappers import LinearColorMapper
from bokeh.models.ranges import Range1d
from bokeh.models.layouts import Column
from bokeh.models.callbacks import CustomJS

from bokeh.core.properties import Instance, String, Float, Bool, Int

from bokcolmaps.get_common_kwargs import get_common_kwargs
from bokcolmaps.generate_colourbar import generate_colourbar
from bokcolmaps.read_colourmap import read_colourmap
from bokcolmaps.get_min_max import get_min_max


class ColourMap(Column):

    '''
    Plots an image as a colour map with a user-defined colour scale and
    creates a hover readout. The image must be on a uniform grid to be
    rendered correctly and for the data cursor to provide correct readout.
    '''

    __view_model__ = 'Column'
    __subtype__ = 'ColourMap'

    __sizing_mode__ = 'stretch_both'

    plot = Instance(Plot)
    cbar = Instance(ColorBar)

    datasrc = Instance(ColumnDataSource)
    cvals = Instance(ColumnDataSource)

    cmap = Instance(LinearColorMapper)

    title_root = String
    zlab = String

    rmin = Float
    rmax = Float
    autoscale = Bool

    xsize = Int
    ysize = Int
    zsize = Int

    cbdelta = Float

    js_hover = String

    cjs_slider = Instance(CustomJS)

    def __init__(self, x, y, z, dm, **kwargs):

        '''
        x,y and z are 1D NumPy arrays for the 3D grid dimensions.
        dm is a 3D NumPy array.
        Supply a bokeh palette name or a file of RGBA floats -
        this will be used if provided.
        xlab,ylab,zlab,dmlab: labels for the axes and data.
        height and width for the plot are in pixels.
        rmin and rmax are fixed limits for the colour scale
        (i.e. it won't autoscale if they are both not None).
        xran and yran: ranges for the x and y axes (e.g. to link to
        another plot).
        hover: enable hover tool readout.
        '''

        palette, cfile, xlab, ylab, zlab,\
            dmlab, rmin, rmax, xran, yran = get_common_kwargs(**kwargs)

        height = kwargs.get('height', 575)
        width = kwargs.get('width', 500)
        hover = kwargs.get('hover', True)

        super().__init__()

        self.cbdelta = 0.01  # Min colourbar range (used if values are equal)

        self.title_root = dmlab
        self.zlab = zlab

        self.rmin = rmin
        self.rmax = rmax
        self.autoscale = True
        if (self.rmin is not None) and (self.rmax is not None):
            self.autoscale = False

        if len(dm.shape) == 2:
            self.ysize, self.xsize = dm.shape
            self.zsize = 1
        elif len(dm.shape) == 3:
            self.zsize, self.ysize, self.xsize = dm.shape

        if len(dm.shape) > 2:  # Default to first slice
            d = dm[0]
        else:
            d = dm

        dm = dm.flatten()

        # All variables stored as single item lists in order to be the same
        # length (as required by ColumnDataSource)
        self.datasrc = ColumnDataSource(data={'x': [x], 'y': [y], 'z': [z],
                                              'image': [d], 'dm': [dm],
                                              'xp': [0], 'yp': [0], 'dp': [0]})

        self.get_cmap(cfile, palette)

        if xran is None:  # Default to whole range unless externally controlled
            xran = Range1d(start=x[0], end=x[-1])
        if yran is None:
            yran = Range1d(start=y[0], end=y[-1])

        ptools = ['reset,pan,wheel_zoom,box_zoom,save']

        # JS code defined whether or not hover tool used as may be needed in
        # class ColourMapLP

        self.js_hover = '''
        var geom = cb_data['geometry'];
        var data = datasrc.data;

        var hx = geom.x;
        var hy = geom.y;

        var x = data['x'][0];
        var y = data['y'][0];
        var d = data['image'][0];

        var dx = x[1] - x[0];
        var dy = y[1] - y[0];
        var xind = Math.floor((hx + dx/2 - x[0])/dx);
        var yind = Math.floor((hy + dy/2 - y[0])/dy);

        if ((xind < x.length) && (yind < y.length)) {
            data['xp'] = [x[xind]];
            data['yp'] = [y[yind]];
            var zind = yind*x.length + xind;
            data['dp'] = [d[zind]];
        }
        '''

        if hover:
            cjs_hover = CustomJS(args={'datasrc': self.datasrc},
                                 code=self.js_hover)
            htool = HoverTool(tooltips=[(xlab, '@xp{0.00}'),
                                        (ylab, '@yp{0.00}'),
                                        (dmlab, '@dp{0.00}')],
                              callback=cjs_hover, point_policy='follow_mouse')
            ptools.append(htool)

        self.plot = Figure(x_axis_label=xlab, y_axis_label=ylab,
                           x_range=xran, y_range=yran,
                           plot_height=height, plot_width=width,
                           tools=ptools, toolbar_location='right')

        self.update_title(0)

        self.plot.title.text_font = 'garamond'
        self.plot.title.text_font_size = '12pt'
        self.plot.title.text_font_style = 'bold'
        self.plot.title.align = 'center'

        dx = abs(x[1] - x[0])
        dy = abs(y[1] - y[0])

        pw = abs(x[-1] - x[0]) + dx
        ph = abs(y[-1] - y[0]) + dy

        # The image is displayed such that x and y coordinate values
        # correspond to the centres of rectangles

        xs = xran.start
        if xs is None:
            xs = 0
        elif xran.end > xran.start:
            xs -= dx / 2
        else:
            xs += dx / 2
        ys = yran.start
        if ys is None:
            ys = 0
        elif yran.end > yran.start:
            ys -= dy / 2
        else:
            ys += dy / 2

        self.plot.image('image', source=self.datasrc, x=xs, y=ys,
                        dw=pw, dh=ph, color_mapper=self.cmap)

        # Needed for HoverTool...
        self.plot.rect(x=(x[0] + x[-1]) / 2, y=(y[0] + y[-1]) / 2, width=pw, height=ph,
                       line_alpha=0, fill_alpha=0, source=self.datasrc)

        self.plot.xaxis.axis_label_text_font = 'garamond'
        self.plot.xaxis.axis_label_text_font_size = '10pt'
        self.plot.xaxis.axis_label_text_font_style = 'bold'

        self.plot.yaxis.axis_label_text_font = 'garamond'
        self.plot.yaxis.axis_label_text_font_size = '10pt'
        self.plot.yaxis.axis_label_text_font_style = 'bold'

        self.cbar = generate_colourbar(self.cmap, cbarwidth=round(height / 20))
        self.plot.add_layout(self.cbar, 'below')

        self.children.append(self.plot)

    def get_cmap(self, cfile, palette):

        '''Get the colour mapper'''

        if self.autoscale:
            min_val, max_val = get_min_max(self.datasrc.data['image'][0],
                                           self.cbdelta)
        else:
            min_val = self.rmin
            max_val = self.rmax

        if cfile is not None:
            self.read_cmap(cfile)
            self.cmap = LinearColorMapper(palette=self.cvals.data['colours'])
        else:
            self.cmap = LinearColorMapper(palette=palette)
        self.cmap.low = min_val
        self.cmap.high = max_val

    def read_cmap(self, fname):

        '''
        Read in the colour scale.
        '''

        self.cvals = read_colourmap(fname)

    def change_slice(self, zind):

        '''
        Change the 2D slice of D being displayed (i.e. a different value of z)
        '''

        if (self.zsize > 1) and (zind >= 0) and (zind < self.zsize):
            zindl = zind * self.xsize * self.ysize
            dms = self.datasrc.data['dm'][0][zindl:zindl + self.xsize * self.ysize]
            self.datasrc.patch({'image': [(0, dms)]})

    def update_cbar(self, zind):

        '''
        Update the colour scale (needed when the data for display changes).
        '''

        if self.autoscale:
            d = self.datasrc.data['dm'][0][zind * self.xsize * self.ysize:
                                           (zind + 1) * self.xsize * self.ysize]
            min_val, max_val = get_min_max(d, self.cbdelta)
            self.cmap.low = min_val
            self.cmap.high = max_val

    def update_title(self, zind):

        if len(self.datasrc.data['z'][0]) > 1:
            self.plot.title.text = self.title_root + ', ' + \
                self.zlab + ' = ' + str(self.datasrc.data['z'][0][zind])
        else:
            self.plot.title.text = self.title_root

    def input_change(self, attrname, old, new):

        '''
        Callback for use with e.g. sliders.
        '''

        self.change_slice(new)
        self.update_cbar(new)
        self.update_title(new)

Example use of the class:

import numpy

from bokeh.plotting import show
from bokeh.palettes import Viridis256

from bokcolmaps.ColourMap import ColourMap
from bokcolmaps.Examples import example_data

x, y, z, D = example_data()
D = D[0]  # Data for first value of z
z = numpy.array([z[0]])  # First value of z

cm = ColourMap(x, y, z, D, cfile=None, palette=Viridis256,
               xlab='x val', ylab='y val', zlab='power val',
               dmlab='Function val')

show(cm)

The example data:

import numpy

def example_data():

    x = numpy.linspace(1, 2, 11)  # x and y must be uniformly spaced for ColourMap class
    y = numpy.linspace(2, 4, 21)
    z = numpy.array([0.5, 0.8, 1, 1.5, 2.5, 3.1])  # z can be non-uniformly spaced
    nx, ny, nz = x.size, y.size, z.size

    D = numpy.ndarray([nz, ny, nx])

    for i in range(nz):
        for j in range(ny):
            for k in range(nx):
                D[i, j, k] = (y[j]*x[k])**z[i]

    return x, y, z, D

…and if it all works, running the example should give the image below…

Hopefully @mateusz can chime in. FYI I did try to run the code to take a quick look but some pieces are missing.

Sorry @Bryan, I forgot to include all the code. In order to avoid cluttering up the forum with the rest of it, the package is available on BitBucket here or from PyPI (pip install bokcolmaps).

I’ve created a minimum example which replicates the problem. The code is below, which needs to be in packages organised as follows:

cctest

Custom.py:

from bokeh.plotting import Figure
from bokeh.layouts import Column


class Custom(Column):

    __view_model__ = 'Column'
    __subtype__ = 'Custom'

    def __init__(self):

        super().__init__()

        plot = Figure(plot_height=500, plot_width=500)
        plot.circle([1, 2, 3], [3, 2, 1], size=20)

        self.children.append(plot)

example.py:

from bokeh.plotting import show
from cctest.Custom import Custom

custom = Custom()
show(custom)

(The init.py under both cctest and Examples is empty.)

The example runs under Bokeh v1.3.4 but fails under v1.4 with the following error:

Error: Model ‘cctest.Custom.Column’ does not exist. This could be due to a widget or a custom model not being registered before first usage.

@Marcus_Donnelly I’m pressed for time so I pasted everything in one file:

from bokeh.plotting import Figure
from bokeh.layouts import Column


class Custom(Column):

    __view_model__ = 'Column'
    __subtype__ = 'Custom'

    def __init__(self):

        super().__init__()

        plot = Figure(plot_height=500, plot_width=500)
        plot.circle([1, 2, 3], [3, 2, 1], size=20)

        self.children.append(plot)


from bokeh.plotting import show

custom = Custom()
show(custom)

And it worked fine for me, are you saying this does not work for you? If this works for you, then perhaps there is some issue when when splitting modules up, will have to try later. Otherwise, if this does not work for you, I think there is something going on on your end.

@Bryan, thanks for the quick response. Yes, that does work for me. It is the hierarchy which makes it fail (at least for me).

OK that’s weird. Not sure how that could make a difference but at this point I would suggest a GH issue so we can directly ping @mateusz about it.

OK, thanks, will raise a GH issue.

GitHub issue here includes successful workaround from @mateusz.

A further error has occurred when running the custom model in the notebook. Code below, with the correction added to address the issue solved above. Needs to be in packages organised as before…

cctest

Custom.py:

from bokeh.plotting import Figure
from bokeh.layouts import Column


class Custom(Column):

    __view_model__ = 'Column'
    __subtype__ = 'Custom'
    __view_module__ = '__main__'

    def __init__(self):

        super().__init__()

        plot = Figure(plot_height=500, plot_width=500)
        plot.circle([1, 2, 3], [3, 2, 1], size=20)

        self.children.append(plot)

example.py:

from bokeh.plotting import show
from cctest.Custom import Custom

custom = Custom()
show(custom)

(The init.py under both cctest and Examples is empty.)

example.py runs successfully at the command line. However if I run the following code in the notebook:

from bokeh.plotting import show
from bokeh.io import output_notebook
from cctest.Custom import Custom
output_notebook()
custom = Custom()
show(custom)

I get the error:
AttributeError: module ‘main’ has no attribute ‘file

Note that if I don’t have a heirarchy, i.e. enter the following in the notebook:

from bokeh.plotting import Figure, show
from bokeh.layouts import Column
from bokeh.io import output_notebook
output_notebook()

class Custom(Column):

    __view_model__ = 'Column'
    __subtype__ = 'Custom'
    __view_module__ = '__main__'

    def __init__(self):

        super().__init__()

        plot = Figure(plot_height=500, plot_width=500)
        plot.circle([1, 2, 3], [3, 2, 1], size=20)

        self.children.append(plot)

custom = Custom()
show(custom)

then it works fine.

Any help appreciated!

Thanks,
Marcus.

Above comment added to GitHub issue here.