Боке - Как иметь один и тот же виджет (или дублировать виджет) на двух разных вкладках?

Я пытаюсь создать фильтр виджетов (состоит из 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 это не отменяет предыдущие пункты.

Другие вопросы по тегам