Extending bokehs classes for a particular chart type/layout

Hi,

I’m trying to write some functions for my research that are reusable. Specifically I would like to implement a Bode plot. This is a stacked plot of two figures with some specific relationship between them. I’m struggling to figure out the best way to implement this in a way that’s compatible with the rest of bokeh’s functions.

What I would like to be able to do:

  1. Include my bode plot as an element in other layouts.
  2. display it using bokeh.io.show()
  3. anything else that you can do to a Column that doesn’t conflict with the set of things that make a bode plot what it is (linked x axes and log scaling mainly).

Based on the above, I started by trying to make a class that derives from bokeh.models.Column. This seems to have two problems however:

  1. It seems that I will have to implement custom javascript for whatever my model is. It feels like I shouldn’t need this, because what I want can already be manually created with existing models.
  2. I can specify that my class requires two figures, and then place them correctly in the Column, but I can’t figure out how to specify anything about those figures. I don’t want to create a generic container where figures are passed into the constructor (like a Column is), but instead something that’s more like the interface of a single bokeh.plotting.figure where you specify data, 3 columns, and visual parameters.

Is there any examples of something similar that I can look at for reference? or some some high level guidance about the best way to accomplish this.

The solution that I’m using right now is to create a new class not derived from any bokeh classes. This class then provides methods to construct a bokeh Column from data, and a method to return that Column so that it can be placed into a larger layout. This feels klunky for a bunch of reasons.

Thanks in advance!
~Max

The ability to fully synchronize between Python and JavaScript is a bedrock principle of Bokeh. All Python Bokeh models have to have a JavaScript counterpart, full stop. So if you want an actual BodePlot model, then you would need to create a custom extension to provide the corresponding JavaScript implementation.

Re-usable does not imply classes or objects, and splitting the steps up into steps you have to manually call yourself, just in order to have a class, seems superfluous. Personally, from what you have described, I would create a plain free function to return a Column containing the plots you require:

def bode(...) -> Column: 
    p1 = figure(y_axis_type="log")
    p2 = figure(y_axis_type="log", x_range=p1.x_range)

    # whatever else you need to do to define these plots here

    return column(p1, p2)

fig = bode(...)

show(fig)   # or put fig in another layout first, etc.

The bode function would take whatever arguments are required for you to fully construct the plots (you have not specified enough detail to say much more).

Thanks for the response!

I understand that having full parity between the python and Javascript object representations is necessary to do what Bokeh does so well. I’m essentially trying to create a higher level representation of bokeh objects on the python side that is then turned into the lower level objects for the browser. Bokeh doesn’t need to know that I consider these two plots to be one plot, it should just see them as the layout that they are.

As you suggested, I could accomplish this by writing a set of functions to create and modify a Column that my code thinks of as a single plot. I was leaning toward writing a class because there is also some data that would be useful to be shared between these functions, and a set of shared data and functions manipulating that data seems exactly what a class should be.

I was asking because it seemed like it would be convenient to be able to reuse all of the argument parsing and layout creation functions that bokeh has already created for plots. So I could just call like Column(figure(), bode.Plot()). Right now I can do this as Column(figure(), bode.Plot().layout), which is fine.

Here’s the class I have right now in a file bode.py

import bokeh.plotting
import bokeh.models
import bokeh.io
import bokeh.layouts
import bokeh.palettes

import itertools

class Plot():
    def __init__(self, palette = None, height = None, **kwargs):
        if palette is None:
            palette = bokeh.palettes.Category10[10]
        if height is None:
            height = 150

        self.colors = itertools.cycle(palette)
        
        self.figure_magnitude = bokeh.plotting.figure(x_axis_type = 'log', y_axis_type = 'log')
        self.figure_magnitude.xaxis.visible = False
        self.figure_magnitude.yaxis.axis_label = 'Magnitude (Ω)'
        
        self.figure_phase = bokeh.plotting.figure(x_range=self.figure_magnitude.x_range, x_axis_type = 'log')
        self.figure_phase.xaxis.axis_label = 'Frequency (Hz)'
        self.figure_phase.yaxis.axis_label = 'Phase Angle (°)'
        
        self.layout = bokeh.layouts.gridplot(children = [[self.figure_magnitude], [self.figure_phase]], 
                                             height = height)
    
    def add_trace(self, *, data, column_freq, column_phase, column_magnitude, line_color = None, **kwargs):
        
        if line_color is None:
            line_color = next(self.colors)
        
        self.figure_magnitude.line(source = data, 
                                   x = column_freq, y = column_magnitude, 
                                   line_color = line_color,
                                   **kwargs)
        
        #remove keywords that we don't want to apply to the lower plot
        for key in ['legend_label', 'legend_group']:
            kwargs.pop(key, None)
        
        self.figure_phase.line(source = data, 
                               x = column_freq, y = column_phase, 
                               line_color = line_color,
                               **kwargs)