bokeh / bokeh

Interactive Data Visualization in the browser, from Python
https://bokeh.org
BSD 3-Clause "New" or "Revised" License
19.22k stars 4.18k forks source link

[BUG] Custom ColumnDataSource selections seem to be overridden by some mechanism in DataTable #14040

Open bjfar opened 2 weeks ago

bjfar commented 2 weeks ago

Software versions

Python version : 3.10.14 | packaged by conda-forge | (main, Mar 20 2024, 12:45:18) [GCC 12.3.0] IPython version : 8.21.0 Tornado version : 6.4.1 Bokeh version : 3.5.1 BokehJS static path : /home/benf/micromamba/envs/testenv/lib/python3.10/site-packages/bokeh/server/static node.js version : v22.6.0 npm version : 10.8.2 jupyter_bokeh version : (not installed) Operating system : Linux-6.8.0-40-generic-x86_64-with-glibc2.35

Browser name and version

Firefox 129.0.1

Jupyter notebook / Jupyter Lab version

Jupyter notebook 6.4.12

Expected behavior

I have created a simple (x, y, t) dataset, and I am viewing (x, y) data in both a scatter plot and a data table using a CDSView that selects t-slices of the data using a slider and some customJS. I select points in the DataTable and they highlight as expected in the scatter plot. My clear button also clears the selection as expected.

When selecting at one t-slice, moving the slider, then clicking the "clear" button, I expect the selection indices in the underlying ColumnDataSource to be completely cleared, and for both the scatter plot and data table to reflect that.

Observed behavior

When selecting at one t-slice, moving the slider, then clicking the "clear" button, the selection is not cleared. Upon sliding back to the view where the selection was made, the old selection reappears.

Example code

import numpy as np
import pandas as pd
from bokeh.io import show, push_notebook
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView, CustomJS, CustomJSFilter, Slider, TableColumn, DataTable, SelectEditor, Button

x = np.arange(0, 10, 0.1)
dfs = []
tstep = 1
ts = range(0, 100, tstep)
for t in ts:
    y = x**(t/50.)
    dfs.append(pd.DataFrame({"x": x, "y": y, "t": t}))

df = pd.concat(dfs)
cds = ColumnDataSource(df)

t_slider = Slider(start=ts[0], end=ts[-1], step=tstep, value=0)

# Callback to notify downstream objects of data change
change_callback = CustomJS(args=dict(source=cds), code="""
    source.change.emit();
""")
t_slider.js_on_change('value', change_callback)

# JS filter to select data rows matching t value on slider
js_filter = CustomJSFilter(args=dict(slider=t_slider), code="""
const indices = [];

// iterate through rows of data source and see if each satisfies some constraint
for (let i = 0; i < source.get_length(); i++){
    if (source.data['t'][i] == slider.value){
        indices.push(true);
    } else {
        indices.push(false);
    }
}
return indices;
""")

# Use the filter in a view
view = CDSView(filter=js_filter)

# Add table to use for selecting data
columns = [
    TableColumn(field="x", title="x", editor=SelectEditor()),
    TableColumn(field="y", title="y", editor=SelectEditor()),
]
data_table = DataTable(source=cds, columns=columns, selectable="checkbox", width=800, view=view)

p = figure(x_range=(0,10), y_range=(0,100))
p.scatter(x='x', y='y', source=cds, view=view)

# Button to clear selection
clear_button = Button(label="Clear selection", button_type="success")
custom_js_button = CustomJS(args=dict(source=cds), code="""
source.selected.indices = [];
""")
clear_button.js_on_event("button_click", custom_js_button)

layout = column(p, t_slider, data_table, clear_button)
show(layout)

Stack traceback or browser console output

No response

Screenshots

Screenshot 2024-08-26 at 11-19-41 Untitled - Jupyter Notebook

Selected points at t-slider value 79.

image

Press "clear" at t-slider value 80.

Slide back to value 79. Selection is still there. However the "clear" does work for data in the current "view" of the DataTable.

bryevdv commented 2 weeks ago

My offhand speculation is that clearing the CDS selection does not clear the table selection for rows that are off-screen. And then one of these two codepaths below gets invoked when the table viewport brings the previous selection back into view, so the table suddenly thinks there is a selection to update, and it happens to "win" by going "last".

https://github.com/bokeh/bokeh/blob/d0f895a52dbb8ead9ed48f4af0eabdfac52975d8/bokehjs/src/lib/models/widgets/tables/data_table.ts#L436-L441

https://github.com/bokeh/bokeh/blob/d0f895a52dbb8ead9ed48f4af0eabdfac52975d8/bokehjs/src/lib/models/widgets/tables/data_table.ts#L481-L485

Assuming so, a possible solution would be to more thoroughly clear the table's internal selection when the CDS selection is cleared.