Search code examples
pythonselectsynchronizationwidgetbokeh

Python Bokeh - filter a table using two synchronised select widgets


I want to filter a table using two bokeh select widgets, see the code structure bellow. I defined two widgets, userm and locations. First, select the user with userm widget (should change the table and also 'locations' widget), and second, select the location with locations widgets (should change the table again).

My code works fine to filter the table based on users, but do not update the locations widgets and the table based on location selection. I'm not sure if it is possible to implement all in the same callback function. Any ideas? Thank you!

#Import libraries
from bokeh.io import output_notebook, show
from bokeh.layouts import widgetbox
from bokeh.models.widgets import Select, DataTable, TableColumn
from bokeh.models.sources import ColumnDataSource, CDSView
from bokeh.models import CustomJS, Select
import pandas as pd

output_notebook()

#Create the dataframe
df = pd.DataFrame({'Index': ['9', '10', '11', '12', '13'],
        'Size': ['250', '150', '283', '433', '183'],
        'X': ['751', '673', '542', '762', '624'],
        'Y': ['458', '316', '287', '303', '297'],
        'User': ['u1', 'u1', 'u2', 'u2', 'u2'],
        'Location': ['A', 'B', 'C', 'C', 'D']
        })

#Create widgets
userm = Select(title = "Select user:", options=list(set(df['User'])), 
               value=list(set(df['User']))[0])
locations = Select(title="Select location:", options=list(set(df['Location'])), 
                   value=list(set(df['Location']))[0])

#Create data source
source=ColumnDataSource(data=dict(User=df['User'], Location=df['Location']))
filteredSource = ColumnDataSource(data=dict(User=[],Location=[]))

#Create data table
columns = [TableColumn(field="User",title="User"),
           TableColumn(field="Location",title="Location",sortable=True)]
data_table=DataTable(source=filteredSource,columns=columns, width=400 )
data_table_unfiltered=DataTable(source=source,columns=columns, width=400 )

callback = CustomJS(args=dict(source=source,
                              filteredSource=filteredSource,
                              data_table=data_table), code="""
    var data = source.data;
    var f = cb_obj.value;
    var df2 = filteredSource.data;
    df2['User']=[]
    df2['Location']=[]
    locations=[]


    for(i = 0; i < data['User'].length;i++){

    if(data['User'][i]==f){

        df2['User'].push(data['User'][i])
        df2['Location'].push(data['Location'][i])
    }

    }

    filteredSource.change.emit()
    data_table.change.emit()

""")

userm.js_on_change('value', callback)
show(widgetbox(userm, locations, data_table))

Solution

  • It is possible to share a single CustomJS callback between widgets, but you cannot use cb_obj then. You have to pass the widgets explicitly.

    callback = CustomJS(args=dict(source=source,
                                  filteredSource=filteredSource,
                                  userm=userm, locations=locations),
                        code="""
        const data = source.data;
        const userm_value = userm.value;
        const locations_value = locations.value;
        const df2 = filteredSource.data;
        df2['User'] = [];
        df2['Location'] = [];
    
        for (let i = 0; i < data['User'].length; i++) {
            if (data['User'][i] === userm_value && data['Location'][i] === locations_value) {
                df2['User'].push(data['User'][i])
                df2['Location'].push(data['Location'][i])
            }
        }
    
        filteredSource.change.emit()
    """)
    
    userm.js_on_change('value', callback)
    locations.js_on_change('value', callback)