CheckboxGroup Simple Example

Hi, I am trying to see if there is a simple CheckBoxGroup example to toggle visibility over a subset of a ColumnDataSource.

For example:

days = ['Mon','Tues','Wed']
source = ColumnDataSource({
    'x': [1,2,3],
    'y': [4,5,2],
    'day': days
})
p=figure()
scatter = p.scatter(source=source)

cbg = CheckboxGroup(labels=days, active=[0])
layout = row(cbg, p)
show(layout)

I want to connect the CheckboxGroup to the scatter.
The only way I can think of given the gallery/doc examples is to do something like the below pseudocode:

Add a visibility column to the Source

cbg.js_on_change('active', callback=CustomJS(args=dict(source=source), code="""
      selected=active checkboxes
      iterate over selected days, 
         for each day iterate over source.data and toggle each glyph's visibility col
      refresh the figure
"""))

I’m sure bokeh has better ways of handling this and I was wondering what the standard/clean way of doing this is (perhaps using a CDSView for each day of the week?)

Note, I did consider looping over the days to render the scatter (so one scatter call for each day of the week), but then I would lose the shared ColumnDataSource.

If you just needed one visible at a time, then I’d suggest a CDSView with a GroupFilter. But those only support filtering on a single group at at time [1] so you’d have to resort to some pretty clunky things with multiple GroupFilters in a UnionFilter or something. Your best bet here might be CustomJSFilter which should return the indices of the rows you want to be visible, e.g a simple example:

code = '''
const indices = []
for (let i = 0; i <= source.data['colname'].length; i++) {
    if (source.data['colname'][i] == 'some_value') {
        indices.push(i)
    }
}
return indices
'''

  1. I’d actually suggest you open a GitHub Issue to request generalizing GroupFilter to also accept a list of groups. That seems like it would be reasonably straightforward to add support for. ↩︎

@Bryan my workaround for this has been to use a basic IndexFilter and CustomJS to update its indices → this setup I’ve found to be the most robust when you need to check multiple widget states (like filtering multiple groups etc) to drive the filter:

from bokeh.models import ColumnDataSource, CustomJS, IndexFilter,CheckboxGroup, IndexFilter, CDSView
from bokeh.plotting import figure, show
from bokeh.layouts import row

days = ['Mon','Tues','Wed']
source = ColumnDataSource({
    'x': [1,2,3],
    'y': [4,5,2],
    'day': days,
})

p=figure()

filt = IndexFilter(indices=list(range(len(days)))) #start w all active

scatter = p.scatter(x='x',y='y',source=source,view=CDSView(filter=filt))

cbg = CheckboxGroup(labels=days, active=[0,1,2])

cb = CustomJS(args=dict(cbg=cbg,source=source,filt=filt)
              ,code='''
              var active_labels = cbg.active.map(x=>cbg.labels[x])
              var indices = []
              for (var i=0;i<source.data['day'].length;i++){
                      if (active_labels.includes(source.data['day'][i])){
                              indices.push(i)
                              }
                      }
              filt.indices = indices
              source.change.emit()
              ''')
cbg.js_on_change('active',cb)
layout = row(cbg, p)
show(layout)
2 Likes

That actually seems better, it just occurs to me I am not sure what all triggers a CustomJSFilter re-compute, you might need an explicit source.change.emit() on a checkbox callback, but as long as you have a checkbox callback anyway, might as well do everything in one place.

exactly, I gave up on trying to get CustomJSFilter to listen/re-execute on user actions :slight_smile:

1 Like

Could be worth an issue, not having options to manually or automatically trigger a re-compute seems like an oversight.

Thanks for the feedback!
So I guess since there isn’t a simple way to do this, my initial solution might be the best. It’s very similar to the one @Bryan posted.
Sharing this in case anyone else wants a simple, yet inelegant solution
The way I did it as follows (partial pseudo-code):

views = [CDSView(filter = GroupFilter(column_name=filter_col_name, group=day)) for day in days_of_week]

scatter_renderers =[]
for view in views:
       scatter = p.scatter(source = source, view = view)
       scatter_renderers.append(scatter)

checkbox_group = CheckboxGroup(labels=days_of_week, active=list(range(len(dates.days_of_week))))
callback = CustomJS(args=dict(scatter_renderers = scatter_renderers), code="""
        const selected = cb_obj.active;
        for(let i=0, j=0; i<scatter_renderers.length; i++){
            scatter_renderers[i].visible = (i == selected[j]);
            if(i == selected[j]) j++;
        }
    """)
checkbox_group.js_on_change('active', callback)
  1. I made a CDSView with a GroupFilter for each Checkbox (day of the week).

  2. I rendered the scatter one by one using each CDSView.

  3. JS to take the index of each selected Checkbox and turn on the visibility of the appropriate scatter (here is where I differ from @Bryan 's solution…not sure which is “better”)

Please note, this is not a particularly robust solution as I assume:

  • The views are in the same order as the checkboxes and the scatters (I use the same variables in my example above, but that’s not a good assumption to make in more complex solutions)
  • All the checkboxes are turned on at the start (the JS code only triggers when a checkbox is clicked by the user)
  • There aren’t other widgets which toggle the checkboxes (again, the JS code above only runs when a user clicks it…if something else were to change the state of the checkboxes this solution wouldn’t capture it)

here is where I differ from @Bryan 's solution…not sure which is “better”

Either seems fine, I thought of a GroupFilter because it could avoid the explicit index computation, but that’s not really avoidable at present if multiple groups need to be considered.

The views are in the same order as the checkboxes and the scatters

Just for the sake of defensive programming, this assumption is probably worth removing, one way or another.

All the checkboxes are turned on at the start

You can coordinate this yourself, so it is as robust as you make it :slight_smile: It would be good to derive the initial state of checkboxes and of the view indices from one source of truth.

There aren’t other widgets which toggle the checkboxes

If you need to adjust the view based on multiple widgets then you probably want to have a single CustomJS that all the widgets trigger on, that looks at all the widgets at once and sets the indices based on the total state of those widgets. Similar to this example:

slider — Bokeh 3.4.0 Documentation

Thanks for your comments!

I just wanted to keep the example as simple as possible (similar to the Gallery examples) so that if someone were to have trouble with checkboxes in the future, they could stumble upon this thread and get a very quick working example to understand. I listed the assumptions so they could build robustness if needed.

1 Like