Difficulty Extracting JavaScript Variable Values to Python

I am encountering an issue with a Bokeh application where I’m trying to capture the selected column index from a DataTable using a combination of Python and JavaScript callbacks. The application is intended to print the selected column index in the Python environment when a column is clicked in the DataTable. The callback function is able to detect the index of the selected cell column correctly, but I am unable to synchronize this index back to a python variable.

Steps to Reproduce:

  1. Run the provided Bokeh application code, which includes defining a DataTable with sample data and setting up callbacks to capture the selected column index.
  2. Select any cell in the DataTable.
  3. Press Button.

Expected Behavior:

The expected behavior is that the selected column index should be printed in the Python environment each time a cell is clicked in the DataTable or the button is pressed.

Actual Behavior:
The row and column index of the selected cell is correctly captured in the CustomJS callback and the values are printed in the browser console but not in the python environment.
The value of the index in the array index_source['col_index'] is never changed from its initial value (you can check this by pressing the button).

from bokeh.models import DataTable, ColumnDataSource, TableColumn, CustomJS, Button
from bokeh.plotting import curdoc

# Sample data
data = dict(
    A=[1, 2, 3, 4, 5],
    B=[6, 7, 8, 9, 10],
    C=[11, 12, 13, 14, 15]
)

# Create a ColumnDataSource
source = ColumnDataSource(data)

# Create a ColumnDataSource to store the column index
index_source = ColumnDataSource(data=dict(col_index=[0]))

# Create columns for the DataTable
columns = [TableColumn(field=col, title=col) for col in data.keys()]

# Define the JavaScript callback
callback = CustomJS(args=dict(source=source, idx=index_source), code="""
    var row = source.selected.indices[0];
    var children = document.querySelector('.bk-DataTable').shadowRoot.querySelector('.bk-data-table').querySelector('div.slick-pane.slick-pane-top.slick-pane-left').querySelector('div.slick-viewport.slick-viewport-top.slick-viewport-left').children[0].querySelector('.active').children;

    if (children) {
        var index = -1;

        for (var i = 0; i < children.length; i++) {
            // Check if the current element's class ends with "selected.active"
            if (children[i].classList.contains('selected') && children[i].classList.contains('active')) {
                index = i;
                break; // Exit the loop once the element is found
            }
        }

        if (index !== -1) {
            // Update the col_index property
            console.log("Selected Column Index:", index - 1, 'Row:', row);
            idx.data['col_index'] = [index - 1];
            idx.change.emit();
        }
    }
""")

def python_callback(attr, old, new):
    print(f"column index: {index_source.data['col_index']}")

def button_click_callback(event):  # Notice the 'event' argument
    print(f"column index: {index_source.data['col_index']}")

# Create a button to trigger print the last selected column index
button = Button(label="Print last selected column index")
button.on_click(button_click_callback)

# Define a function to reset the selected indices
def reset_indices(attr, old, new):
    source.selected.update(indices=[])

# Attach the Python callback to the ColumnDataSource storing the column index
index_source.on_change('data', python_callback)

# Attach the JavaScript callback to the ColumnDataSource of the table
source.selected.js_on_change('indices', callback)
source.selected.on_change('indices', reset_indices)

data_table = DataTable(source=source, columns=columns, width=800, height=300)

curdoc().add_root(data_table)
curdoc().add_root(button)

index_source.on_change('data', python_callback) will only trigger if source.data is completely re-assigned to, e.g. source.data = new_data. [1] You could re-work your code to do that, but I’d suggest defining a tiny DataModel is a better approach than using a whole CDS for this.


  1. And idx.change.emit() only related to events on the JS side, it will not cause anything to happen on the Python side. ↩︎