I achieved the very same thing by tying the renderer to a source (ColumnDataSource
) and view (CDSView
). I then assigned a CustomJS
callback to the legend item. This callback amends the view’s filters
property. I then triggered an update of the plot by emitting a change from the source (referenced by the renderer). The callback is in JS so it all runs very quickly in the browser.
A self-contained example follows.
from bokeh.io import output_notebook, show
from bokeh.models import CDSView, ColumnDataSource, CustomJS, IndexFilter, Legend, LegendItem
from bokeh.plotting import figure
# Load BokehJS library. Remove if not running in Jupyter.
output_notebook()
# Create a data source having data for both lines.
source = ColumnDataSource(
data={
'x': [0,1,2,3,4],
'y': [1,2,4,8,16],
},
)
# Create views for each line using some filters. Name the filters. This will come in handy later.
filter1 = IndexFilter(indices=source.data['x'][:2], name='filter1') # first two points as an example
view1 = CDSView(filters=[filter1], source=source)
filter2 = IndexFilter(indices=source.data['x'][2:], name='filter2') # remaining points
view2 = CDSView(filters=[filter2], source=source)
# Draw the line based on the data source and view.
p = figure()
r1 = p.line(x='x', y='y', source=source, view=view1, name='line1', muted_alpha=0.2)
r2 = p.line(x='x', y='y', source=source, view=view2, name='line2', muted_alpha=0.2)
# Manually construct a legend.
legend = Legend(
items=[
LegendItem(label='line1', renderers=[r1]),
LegendItem(label='line2', renderers=[r2]),
],
location='bottom_right',
click_policy='mute', # or hide
)
p.add_layout(legend)
# For each renderer, create a callback to filter it in/out of view.
for r, view, idx_filter in zip([r1, r2], [view1, view2], [filter1, filter2]):
cb = CustomJS(
args=dict(source=source, view=view, idx_filter=idx_filter),
code="""
console.log(cb_obj);
var visible = cb_obj.visible;
// The renderer is currently visibile, but it's legend item was clicked, so the user has requested to make it invisible.
if (visible) {
const filters = view.filters;
// Remove filters matching the given filter's name property.
view.filters = filters.reduce(
(a, e, i) => (e.name != idx_filter.name) ? a.concat(e) : a, []
);
}
// The renderer is currently invisibile, but it's legend item was clicked, so the user has requested to make it visible.
else {
// Add the filter to the view.
view.filters.push(idx_filter);
}
source.change.emit();
""",
)
# Register the callback.
r.js_on_event('tap', cb) # or js_on_change('visible', cb)
show(p)
I’ve actually used this technique with much more complicated legend(s) on a scatter plot – it had a legend for the colors and a separate legend for the marker. Just putting this out there in case anyone stumbles on this with the same issue.