How to maintain selection when filtering?

I’m trying to support three things: scatter plot, table with only lasso selection displayed, and range slider for filtering. Here’s my attempt:

from random import random

from bokeh.layouts import layout
from bokeh.models import ColumnDataSource, CustomJS, RangeSlider
from bokeh.plotting import figure, curdoc

x = [random() for x in range(1_000)]
y = [random() for y in range(1_000)]

s1 = ColumnDataSource(data=dict(x=x, y=y))
p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")
p1.circle('x', 'y', source=s1, alpha=0.6)

s2 = ColumnDataSource(data=dict(x=[], y=[]))

from bokeh.models import DataTable, TableColumn

columns = [
    TableColumn(field="x", title='x'),
    TableColumn(field="y", title='y'),
]
data_table = DataTable(source=s2, columns=columns)

s1.selected.js_on_change('indices', CustomJS(args=dict(s1=s1, s2=s2), code="""
        var inds = cb_obj.indices;
        var d1 = s1.data;
        var d2 = s2.data;
        d2['x'] = []
        d2['y'] = []
        for (var i = 0; i < inds.length; i++) {
            d2['x'].push(d1['x'][inds[i]])
            d2['y'].push(d1['y'][inds[i]])
        }
        s2.change.emit();
    """)
)

x_slider = RangeSlider(title='X', start=0, end=1, value=(0, 1), step=0.01)

def update():
    min_x, max_x = x_slider.value
    indices = [i for i, e in enumerate(x) if min_x <= e <= max_x]
    s1.data = dict(
        x=[x[i] for i in indices],
        y=[y[i] for i in indices],
    )

x_slider.on_change('value', lambda attr, old, new: update())

layout = layout([p1, data_table], x_slider)
doc = curdoc()
doc.add_root(layout)

The lasso selection/table and range slider work well independently but not together. If I select data points with lasso then change the slider, the selected points in the scatter plot change. I’m replacing s1.data in update() but I probably also need to update the selection? I’m not really sure how selections work. Help appreciated!

The jumping selection is because you change the set of points but the s1.selected.indices stays the same, I think.

In any case, this is how I’d do it:

from random import random

from bokeh.layouts import layout
from bokeh.models import ColumnDataSource, CustomJS, RangeSlider, CDSView, CustomJSFilter
from bokeh.plotting import figure, curdoc

p1 = figure(plot_width=400, plot_height=400, tools="lasso_select", title="Select Here")

from bokeh.models import DataTable, TableColumn

ds = ColumnDataSource(data=dict(x=[random() for x in range(1_000)],
                                y=[random() for y in range(1_000)]))

x_slider = RangeSlider(title='X', start=0, end=1, value=(0, 1), step=0.01)
slider_filter = CustomJSFilter(args=dict(slider=x_slider),
                               # language=JavaScript
                               code="""
                                  const [min_x, max_x] = slider.value;
                                  return source.data['x'].reduce((acc, x, idx) => {
                                      if (x >= min_x && x <= max_x) acc.push(idx);
                                      return acc
                                  }, [])
                              """)
x_slider.js_on_change('value', CustomJS(args=dict(ds=ds), code="ds.change.emit();"))

selection_filter = CustomJSFilter(code="return source.selected.indices;")
ds.selected.js_on_change('indices', CustomJS(args=dict(ds=ds), code="ds.change.emit();"))

circle_view = CDSView(source=ds, filters=[slider_filter])
p1.circle('x', 'y', source=ds, view=circle_view, alpha=0.6)

table_view = CDSView(source=ds, filters=[slider_filter, selection_filter])
columns = [
    TableColumn(field="x", title='x'),
    TableColumn(field="y", title='y'),
]
data_table = DataTable(source=ds, columns=columns, view=table_view)

layout = layout([p1, data_table], x_slider)
doc = curdoc()
doc.add_root(layout)

BTW note that it also works without the server - you can generate a static standalone HTML page from it.