Dynamically adding widgets destroy the plot

I try dynamically add widgets. Thera are 2 classes. First i create a canvas named widget.
And in Controller method i try dynamically add widgets.

class DemoController:

    def __create_controls(self):
            add_signal_btn = Button(label="Add signal", button_type="default")
            add_signal_btn.on_click(self.__add_btn_handler)
            clear_signal_btn = Button(label="Clear signal", button_type="default", width=WIDTH)
            clear_signal_btn.on_click(self.__clear_btn_handler)
            self.widget = column(add_signal_btn, clear_signal_btn)

    def __add_btn_handler(self):
        ...
        self.widget.children.append(column(Div(text=name, width=WIDTH),
                                             row(amp_slider, freq_slider)))

class Dataview:

    def __create_controls(self):
        ...
        self.widget = column(Tabs(tabs=[Panel(child=self.__spec_figure, title='Spectrum'),
                                              Panel(child=self.__signal_figure, title='Signal')]),
                                   self.__data_table)


dataview = Dataview()
controller = DemoController()

curdoc().add_root(row(dataview.widget, controller.widget)

Then i dynamically add widgets my plots is changed. Why it’s happen ?

Maybe it will be useful, bokeh writes
W-1000 (MISSING_RENDERERS): Plot has no renderers: Figure(id=‘1011’, …)

It’s possibly a bug, but more information is needed:

  • All revenant software versions and system info

  • Most critically: a complete, MINIMAL reproducer that can actually be run and investigated.

  1. Dataview
import numpy as np

from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import Panel, Tabs, ColumnDataSource, DataTable, \
                         TableColumn, HoverTool, BoxEditTool

PLOT_WIDTH = 1000
PLOT_HEIGHT = 570

class Dataview:

    def __init__(self):
        self.__signal_figure = None
        self.__spec_figure = None
        self.__table_src = None
        self.__data_table = None
        self.__select_rect_prop = None
        self.__widget = None
        self.__signal_src = {}
        self.__spectrum = {}
        self.__spec_param_names = ('snr', 'sinad', 'sfdr', 'thd')
        self.__create_controls()

    @property
    def widget(self):
        return self.__widget

    def __create_controls(self):
        table_data = {'name':[]}
        table_columns = [TableColumn(field="name", title="Signal name")]
        for par_name in self.__spec_param_names:
            table_data[par_name] = []
            table_columns.append(TableColumn(field=par_name,
                                             title=par_name.upper() + ", dB"))
        self.__table_src = ColumnDataSource(table_data)
        hover = HoverTool(tooltips=[('', "$name"), ('', "($x, $y)")])
        self.__data_table = DataTable(source=self.__table_src, columns=table_columns,
                                      width=PLOT_WIDTH, height=400)
        self.__signal_figure = figure(plot_height=PLOT_HEIGHT, plot_width=PLOT_WIDTH,
                                      tools=[hover, "pan,box_zoom,wheel_zoom,save,reset"],
                                      x_axis_label='Time', y_axis_label='Amp')
        self.__spec_figure = figure(plot_height=PLOT_HEIGHT, plot_width=PLOT_WIDTH,
                                    tools=[hover, "pan,box_zoom,wheel_zoom,save,reset"],
                                    x_axis_label='Freq', y_axis_label='Amp, dB')
        self.__select_rect_prop = ColumnDataSource({'x': [], 'y': [], 'width': [], 'height': []})
        selected_rect = self.__spec_figure.rect(x='x', y='y', width='width', height='height',
                                                source=self.__select_rect_prop, alpha=0.2,
                                                color='green')
        self.__spec_figure.add_tools(BoxEditTool(renderers=[selected_rect],
                                                 custom_tooltip='NSD area'))
        self.__widget = column(Tabs(tabs=[Panel(child=self.__spec_figure, title='Spectrum'),
                                          Panel(child=self.__signal_figure, title='Signal')]),
                               self.__data_table)

    @staticmethod
    def __get_segment_from_rect(rect):
       pass

    def __calc_nsd_selected_rect(self, full_scale):
        pass

    def __update_nsd_table_col(self, *args, **signal_param):
        pass

    def add_signal(self, name, signal, **kwargs):
        self.__signal_src[name] = ColumnDataSource({'x':[], 'y':[]})
        self.__signal_figure.line(x='x', y='y', source=signal,
                                  color=kwargs['color'], name=name, legend_label=name)
        self.__spec_figure.line(x='x', y='y', source=self.__signal_src[name],
                                color=kwargs['color'], name=name, legend_label=name)
        new_table_data = {'name':[name]}
        for par_name in self.__spec_param_names:
            new_table_data[par_name] = [0]
        self.__table_src.stream(new_table_data)
        self.__select_rect_prop.on_change('data', lambda attr, old, new:
                                          self.__update_nsd_table_col(attr, old,
                                                                      new, **kwargs))
        signal.on_change('data', lambda attr, old, new:
                         self.__spec_update(attr, old, new,
                                            name, **kwargs))
        self.__spec_update('data', None, signal.data, name, **kwargs)

    def __update_legend(self):
        self.__signal_figure.legend.location = "top_left"
        self.__signal_figure.legend.click_policy = "hide"

        self.__spec_figure.legend.location = "top_left"
        self.__spec_figure.legend.click_policy = "hide"

    def __update_snr_table(self, row_name, spectrum, **snr_params):
        pass

    def __spec_update(self, attr, old, new, name, **kwargs):
        if len(new['y']) == 0:
            self.__signal_src[name].data = {'x': [], 'y': []}
            spectrum = np.array([])
        else:
            spectrum = np.abs(np.fft.fft(new['y']))
            self.__spectrum[name] = spectrum
            num_of_dots = 1000
            self.__signal_src[name].data = {'x': np.linspace(0, kwargs['fband'], num_of_dots),
                                            'y': 20 * np.log10(spectrum[:num_of_dots])}
        self.__update_nsd_table_col(None, **kwargs)
        self.__update_snr_table(name, spectrum=spectrum, **kwargs)
        self.__update_legend()

    def clear(self, name=None):
        pass

  1. DemoController:
import random
import numpy as np

from bokeh.plotting import curdoc
from bokeh.layouts import row, column, layout
from bokeh.models import Slider, Button, ColumnDataSource, Div

from dataview import Dataview

WIDTH = 200

class DemoController:

    def __init__(self):
        self.__signal_src = {}
        self.__current_indx = 0
        self.__dataview = None
        self.__widget = None
        self.__spec_param = {}
        self.__create_controls()

    @property
    def widget(self):
        return self.__widget

    def __create_controls(self):
        add_signal_btn = Button(label="Add signal", button_type="default")
        add_signal_btn.on_click(self.__add_btn_handler)
        self.__widget = column(add_signal_btn)

    def connect(self, dataview: Dataview):
        self.__dataview = dataview

    def __generate_signal(self):
        name = f'signal{self.__current_indx}'
        self.__spec_param[name] = {'full_scale':1, 'color': 'red', 'amp':1, 'ftone':.13, 'fband':.5,
                                   'nharm':1, 'well_dc':100, 'well_tone':100, 'well_harm':100}
        self.__spec_param[name]['color'] = random.choice(('blue', 'red', 'black'))
        self.__signal_src[name] = ColumnDataSource({'x': np.arange(10000),
                                                    'y': np.sin(2 * np.pi * \
                                                         self.__spec_param[name]['ftone'] *\
                                                         np.arange(10000))})
        return name

    def __add_btn_handler(self):
        name = self.__generate_signal()
        amp_slider = Slider(start=0.1, end=10, value=0.1, step=.1,
                            name=name +' amp', title="Amplitude", width=WIDTH)
        freq_slider = Slider(start=0.01, end=0.5, value=0.1, step=.01,
                             name=name +' ftone', title="Frequency", width=WIDTH)
        amp_slider.on_change('value', lambda attr, new, old:
                             self.__signal_change(attr, old, new,
                                                  amp_slider.name,
                                                  self.__signal_src[name]))
        freq_slider.on_change('value', lambda attr, new, old:
                              self.__signal_change(attr, old, new,
                                                   freq_slider.name,
                                                   self.__signal_src[name]))
        clear_signal_btn = Button(label="Clear signal", button_type="default", width=WIDTH)
        clear_signal_btn.on_click(self.__clear_btn_handler)

        self.__widget.children.append((column(row(Div(text=name, width=WIDTH), clear_signal_btn),
                                              row(amp_slider, freq_slider))))
        self.__dataview.add_signal(name, self.__signal_src[name], **self.__spec_param[name])

        self.__current_indx += 1

    def __clear_btn_handler(self):
        pass

    def __signal_change(self, attr, old, value, name, signal):
        sig_name, key = name.split()
        self.__spec_param[sig_name][key] = value
        signal.data['y'] = self.__spec_param[sig_name]['amp']*np.sin(2 * np.pi *
                                                             self.__spec_param[sig_name]['ftone'] *
                                                             np.arange(len(signal.data['y'])))
  1. Dispatcher
from bokeh.layouts import row
from bokeh.plotting import curdoc

class Dispatcher:

    def __init__(self):
        pass

    def draw(self, *args):
        curdoc().add_root(row([item.widget for item in args]))
  1. Test.
import numpy as np

from bokeh.models import ColumnDataSource

from dataview import Dataview
from dispathcer import Dispatcher
from demo_controller import DemoController

dataview = Dataview()
controller = DemoController()
controller.connect(dataview)
disp = Dispatcher()
disp.draw(dataview, controller)

Save all classes in to one folder. Run test: bokeh serve --show test.py

  1. Bokeh 2.0.1
  2. Linux version 4.18.0-147.el8.x86_64 ([email protected]) (gcc version 8.3.1 20190507 (Red Hat 8.3.1-4) (GCC)) #1 SMP Wed Dec 4 21:51:45 UTC 2019

Hi @FreeRP please edit your post to use code formatting so that the code is intelligible (either with the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks)

Additionally, please make some effort to reduce the code to a minimal reproducer, i.e. the shortest self-contained script that is just enough to demonstrate the issue at hand, with nothing extraneous. For example: can this be reproduced with toy test case that has two or three widgets instead of 15?

Sorry. I reduced and formatted code. Push button add_signal and you will see the problem.
i that plasce of DemoController code is proble.

    ...
    self.__widget.children.append((column(row(Div(text=name, width=WIDTH), clear_signal_btn),
                                          row(amp_slider, freq_slider))))
    self.__dataview.add_signal(name, self.__signal_src[name], **self.__spec_param[name]) # here is the problem

I can’t reproduce the behavior that you’re seeing. But I see errors regarding Legend in the JS console. Does your code work for you if you remove the legend_label arguments from the glyph functions?

Yes, it works if i remove the legend_label

Now on my machine is fine too. I don’t understand wtha happen.

Can you show me your plots, please? Is’it ok with them ?

With the unmodified version of your code, I can only break the axes by clicking on the “Add signal” button. I could not break the main body of the plot.

It seems that there’s some race condition going on with legends when you add a new glyph - the legend is rendered before the glyph itself is added. I’ll try to understand what’s going on. In the mean time, you can try creating and updating the legend manually, with the Legend and LegendItem classes.

It’s strange thing. I can’t reproduce describe problem above, but now i have problem with broken axis too.

It’s definitely a race condition. If I replace that 10000 with a lower number, say 1000, I cannot reproduce the behavior at all.

Not agree with you. I removed all legends, and it work fine with 10000 points

Yes, I was talking about the case where there is a legend.

I have created [BUG] Race condition between legend and glyph when using `legend_label` · Issue #9953 · bokeh/bokeh · GitHub
BTW notice how my example differs from yours. I didn’t come up with it by myself - I was just gradually removing stuff from your example while checking on every step that it still resulted in the same error.

FWIW a manually created legend seems to work just fine, so at least there seems to be a workaround for you.

1 Like

Thank you!