DataTable is not updating with multichoice widget as expected

Hello all,

I’m trying to show a data table linked with multi-choice widget. The goal is to only show the rows that matches the multi-choice. I’m not sure what I’m missing, it should work but it doesn’t. The selection doesn’t change the table.

code below:


from bokeh.models import ColumnDataSource, DataTable, TableColumn, CustomJS, MultiChoice
from bokeh.plotting import figure, show
import pandas as pd
import numpy as np




original_data = dict(species = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','C','D','E','A','A'],
length = [2, 10, 20, 40, 60, 80, 70, 50, 15, 36, 76, 74, 72, 44, 36, 18, 40, 64, 40, 64, 40, 30, 120],
weight = [2, 100, 150, 200, 420, 700, 600, 300, 200, 200, 620, 610, 601, 610, 601, 80, 205, 80, 800, 700, 240, 160, 800],
set_number = ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '3', '4', '1', '5', '1'],
sex_id = [2, 0, 0, 0, 2, 1, 1, 0, 2, 1, 1, 1, 0, 1, 1, 0, 2, 1, 1, 2, 1, 0, 2])

df = pd.DataFrame(original_data)
field= [c for c in df.columns]
columns = []
for i in field:
    columns.append(TableColumn(field=i,title=i))
source = ColumnDataSource(df)
table_source = ColumnDataSource(df.iloc[0:0])
table = DataTable(columns=columns,source=table_source)

options = list(df.species.unique())
multi_choice = MultiChoice(value=[], options=options, title='species:')


callback = CustomJS(args=dict(source=source, table_source=table_source,multi_choice=multi_choice,table=table), code="""    
    var s1 = source.data;
    var s2 = table_source.data;
    var selected = multi_choice.value;
    var s2['species'] = [];
    var s2['length'] = [];
    var s2['weight'] = [];
    var s2['set_number'] = [];
    var s2['sex_id'] = [];

for (var i = 0; i < s1['species'].length; i++) {
        if (selected.indexOf(s1['species'][i]) >= 0)  {
            s2['species'].push(s1['species][i])
            s2['length'].push(s1['length'][i])
            s2['weight'].push(s1['weight'][i])
            s2['set_number'].push(s1['set_number'][i])
            s2['sex_id'].push(s1['sex_id'][i])



        }
    }
    table_source.change.emit();
    table.change.emit();
""")


multi_choice.js_on_change('value', callback)



show(row(table,multi_choice))

This:

does not seem like valid JavaScript code. I don’t think it is legal to have a var there since s2 already exists.[1] Are you seeing any errors reported in the browser’s JS console?


  1. And as an aside: in general you should prefer const over var unless there is a reason not to ↩︎

I have updated my callback code, I also changed the lookup condition, but now I’m seeing in the browser console the error:

[bokeh] data source has columns of inconsistent lengths

callback = CustomJS(args=dict(source=source, table_source=table_source,multi_choice=multi_choice,table=table), code="""    
    var s1 = source.data;
    var s2 = table_source.data;
    var selected = multi_choice.value;
    s2['species'] = [];
    s2['length'] = [];
    s2['weight'] = [];
    s2['set_number'] = [];
    s2['sex_id'] = [];

for (var i = 0; i < s1['species'].length; i++) {
    selected.forEach((elem) => {
        if (s1['species'][i].includes(elem)) {
            s2['species'].push(s1['species'][i]);
            s2['length'].push(s1['length'][i]);
            s2['weight'].push(s1['weight'][i]);
            s2['set_number'].push(s1['set_number'][i]);
            s2['sex_id'].push(s1['sex_id'][i]);



        }
    })
    }
    table_source.change.emit();
    table.change.emit();
""")

Right, all the columns in a CDS should always be the same length at all times. Rather than clearing out and updating the existing data dict in place, it would be much better to create a blank new new_data object, and put all the data in that instead. Then afterwards you can just do

table_source.data = new_data

The other advantage of doing it this way is that the emit calls will not be needed. Bokeh can automatically detect when a CDS .data is assigned to,

Thank you Bryan for your response. Unfortunately, it’s not working :sweat_smile: I’m getting the following error

Uncaught TypeError: new_data.length.push is not a function

which is extremely weird because I tried the same code in the JS console and it’s working :thinking:

callback = CustomJS(args=dict(source=source, table_source=table_source,multi_choice=multi_choice,table=table), code="""    
    var s1 = source.data;
    var selected = multi_choice.value;
    var new_data= [];
    new_data['species']=[];
    new_data['length']=[];
    new_data['weight']=[];
    new_data['set_number']=[];
    new_data['sex_id']=[];

    
function containsAny(source,target)
{
    var result = source.filter(function(item){ return target.indexOf(item) > -1});   
    return (result.length > 0);  
}

for (var i = 0; i < s1['species'].length; i++) {
        if (containsAny(selected,s1['species'][i])) {
            new_data['species'].push(s1['species'][i])
            new_data['length'].push(s1['length'][i])
            new_data['weight'].push(s1['weight'][i])
            new_data['set_number'].push(s1['set_number'][i])
            new_data['sex_id'].push(s1['sex_id'][i])



        }
    }
table_source.data = new_data
""")

I tried the following in the console and it’s working as expected:


var new_ = [];
new_['sp'] = ['A','B','C'];
new_['jh'] = ['1','0','AB'];
new_['oa'] = [27493,9837,3781];


var old = [];
old['sp'] = [];
old['jh'] = [];
old['oa'] = [];


selected = ['A','B','C']
function containsAny(source,target)
{
    var result = source.filter(function(item){ return target.indexOf(item) > -1});   
    return (result.length > 0);  
}

for (var i = 0; i < new_['sp'].length; i++) {
        if (containsAny(selected,new_['sp'][i])) {
            old['sp'].push(new_['sp'][i]);
            old['jh'].push(new_['jh'][i]);
            old['oa'].push(new_['oa'][i]);




        }
    }
console.log(old)

You have declared new_data as an array, but new_data should be a dict/object that you add columns to.

const new_data = {}

new_data["some_col"] = []

new_data["some_col"].push(new_item)
1 Like

works perfectly thank you so much for all the help.

Full working code below:


from bokeh.models import ColumnDataSource, DataTable, TableColumn, CustomJS, MultiChoice
from bokeh.plotting import figure, show
import pandas as pd
import numpy as np
from bokeh.layouts import row, column




original_data = dict(species = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','C','D','E','A','A'],
length = [2, 10, 20, 40, 60, 80, 70, 50, 15, 36, 76, 74, 72, 44, 36, 18, 40, 64, 40, 64, 40, 30, 120],
weight = [2, 100, 150, 200, 420, 700, 600, 300, 200, 200, 620, 610, 601, 610, 601, 80, 205, 80, 800, 700, 240, 160, 800],
set_number = ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '3', '4', '1', '5', '1'],
sex_id = [2, 0, 0, 0, 2, 1, 1, 0, 2, 1, 1, 1, 0, 1, 1, 0, 2, 1, 1, 2, 1, 0, 2])

df = pd.DataFrame(original_data)
field= [c for c in df.columns]
columns = []
for i in field:
    columns.append(TableColumn(field=i,title=i))
source = ColumnDataSource(df)
table_source = ColumnDataSource(df.iloc[0:0])
table = DataTable(columns=columns,source=table_source)

options = list(df.species.unique())
multi_choice = MultiChoice(value=[], options=options, title='species:')


callback = CustomJS(args=dict(source=source, table_source=table_source,multi_choice=multi_choice), code="""    
    var s1 = source.data;
    var selected = multi_choice.value;
    var new_data= {};
    new_data['species']=[];
    new_data['length']=[];
    new_data['weight']=[];
    new_data['set_number']=[];
    new_data['sex_id']=[];

    
function containsAny(source,target)
{
    var result = source.filter(function(item){ return target.indexOf(item) > -1});   
    return (result.length > 0);  
}

for (var i = 0; i < s1['species'].length; i++) {
        if (containsAny(selected,s1['species'][i])) {
            new_data['species'].push(s1['species'][i])
            new_data['length'].push(s1['length'][i])
            new_data['weight'].push(s1['weight'][i])
            new_data['set_number'].push(s1['set_number'][i])
            new_data['sex_id'].push(s1['sex_id'][i])



        }
    }
table_source.data = new_data
""")


multi_choice.js_on_change('value', callback)



show(row(table,multi_choice))
1 Like