1

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.

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, that gets me closer, but the result has two issues. The first is that the initial plot (when no selection has been made yet) appears but is blank, and has a bunch of tiny text crammed into the upper left corner of the grid. The second problem is that the x_range value does not get updated, so I still have ticks on the x axis for items that are filtered out of the plot.
You need to set the y_range explicitly (I used y_range=(0,1) when testing) Bokeh cannot autorange when there is no data to autorange, and you have it set up to start with an empty view. Alternatively you can choose to one of the categories to start with so that there is data present at the start. The range issue is a separate question. Please open a new topic (here on on the Discourse)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.