I am trying to display a bar chart and have the contents filtered by a Select object. As simple as it seems, I have not been able to find a working solution after two days of looking. I need to do this with CustomJS, not bokeh server.
Here is the code I am trying, but when I run it nothing is displayed, not even an empty plot.
import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CustomJS, CustomJSFilter, CDSView, Select, IndexFilter
from bokeh.io import show, output_notebook
from bokeh.layouts import column
output_notebook()
df = pd.DataFrame({'Subject': ['Math', 'Math', 'Math', 'Science', 'Science', 'Science'],
'Class': ['Algebra', 'Calculus', 'Trigonometry', 'Biology', 'Chemistry', 'Physics'],
'FailRate': [0.05, 0.16, 0.31, 0.12, 0.20, 0.08]})
src = ColumnDataSource(df)
subj_list = sorted(list(set(src.data['Subject'])))
callback = CustomJS(args=dict(src=src), code='''
src.change.emit();
''')
js_filter = CustomJSFilter(code='''
var indices = [];
for (var i = 0; i < src.get_length(); i++){
if (src.data['Subject'][i] == select.value){
indices.push(true);
} else {
indices.push(false);
}
}
return indices;
''')
options = ['Please select...'] + subj_list
select = Select(title='Subject Selection', value=options[0], options=options)
select.js_on_change('value', callback)
view = CDSView(source=src, filters=[js_filter])
class_list = sorted(list(src.data['Class']))
p = figure(x_range=class_list, plot_height=400, plot_width=400)
p.vbar('Class', top='FailRate', width=0.9, source=src, view=view)
show(column(select, p))
As far as I can tell, part of the problem involves this line (or the 'view' variable):
p.vbar('Class', top='FailRate', width=0.9, source=src, view=view)
Before I added 'view=view' to the above line, I was at least getting a select box and plot displayed, even though the interaction was not working.
Bokeh can magically transport your Python objects across runtimes to appear in your browsers as JavaScript objects, but there are limits to the magic. You have to tell Bokeh exactly which objects to transport, by providing the args
parameter to the CustomJS
object. Also, this is simpler if you use a callback to update the indices in an IndexFilter
. Then you only need one callback:
filter = IndexFilter(indices=[])
callback = CustomJS(args=dict(src=src, filter=filter), code='''
const indices = []
for (var i = 0; i < src.get_length(); i++) {
console.log(i, src.data['Subject'][i], cb_obj.value)
if (src.data['Subject'][i] == cb_obj.value) {
indices.push(i)
}
}
filter.indices = indices
src.change.emit()
''')
select.js_on_change('value', callback)
view = CDSView(source=src, filters=[filter])
One last note: instead of passing and using select
I used the implicit cb_obj
variable that is always the object that triggered the change. (There may be a bug with using a passed-in select)
EDIT: No, no bug. The issue was that the Select
was defined after the callback, and since things were in the notebook, it was using the value from the previous cell execution. Unfortunately the intrinsic nature of the notebook makes issues with corrupted state very easy to achieve, in general. If you define the Select
first, then the code works as expected with select
instead of cb_obj
:
options = ['Please select...'] + subj_list
select = Select(title='Subject Selection', value=options[0], options=options)
cb = CustomJS(args=dict(select=select, src=src, filter=filter), ...)
even across repeated cell invocations. Note there is nothing wrong with cb_obj
but many people prefer to pass (and name) the widgets explicitly.
As a final note I will mention that learning to check the browser's JS console is indispensable for debugging CustomJS
type callbacks. The errors from your code showed up there immediately.