боке: добавить / удалить панели, сохраняя текущую панель - PullRequest
0 голосов
/ 29 октября 2018

Я пытаюсь написать приложение Bokeh, которое динамически добавляет и удаляет панели вкладок в ответ на события, происходящие на сервере. Это работает, но добавление или удаление панели слева от активной панели приводит к изменению активной панели, поскольку она использует фиксированный индекс для активной панели, а не отслеживает, какой объект панели является активным.

В основном это можно исправить, сначала получив активную панель, а затем установив свойство active для объекта Tabs, чтобы принудительно вернуть его на текущую активную панель, но с этим есть две проблемы:

  1. Мерцает: браузер переключает вкладки, затем снова переключается. Я пытался использовать hold + unhold - без улучшений.
  2. В документации по архитектуре сервера отмечено, что изменение свойств как сервера, так и клиента не обрабатывается должным образом и не рекомендуется. Я предполагаю, что это проявилось бы как состояние гонки, когда пользователь выбирает какую-то другую вкладку, но сервер затем возвращает ее обратно на вкладку, на которой он находился, плюс у сервера было бы неверное представление о том, какую вкладку использовал клиент.

Есть ли лучший подход?

Некоторый минимальный код (который будет использоваться с bokeh server) для демонстрации основной проблемы (не включает обходной путь из моего второго абзаца):

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))

1 Ответ

0 голосов
/ 21 июня 2019

Рабочий пример с асинхронным обработчиком для устранения мерцания и улучшения резонанса.

Сделано кучу изменений:

  1. содержит приложение, включая точку входа serve().
  2. исправить проблему с потерянным заказом
  3. восстановить выбор вкладки после вставки
  4. запускается на исполнителе; Обновления интерфейса теперь асинхронные

Код ниже:

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()
...