Updating Figure Legend Using Data Source

It sounds like what you want to do is

  1. dynamically add/remove columns from the CDS, and
  2. have those additions reflected in the glyphs and the legend.

Similar questions have been asked before on the Discourse, and there’s not a perfectly straightforward way to do this. Re adding/removing columns, I think the most relevant post is here; for the legend, a possible approach is given here.

The basic ideas from these posts are:

  1. Rather than adding/removing columns from your CDS, a better approach is to have the data and glyphs always exist, but toggle the visible attribute to be true/false based on the user’s input.
  2. There is not currently a great way to update the legend as you describe. The Legend will show all glyphs, whether they are visible or not. So, explicitly create a Legend with only the subset of glyphs you want to see on every update.

Here’s an example worked up from your code, to be run as a bokeh server. This all assumes that the maximum possible number of y-vectors your user can select is something manageable (you proposed 6, I arbitrarily chose 15 for my example). If the number of y-values is very high, like 1000, then this might not be a great solution at scale. But see what you think.

from bokeh.io import curdoc
import numpy as np
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Slider, Legend, LegendItem
from bokeh.layouts import column


dictxy = {"x_vector": np.arange(0, 10, 1)}

y_max = 15  # I arbitrarily chose 15 as the max number of y arrays

# create arrays y_1 ... y_n, which are scaled versions of the x array
for i in range(1, y_max + 1):
    dictxy["y_" + str(i)] = dictxy["x_vector"] * (i + 1)

label = ['Times' + str(n) for n in range(2, y_max + 2)]
CDSxy = ColumnDataSource(dictxy)

# slider for user interactivity. Arbitrarily setting default value to 5.
y_slider = Slider(start=1, end=y_max, value=5, step=1, title="y value")


# This is the bokeh server callback that will be run every time the user changes the slider value.
def update_plot(attr, old, new):
    # clear out existing legend and start over; we will entirely redraw
    p.legend.items = []
    for i in range(1, y_max):
        # if line number is less than new value from the slider, we should see it and it should be in the legend.
        # if not, set to invisible, and do not add to legend.
        if i <= new:
            lines[i-1].visible = True
            p.legend.items.append(LegendItem(label=label[i-1], renderers=[lines[i-1]]))
        else:
            lines[i-1].visible = False


y_slider.on_change("value", update_plot)

p = figure()

# Add all our line renderers. We'll keep them in a list so we can easily access them later.
lines = []
for i in range(1, y_max + 1):
    if i <= y_slider.value:
        visible = True
    else:
        visible = False
    lines.append(p.line(x='x_vector', y='y_' + str(i), source=CDSxy, visible=visible))

# Every line we just drew would go into the figure's Legend by default. We want only visible lines in the Legend.
# Start with an empty legend and add only what we want:
custom_legend = Legend()
for i in range(1, y_max + 1):
    if i <= y_slider.value:
        custom_legend.items.append(LegendItem(label=label[i-1], renderers=[lines[i-1]]))
p.add_layout(custom_legend)

col = column(y_slider, p)

doc = curdoc()
doc.add_root(col)