Search code examples
pythonplotbokeh

Bokeh how to get GlyphRenderer for Annotation


With Bokeh, how do I get a handle to the Renderer (or GlyphRenderer) for an Annotation? Is this possible?

I would like to be able to toggle a Band (which is an Annotation) on and off with an interactive legend, so I need to be able to pass a list of Renderers to the LegendItem constructor.

This code:

maxline = fig.line(x='Date', y=stn_max, line_width=0.5, legend=stn_max, name="{}_line".format(stn_max), color=stn_color, alpha=0.75, source=source)
minline = fig.line(x='Date', y=stn_min, line_width=0.5, legend=stn_min, name="{}_line".format(stn_min), color=stn_color, alpha=0.75, source=source)
band = bkm.Band(base='Date', lower=stn_min, upper=stn_max, fill_alpha=0.50, line_width=0.5, fill_color=stn_color, source=source)
bkm.LegendItem(label=stn, renderers=[maxline, minline, band])

Produces this error

...
ValueError: expected an element of List(Instance(GlyphRenderer)), got seq with invalid items [Band(id='1091', ...)]

Solution

  • For LegendItem only instances of GlyphRenderer can be passed to its renderers attribute and Band is not based on GlyphRenderer so it gives error. In the code below the Band visibility is being toggled by means of a callback:

    from bokeh.plotting import figure, show
    from bokeh.models import Band, ColumnDataSource, Legend, LegendItem, CustomJS
    import pandas as pd
    import numpy as np
    
    x = np.random.random(2500) * 140 - 20
    y = np.random.normal(size = 2500) * 2 + 5
    df = pd.DataFrame(data = dict(x = x, y = y)).sort_values(by = "x")
    
    sem = lambda x: x.std() / np.sqrt(x.size)
    df2 = df.y.rolling(window = 100).agg({"y_mean": np.mean, "y_std": np.std, "y_sem": sem})
    df2 = df2.fillna(method = 'bfill')
    df = pd.concat([df, df2], axis = 1)
    df['lower'] = df.y_mean - df.y_std
    df['upper'] = df.y_mean + df.y_std
    
    source = ColumnDataSource(df.reset_index())
    p = figure(tools = "pan,wheel_zoom,box_zoom,reset,save")
    scatter = p.scatter(x = 'x', y = 'y', line_color = None, fill_alpha = 0.3, size = 5, source = source)
    band = Band(base = 'x', lower = 'lower', upper = 'upper', source = source)
    p.add_layout(band)
    p.title.text = "Rolling Standard Deviation"
    p.xaxis.axis_label = 'X'
    p.yaxis.axis_label = 'Y'
    
    callback = CustomJS(args = dict(band = band), code = """
    if (band.visible == false)
        band.visible = true;
    else
        band.visible = false; """)
    
    legend = Legend(items = [ LegendItem(label = "x", renderers = [scatter, band.source.selection_policy]) ])
    legend.click_policy = 'hide'
    scatter.js_on_change('visible', callback)
    p.add_layout(legend)
    show(p)
    

    Result:

    enter image description here