我正在尝试编写一个 Bokeh 应用程序,该应用程序可以动态添加和删除选项卡面板以响应服务器中发生的事件。它可以工作,但是添加或删除事件面板左侧的面板会导致事件面板发生更改,因为它使用事件面板的固定索引,而不是跟踪哪个面板对象处于事件状态。
我可以通过首先检索事件面板,然后在 Tabs
对象上设置 active
属性来强制其返回当前事件面板来解决此问题,但是有这有两个问题:
- 它闪烁:浏览器切换选项卡,然后再次切换回来。我尝试过使用按住+取消按住 — 没有任何改善。
- 服务器架构文档指出,从服务器和客户端更改属性的处理方式不佳,因此不建议这样做。我想这会显示为竞争条件,其中用户选择其他一些选项卡,但服务器随后将其强制返回到其所在的选项卡,而且服务器会对客户端正在使用的选项卡有错误的了解。
有更好的方法吗?
一些最小的代码(与 Bokeh 服务器
一起使用)来演示基本问题(不包括我的第二段中的解决方法):
from bokeh.models.widgets import Tabs, Panel, Paragraph, Button
from bokeh.layouts import column
from bokeh.io import curdoc
def callback():
if len(tabs.tabs) == 1:
tabs.tabs.insert(0, panels[0])
else:
del tabs.tabs[0]
panels = [
Panel(child=Paragraph(text='Panel 1'), title='Panel 1'),
Panel(child=Paragraph(text='Panel 2'), title='Panel 2')
]
tabs = Tabs(tabs=list(panels))
button = Button(label='Toggle')
button.on_click(callback)
curdoc().add_root(column(button, tabs))
最佳答案
工作示例,使用异步处理程序来修复闪烁并提高响应能力。
做了很多改变:
- 包含包含
serve()
入口点的应用程序。 - 解决订单丢失的问题
- 插入后恢复选项卡选择
- 按钮在执行器上运行; UI 更新现在是异步的
代码如下:
import functools
from bokeh.layouts import column
from bokeh.models.widgets import Tabs, Panel, Paragraph, Button
from bokeh.server.server import Server
from tornado.gen import coroutine
import tornado.concurrent
import tornado.ioloop
class BokehExample(object):
def __init__(self):
# Needed for run_on_executor
self.executor = tornado.concurrent.futures.ThreadPoolExecutor(max_workers=4)
self.io_loop = tornado.ioloop.IOLoop.current()
# UI Elements
self.button = None
self.panels = None
self.tabs = None
# Document
self.doc = None
@tornado.concurrent.run_on_executor
def _execute_async(self, function, *args, **kwargs):
"""
Run function on executor
"""
return function(self, *args, **kwargs)
def _update_async(self, element, attr_name, attr_value):
"""
Decouple the GUI update from the calling thread.
"""
def set_ui_attr():
setattr(element, attr_name, attr_value)
self.doc.add_next_tick_callback(functools.partial(set_ui_attr))
def async_button_handler(function):
@functools.wraps(function)
@coroutine
def wrapper(self, *args, **kwargs):
rval = yield BokehExample._execute_async(self, function, *args, **kwargs)
return rval
return wrapper
@async_button_handler
def callback(self):
active_panel_id = self.tabs.tabs[self.tabs.active].id
nr_tabs = len(self.tabs.tabs)
# Insert logic
if nr_tabs == 1:
# async version of: self.tabs.tabs = self.panels
self._update_async(self.tabs, 'tabs', self.panels)
new_index_of_active_tab = next(i for i, t in enumerate(self.panels) if t.id == active_panel_id)
self._update_async(self.tabs, 'active', new_index_of_active_tab)
# Delete logic
else:
self._update_async(self.tabs, 'tabs', [p for p in self.panels if p.id == active_panel_id])
def render(self, doc):
# Note that the IDs are ascending. This property is used to restore ordering on delete/insert cycles.
# You can also use other logic for that, for example a dictionary of titles and their relative positions.
self.panels = [
Panel(id="1001", child=Paragraph(text='Panel 1'), title='Panel 1'),
Panel(id="1002", child=Paragraph(text='Panel 2'), title='Panel 2'),
Panel(id="1003", child=Paragraph(text='Panel 3'), title='Panel 3')
]
self.tabs = Tabs(tabs=self.panels)
self.button = Button(label='Toggle')
self.button.on_click(self.callback)
self.doc = doc
self.doc.add_root(column(self.button, self.tabs))
def serve(self):
server = Server({'/': self.render}, num_procs=1)
server.start()
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
if __name__ == '__main__':
BokehExample().serve()
关于python - Bokeh :添加/删除面板,同时保留当前面板,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53052962/