I'm trying to create a widget filter (made up of TextInput
and MultiSelect
) that is replicated on two different Bokeh Tabs. The desired functionality is that filtering results should be preserved between tabs, regardless of which filter receives the text to filter off of.
The code below(it is working code) builds the Filter
widget which is instantiated as filter1
and filter2
. The callback is the update
function which does the actual filtering and updates the MultiSelect
part of the filter.
from bokeh.io import curdoc
from bokeh.layouts import column, widgetbox, row, layout, gridplot
from bokeh.models import Slider, Select, TextInput, MultiSelect
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
multiselect = None
input_box = None
def update(widget, attr, old, new):
print("df['fruits']: {}".format(list(df['fruits'])))
print("{} : {} changed: Old [ {} ] -> New [ {} ]".format(widget, attr, old, new))
if widget == 'input':
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
print("col_date: {}".format(col_data))
multiselect.update(options = sorted(list(col_data)))
def init():
global multiselect
multiselect = MultiSelect(title = 'multiselect',
name = 'multiselect',
value = [],
options = list(df["fruits"]))
multiselect.on_change('value', partial(update, multiselect.name))
global input_box
input_box = TextInput(title = 'input',
name ='input',
value='Enter you choice')
input_box.on_change('value', partial(update, input_box.name))
class Filter:
def __init__(self):
self.multiselect = multiselect
self.input_box = input_box
self.widget = widgetbox(self.input_box, self.multiselect)
init()
filter1 = Filter().widget
filter2 = Filter().widget
curdoc().add_root(row(filter1, filter2))
The code above produces/assembles the widget as shown here:
Also, the functionality of the two mirrored filters is as desired; when text is entered in one of the boxes, the results are displayed on both filters.
Now, and here is where I need help, I want the same filters with the same functionality but I need them in two different tabs; one filter in one tab and the other filter in the other tab.
The code used to build the two tabs structure is:
p1 = Panel(child = filter1, title = "Panel1")
p2 = Panel(child = filter2, title = "Panel2")
tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))
On the results side, the code preserves the desired functionality but filters are displayed on the same page. More than that, panels/tabs are not even being built.
Any idea what's missing? (If you want to play with the code it should work right off the bat if you have bokeh installed.)
I do not think your example should even build a document, both your textinputs and multiselect models have the same id, which may be why the display of tabs gets messed up.
My solution is similar to HYRY's, but with a more general function to share attributes using two different things:
model.properties_with_values()
Can be used with any bokeh model and returns a dictionary of all the attribute:value pairs of the model. It's mostly useful in ipython to explore bokeh objects and debug
Document.select({'type':model_type})
Generator of all the widgets of the desired type in the document
Then I just filter out the widgets that do not share the same tags as the input widget, which would avoid "syncing" other inputs/multiselect not generated with box_maker(). I use tags because different models cannot have the same name.
When you change a TextInput value, it will change the associated Multiselect in the update function, but it will also change all the other TextInputs and trigger their update in the same way too. So each Input triggers update once and changes the options of their respective multiselect (and not multiplte times each because it's a "on_change" callback, if you give the same value for the new input it does not trigger).
For the Multiselect the first trigger of update will do the job, but since it changed the values of the other Multiselect it still triggers as many times as there are Multiselect widgets.
from bokeh.io import curdoc
from bokeh.layouts import widgetbox
from bokeh.models import TextInput, MultiSelect
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
def sync_attr(widget):
prop = widget.properties_with_values() # dictionary of attr:val pairs of the input widget
for elem in curdoc().select({'type':type(widget)}): # loop over widgets of the same type
if (elem!=widget) and (elem.tags==widget.tags): # filter out input widget and those with different tags
for key in prop: # loop over attributes
setattr(elem,key,prop[key]) # copy input properties
def update(attr,old,new,widget,other_widget):
print("\ndf['fruits']: {}".format(list(df['fruits'])))
print("{} : {} changed: Old [ {} ] -> New [ {} ]".format(str(widget),attr, old, new))
if type(widget)==TextInput:
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
print("col_date: {}".format(col_data))
other_widget.update(options = sorted(list(col_data)))
sync_attr(widget)
def box_maker():
multiselect = multiselect = MultiSelect(title = 'multiselect',tags=['in_box'],value = [],options = list(df["fruits"]))
input_box = TextInput(title = 'input',tags=['in_box'],value='Enter you choice')
multiselect.on_change('value',partial(update,widget=multiselect,other_widget=input_box))
input_box.on_change('value',partial(update,widget=input_box,other_widget=multiselect))
return widgetbox(input_box, multiselect)
box_list = [box_maker() for i in range(2)]
tabs = [Panel(child=box,title="Panel{}".format(i)) for i,box in enumerate(box_list)]
tabs = Tabs(tabs=tabs)
curdoc().add_root(tabs)
Note that the highlighting of the options in multiselect may not look consistent, but that just seems to be visual as the values/options of each of them are changing correctly.
But unless you are particularly attached to the layout look when you put the widgets inside the panels, you could just put one input and multiselect outside, and write their callbacks to deal with what will be in the different panels.