☰Read-only archive⌕
Looking for help with Bokeh?

This is a read-only archive. Ask questions and join the conversation on GitHub.

Visit Community Support on GitHub →
Archived discussions · Captured October 28, 2026

Dynamically adding widgets destroy the plot

Community Support
FreeRP
FreeRP

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’, …)

⌁   Link to post
Bryan
Bryan

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.

⌁   Link to post
FreeRP
FreeRP
  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 (mockbuild@kbuilder.bsys.centos.org) (gcc version 8.3.1 20190507 (Red Hat 8.3.1-4) (GCC)) #1 SMP Wed Dec 4 21:51:45 UTC 2019
⌁   Link to post
Bryan
Bryan

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?

⌁   Link to post
FreeRP
FreeRP

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
⌁   Link to post
p-himik
p-himik

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?

⌁   Link to post
p-himik
p-himik

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.

⌁   Link to post

End of archived discussion · Replies are disabled