Боке - Как иметь один и тот же виджет (или дублировать виджет) на двух разных вкладках?
Я пытаюсь создать фильтр виджетов (состоит из TextInput
а также MultiSelect
), который повторяется на двух разных вкладках Bokeh. Желаемая функциональность заключается в том, что результаты фильтрации должны сохраняться между вкладками, независимо от того, какой фильтр получает текст для фильтрации.
Код ниже (это рабочий код) строит Filter
виджет, который создается как filter1
а также filter2
, Обратный звонок является update
функция, которая выполняет фактическую фильтрацию и обновляет MultiSelect
часть фильтра.
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))
Код выше производит / собирает виджет, как показано здесь:
Кроме того, функциональность двух зеркальных фильтров является желаемой; когда текст вводится в одно из полей, результаты отображаются на обоих фильтрах.
Теперь, и здесь я нуждаюсь в помощи: мне нужны одни и те же фильтры с одинаковыми функциями, но они нужны мне на двух разных вкладках; один фильтр на одной вкладке и другой фильтр на другой вкладке.
Код, используемый для построения структуры двух вкладок:
p1 = Panel(child = filter1, title = "Panel1")
p2 = Panel(child = filter2, title = "Panel2")
tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))
Что касается результатов, код сохраняет желаемую функциональность, но фильтры отображаются на той же странице. Более того, панели / вкладки даже не создаются.
Есть идеи, чего не хватает? (Если вы хотите поиграть с кодом, он должен работать сразу после установки Bokeh.)
2 ответа
Я не думаю, что ваш пример должен даже создавать документ, так как ваши текстовые входы и модели с множественным выбором имеют одинаковый идентификатор, что может быть причиной неправильного отображения вкладок.
Мое решение аналогично HYRY, но с более общей функцией для обмена атрибутами, используя две разные вещи:
model.properties_with_values ()
Может использоваться с любой моделью боке и возвращает словарь всех пар атрибут: значение модели. В ipython в основном полезно исследовать объекты боке и отлаживать
Document.select({'типа':model_type})
Генератор всех виджетов нужного типа в документе
Затем я просто отфильтровываю виджеты, которые не разделяют те же теги, что и виджет ввода, что позволит избежать "синхронизации" других входов / множественного выбора, не сгенерированных с помощью box_maker(). Я использую теги, потому что разные модели не могут иметь одно и то же имя.
Когда вы изменяете значение TextInput, оно изменяет связанный Multiselect в функции обновления, но также изменяет все другие TextInputs и запускает их обновление таким же образом. Таким образом, каждый триггер Input обновляется один раз и изменяет параметры своего соответствующего мультиселекта (и не умножает каждый раз, потому что это обратный вызов on_change, если вы задаете то же значение для нового входа, который он не вызывает).
Для Multiselect первый триггер update выполнит свою работу, но, поскольку он изменил значения другого Multiselect, он все еще будет активирован столько раз, сколько есть виджеты Multiselect.
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)
Обратите внимание, что выделение параметров в множественном выборе может выглядеть не согласованно, но это только кажется визуальным, поскольку значения / параметры каждого из них изменяются правильно.
Но если вы не особенно привязаны к внешнему виду макета, когда размещаете виджеты внутри панелей, вы можете просто поместить один вход и множественный выбор снаружи и написать их обратные вызовы, чтобы иметь дело с тем, что будет на разных панелях.
Вы не можете использовать одну и ту же модель виджета для создания нескольких видов. Вы можете создавать новые виджеты на каждой вкладке и связать значение:
from bokeh.io import curdoc
from bokeh.layouts import column, widgetbox, row, layout, gridplot
from bokeh.models import Slider, Select, TextInput, MultiSelect, CustomJS
from bokeh.models.widgets import Panel, Tabs
import pandas as pd
from functools import partial
df = pd.DataFrame(["apples", "oranges", "grapes"], columns=["fruits"])
class Filter:
def __init__(self):
self.multiselect = MultiSelect(title = 'multiselect',
name = 'multiselect',
value = [],
options = list(df["fruits"]))
self.multiselect.on_change('value', self.selection_changed)
self.input_box = TextInput(title = 'input',
name ='input',
value='Enter you choice')
self.input_box.on_change('value', self.input_box_updated)
self.widget = widgetbox(self.input_box, self.multiselect)
def input_box_updated(self, attr, old, new):
print(attr, old, new)
col_data = list(df[df['fruits'].str.contains(new)]['fruits'])
self.multiselect.update(options = sorted(list(col_data)))
def selection_changed(self, attr, old, new):
print(new)
filter1 = Filter()
filter2 = Filter()
def link_property(property_name, *widgets):
wb = widgetbox(*widgets)
wb.tags = [property_name, 0]
def callback(widgets=wb):
if widgets.tags[1] != 0:
return
widgets.tags[1] = 1
for widget in widgets.children:
widget[widgets.tags[0]] = cb_obj.value
widgets.tags[1] = 0
jscallback = CustomJS.from_py_func(callback)
for widget in widgets:
widget.js_on_change(property_name, jscallback)
link_property("value", filter1.input_box, filter2.input_box)
link_property("value", filter1.multiselect, filter2.multiselect)
p1 = Panel(child = filter1.widget, title = "Panel1")
p2 = Panel(child = filter2.widget, title = "Panel2")
tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))
Кажется, что есть ошибка в MultiSelect
это не отменяет предыдущие пункты.