Dynamic number of widgets / params

I’d like to make GUI element where there users can select and choose a number of colors. So the number of widgets needed is variable.

I’m currently using something like this:

class Colors(param.Parameterized):
    num = param.Number(1, bounds=(0, None))
    
    def __init__(self, **param):
        super(Colors, self).__init__(**param)
        
        self.colors_col = pn.Column(pn.widgets.ColorPicker())
    
    @property
    def colors(self):
        return list([color.value for color in self.colors_col])
    
    @param.depends('num', watch=True)
    def _update_colors(self):
        while len(self.colors_col) != self.num:
            if len(self.colors_col) > self.num:
                self.colors_col.pop(-1)
            elif len(self.colors_col) < self.num:
                self.colors_col.append(pn.widgets.ColorPicker())
                
    def panel(self):
        return pn.Column(pn.Param(self.param), self.colors_col)
                     
f = Colors()
f.panel().servable()

And then I can get the colors out from the colors property. However, I would like to have a graph update when the colors change.
How can I best do this? Can I make a dynamic number of parameters on the Colors object? I was looking at the _add_parameter method but that doesnt seem like the way.
Or make a List param which holds the color values and link the widgets to that?

1 Like

OK I’m now doing this, I’m not sure if this is ‘correct’ but it works and I’m quite pleased with it. Turned out not to be that difficult.


class Colors(param.Parameterized):
    num = param.Number(3, bounds=(0, None))
    colors = param.List([])
    color_defaults = ['#1930e0', '#eded0e', '#cc0c49']
    
    def __init__(self, **param):
        super(Colors, self).__init__(**param)
        
        self.colors_col = pn.Column()
        for _ in range(self.num):
            self._add_color()
    
        self.param.watch(self.color_callback, ['colors'])
    
    
    def _add_color(self):
        try:
            default = self.color_defaults[len(self.colors_col)]
        except IndexError:
            default = '#FFFFFF'
            
        self.colors.append(default)
        widget = pn.widgets.ColorPicker(value=default)
        self.colors_col.append(widget)
        widget.param.watch(self.color_event, ['value'])
        
    def _remove_color(self):
        widget = self.colors_col.pop(-1)
        self.colors.pop(-1)
        [widget.param.unwatch(watcher) for watcher in widget.param._watchers]
        del widget
    
    @param.depends('num', watch=True)
    def _update_colors(self):
        while len(self.colors_col) != self.num:
            if len(self.colors_col) > self.num:
                self._remove_color()
            elif len(self.colors_col) < self.num:
                self._add_color()
        self.param.trigger('colors')

    def color_event(self, *events):
        for event in events:
            idx = list(self.colors_col).index(event.obj)
            self.colors[idx] = event.new
        self.param.trigger('colors')
        print(self.colors)
                
    def panel(self):
        return pn.Column(pn.Param(self.param), self.colors_col)
                 
        
    def color_callback(self, events):     
        print(events)
                    
c = Colors()
c.panel().servable()

I’ve tried to obliviate the widget that gets removed but not very successfully, if i make a weakref to the widget that still persists.

EDIT: Added a color list that holds the colors. I trigger it manually because assigning elements doenst trigger it. It works well but the field showing the value of the list doesnt uptake (although I dont need this atm)

1 Like