Skip to content

Instantly share code, notes, and snippets.

@pbugnion
Last active September 28, 2024 11:00
Show Gist options
  • Save pbugnion/5bb7878ff212a0116f0f1fbc9f431a5c to your computer and use it in GitHub Desktop.
Save pbugnion/5bb7878ff212a0116f0f1fbc9f431a5c to your computer and use it in GitHub Desktop.
Multiple checkbox selection with searching with ipywidgets

Multiple selection with checkboxes and search field

Often, you want the user to choose n options (where n is small-ish) from a very large (hundreds or thousands) number of possibilities. Good UX around this dictates that the user should be able to search for the options they want.

This gist puts together a minimal example of binding a search field with multiple checkboxes using ipywidgets.

Usage

widget = multi_checkbox_widget(['hello', 'world'])
widget # Display the widget

To get the selected options:

selected_options = [widget.description for widget in w.children[1].children if widget.value]

Possible improvements

  • This is in need of some layout/CSS TLC.

  • At the moment, we just use difflib from the standard library for searching through the options. It mostly works, but I don't think it's what difflib is supposed to be used as. Using a different search algorithm might lead to better results.

import difflib
import random
import requests
import ipywidgets as widgets
def multi_checkbox_widget(descriptions):
""" Widget with a search field and lots of checkboxes """
search_widget = widgets.Text()
options_dict = {description: widgets.Checkbox(description=description, value=False) for description in descriptions}
options = [options_dict[description] for description in descriptions]
options_widget = widgets.VBox(options, layout={'overflow': 'scroll'})
multi_select = widgets.VBox([search_widget, options_widget])
# Wire the search field to the checkboxes
def on_text_change(change):
search_input = change['new']
if search_input == '':
# Reset search field
new_options = [options_dict[description] for description in descriptions]
else:
# Filter by search field using difflib.
close_matches = difflib.get_close_matches(search_input, descriptions, cutoff=0.0)
new_options = [options_dict[description] for description in close_matches]
options_widget.children = new_options
search_widget.observe(on_text_change, names='value')
return multi_select
# Example of using the widget
# Get lots of words for our options
words_url = 'https://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain'
response = requests.get(words_url)
response.raise_for_status()
words = response.text
words = set([word.lower() for word in words.splitlines()])
descriptions = random.sample(words, 100)
multi_checkbox_widget(descriptions)
@slamer59
Copy link

slamer59 commented Dec 3, 2018

Selected options are obtained with:

selected_options = [w.description for w in widget.children[1].children if w.value]
selected_options

@Joao-Martins-farfetch
Copy link

Great work! Thanks!

@ragrawal
Copy link

ragrawal commented May 1, 2019

it doesn't work with interact function. Can you please confirm

@seabarron
Copy link

How do you use this with interact/interactive? I understand you are creating a Vbox but cannot seem to integrate this. Thanks

@enryH
Copy link

enryH commented Feb 11, 2020

Thanks a lot! Do you see a possibility adding layout={'flex-flow': 'flex-wrap'} instead of {'overflow': 'scroll'}? Currently I just get a long list using your implementation and nothing to scroll.

Flex-Flow-Reference

@enryH
Copy link

enryH commented Feb 13, 2020

Okay, thanks to ac24 I figured it out. One needs to pass the option as an Layout instance:

 options_widget = widgets.VBox(options, layout=widgets.Layout(overflow='scroll' )) # alternative: flex_flow='row wrap'

@lucindat
Copy link

lucindat commented Mar 9, 2020

this code:
selected_options = [w.description for w in widget.children[1].children if w.value]
selected_options

does not seems to work.
what is exactly widget.children[1].children?

thanks

@enryH
Copy link

enryH commented Mar 10, 2020

If I see this correctly you need to assign the multibox to widget:

widget = multi_checkbox_widget(descriptions)
selected_options = [w.description for w in widget.children[1].children if w.value]
selected_options

@lior93
Copy link

lior93 commented May 4, 2020

widget = multi_checkbox_widget(descriptions)
widget # will display the widget

selected_options = [widget.description for widget in widget.children[1].children if widget.value]
selected_options

@MattJBritton
Copy link

@ragrawal @seabarron See below for a (much belated) solution to your problem, which I had as well.

@MattJBritton
Copy link

Like some of the commenters above, I was unable to use this excellent code with interact/interactive, which is my main use case for using Jupyter Widgets. However, I was able to find a fix. You can find my solution in a fork of this project.

It is impossible (to my knowledge) to use interact/interactive because those functions cannot accept a VBox as a parameter, and the output of multi_checkbox_widget() is a VBox. However, you can create the individual checkbox widgets outside of the multi_checkbox_widget() function, pass them in, and then use the Jupyter Widgets interactive_output() method instead. This method allows you to pass widgets (representing interactive parameter controls) to a function without displaying them, and then display inside of a container later.

Because the number of checkboxes is arbitrary, our function must then take **args as its only parameter. I was unfortunately unable to pass ipywidgets.fixed() to the function, so I had to use global variables to access a data set in the function. This is not a problem in the trivial example in the gist, but was a consideration for my use case.

I also added a small UX improvement by automatically sorting any selected items to the top of the list.

Lastly, note that I use @output_widget.capture() as a function decorator for my on_text_change() function. This was not present in the original, and is only required for correct functioning in Jupyter Lab, not Jupyter Notebook. See here for more info.

I hope this is helpful for those who stumble across this page in the future!

@agg437
Copy link

agg437 commented Nov 7, 2020

Hey! @pbugnion
I don't know if it is too late, but I think there is a simple way to improve the search algorithm. It would be to rewrite the close_matches
definition as it follows:
close_matches = sorted(list(filter(lambda x: search_input in x, descriptions)))
With that code you manage to extract all the words that contain the search_input, and besides, words are showed in alphabetical order.
I hope it helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment