Bokeh - 如何在两个不同的选项卡中拥有相同的小部件(或复制小部件)?

Bokeh - How to have the same widget (or duplicate a widget) in two different tabs?

我正在尝试创建一个在两个不同的 Bokeh Tabs 上复制的小部件过滤器(由 TextInputMultiSelect 组成)。所需的功能是过滤结果应保留在选项卡之间,无论哪个过滤器接收要过滤掉的文本。

下面的代码(它是工作代码)构建了实例化为 filter1filter2Filter 小部件。回调是 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))

produces/assembles 小部件上方的代码,如下所示:

此外,两个镜面滤镜的功能也符合要求;当在其中一个框中输入文本时,结果会显示在两个过滤器中。

现在,这就是我需要帮助的地方,我想要具有相同功能的相同过滤器但是我需要在两个不同的选项卡中使用它们;一个选项卡中的一个过滤器和另一个选项卡中的另一个过滤器。

用于构建两个选项卡结构的代码是:

p1 = Panel(child = filter1, title = "Panel1")

p2 = Panel(child = filter2, title = "Panel2")

tabs = Tabs(tabs=[ p1, p2 ])
curdoc().add_root(layout(tabs))

在结果方面,代码保留了所需的功能 过滤器显示在同一页面上。不仅如此,panels/tabs 甚至还没有建成。
知道缺少什么吗? (如果你想玩代码,如果你安装了 bokeh,它应该可以立即运行。)

您不能使用相同的小部件模型来创建多个视图。您可以在每个选项卡中创建新的小部件,并且 link 值:

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 中似乎存在无法取消选择之前项目的错误。

我认为您的示例甚至不应该构建文档,您的文本输入和多选模型具有相同的 ID,这可能就是选项卡显示混乱的原因。

我的解决方案与 HYRY 的类似,但具有更通用的功能,可以使用两种不同的方式共享属性:

model.properties_with_values()

可以与任何散景模型和 returns 模型所有 attribute:value 对的字典一起使用。它在 ipython 中主要用于探索散景对象和调试

Document.select({'type':model_type})

文档中所需类型的所有小部件的生成器

然后我只过滤掉与输入小部件不共享相同标签的小部件,这将避免 "syncing" 其他 inputs/multiselect 不是由 box_maker() 生成的。我用tags是因为不同机型不能重名

当您更改 TextInput 值时,它会更改更新函数中关联的多选,但它也会更改所有其他 TextInput 并以相同的方式触发它们的更新。因此,每个 Input 触发更新一次并更改其各自多选的选项(而不是每次多次选择,因为它是一个 "on_change" 回调,如果您为新输入提供相同的值,它不会触发)。

对于 Multiselect,更新的第一个触发器将完成这项工作,但由于它更改了另一个 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)

请注意,多选中选项的突出显示可能看起来不一致,但这似乎是视觉上的,因为每个选项的 values/options 都在正确更改。

但是,除非您在将小部件放在面板内时特别注重布局外观,否则您可以只将一个输入和多选放在外面,并编写它们的回调来处理不同面板中的内容。