Bind dynamic list of widgets

I am trying to build a dynamic list of color pickers. I am not sure about the bindings and seems like what I tried so far is not doing it :sweat_smile:

I have a minimal reproducible non working example which is showing what I would like to do. I would like that a change on any ColorPicker would update the following widget. My guess so far is that I am binding the Card instead of each individual components.

I am very new to Panel so I might have missed something very obvious. Like I don’t know if there is a handy tool to check all my bindings besides running and seeing if my app works or not.

import panel as pn

pn.extension(template="material")

# these 2 are functions that I bind in my real app, but does not seem to be the problem as this also fails
# the length of both lists is variable, here it's an example with 3, but can be anything.
base_colors = ["#0072b5"]*3
states = ["low", "medium", "high"]

def color_pickers(states, colors):
    pickers = [
        pn.widgets.ColorPicker(name=state, value=color)
        for state, color in zip(states, colors)
    ]

    card_pickers = pn.Card(*pickers, title="My colors:")
    return card_pickers


def palette(colors):
    list_colors = [color.value for color in colors]
    return pn.pane.Markdown(f"{list_colors}")


interactive_color_pickers = pn.bind(color_pickers, states, base_colors)
interactive_palette = pn.bind(palette, interactive_color_pickers)


pn.Column(interactive_color_pickers, interactive_palette).servable(title="Minimal")

Here’s one way to do it:

import panel as pn

pn.extension(template="material")

# these 2 are functions that I bind in my real app, but does not seem to be the problem as this also fails
BASE_COLOR = "#0072b5"
STATES = ["low", "medium", "high"]

def palette(*colors):
    list_colors = [color for color in colors]
    return pn.pane.Markdown(f"{list_colors}")

color_pickers = [pn.widgets.ColorPicker(name=state, value=BASE_COLOR) for state in STATES]
pn.Column(*color_pickers, pn.bind(palette, *color_pickers))

1 Like

Here’s a param way:

import panel as pn
import param

pn.extension(template="material")
BASE_COLOR = "#0072b5"

class ColorsDisplay(param.Parameterized):
    
    low = param.Color(default=BASE_COLOR)
    medium = param.Color(default=BASE_COLOR)
    high = param.Color(default=BASE_COLOR)

    @param.depends('low', 'medium', 'high')
    def palette(self):
        list_colors = [color for color in [self.low, self.medium, self.high]]
        return pn.pane.Markdown(f"{list_colors}")
    
    def view(self):
        return pn.Column(pn.Param(self.param), self.palette)

ColorsDisplay().view()

Thanks!

I am trying to see if this is working. I cannot have the states and colors as you did as in my case they are dynamically being generated and the size is not constant.

For more context, I pushed a branch of my current code here. The relevant part starts L287, the functions are not organised yet for that part as I am still fighting around.

Good point; here’s a dynamic ver.

import panel as pn
import param

pn.extension(template="material")
BASE_COLOR = "#0072b5"

class ColorsDisplay(param.Parameterized):
    
    colors = param.List(default=[BASE_COLOR])

    def __init__(self, **params):
        self._color_pickers = pn.Column()
        super().__init__(**params)

    def _pick_colors(self, event):
        """
        When color is picked from the color picker, add it to the list of selected colors.
        """
        self.colors = [color_picker.value for color_picker in self._color_pickers]

    @param.depends("colors", watch=True, on_init=True)
    def _create_color_pickers(self):
        current_colors = [color_picker.value for color_picker in self._color_pickers]
        if self.colors == current_colors:
            return

        print("Updating color pickers...")
        color_pickers = []
        for color in self.colors:
            color_picker = pn.widgets.ColorPicker(name="Color", value=color)
            color_picker.param.watch(self._pick_colors, "value")
            color_pickers.append(color_picker)
        self._color_pickers[:] = color_pickers

    @param.depends('colors')
    def palette(self):
        return pn.pane.Markdown(f"{self.colors}")
    
    def view(self):
        return pn.Column(self._color_pickers, self.palette)

colors_display = ColorsDisplay()
colors_display.view()

Thanks!

Is there a reason I should use this param way over the “other” way? So far I only used the non param way. But happy to learn to do that.

EDIT: sorry I don’t get how that fits with my MWE. I am really new to Panel (and never used Param).

Here’s the functional way. I think param can group your parameters more effectively, and potentially scale (it’s how panel / holoviews is built internally).

Initially pn.bind is easier to pick up and use, but after it could potentially get messy with watch=True is my understanding.

import panel as pn
import param

pn.extension(template="material")
BASE_COLOR = "#0072b5"


def update_colors_select(event):
    colors = [color_picker.value for color_picker in color_pickers]
    colors_select.param.update(
        options=colors,
        value=colors,
    )

def create_color_pickers(colors):
    color_picker_list = []
    for color in colors:
        color_picker = pn.widgets.ColorPicker(name="Color", value=color)
        color_picker.param.watch(update_colors_select, "value")
        color_picker_list.append(color_picker)
    color_pickers[:] = color_picker_list

def create_color_palette(colors):
    return pn.pane.HTML(f"{colors}")


colors_select = pn.widgets.MultiSelect(name="Colors", visible=False)
color_pickers = pn.Column()
pn.bind(create_color_pickers, colors=colors_select.param.value, watch=True)
color_palette = pn.bind(create_color_palette, colors=colors_select.param.value)
colors_select.param.update(
    value=[BASE_COLOR],
    options=[BASE_COLOR],
)  # initialize
pn.Column(colors_select, color_pickers, color_palette)

There’s some discussion here:

1 Like

Oh great! :raised_hands: looks like I am close now.

For some reason if I adapt your code, I don’t get the initial selection. So I need to show the MultiSelect and then if I select everything it works. It looks like I don’t manage to have the variable number of initial colors:

def base_colors(args):
    # see my branch for details
    return ['#fdb97d', '#c6c7e1', '#fca082']


# dummy binding, needed on my side
interactive_base_colors = pn.bind(base_colors, 3)

colors_select = pn.widgets.MultiSelect(
    value=interactive_base_colors,
    options=interactive_base_colors,
    name="Colors",
    visible=False
)

Here I don’t know how to make the update part.

Oh doing this works!

import panel as pn

pn.extension(template="material")


def base_colors(args):
    return ['#fdb97d', '#c6c7e1', '#fca082']


interactive_base_colors = pn.bind(base_colors, 3)


def update_colors_select(event):
    colors = [color_picker.value for color_picker in color_pickers]
    colors_select.param.update(
        options=colors,
        value=colors,
    )

def create_color_pickers(colors):
    color_picker_list = []
    for color in colors:
        color_picker = pn.widgets.ColorPicker(name="Color", value=color)
        color_picker.param.watch(update_colors_select, "value")
        color_picker_list.append(color_picker)
    color_pickers[:] = color_picker_list

def create_color_palette(colors):
    return pn.pane.HTML(f"{colors}")


color_pickers = pn.Card()
colors_select = pn.widgets.MultiSelect(
    value=interactive_base_colors,
    options=interactive_base_colors,
    name="Colors",
    visible=False
)


dummy = pn.bind(create_color_pickers, colors=colors_select.param.value, watch=True)
color_palette = pn.bind(create_color_palette, colors=colors_select.param.value)


pn.Column(dummy, colors_select, color_pickers, color_palette).servable()

Calling the binding (dummy) in the pane seems to trigger everything.

Thanks again for all your help and patience @ahuang11 :bowing_man:

1 Like

Hey @tupui!! I’ll try to spend some more time looking at your example later today or this week, but this raises some concerns:

dummy = pn.bind(create_color_pickers, colors=colors_select.param.value, watch=True)
pn.Column(dummy, colors_select, color_pickers, color_palette).servable()

As dummy = pn.bind(..., watch=True) registers create_color_pickers as a callback and I believe passing dummy to pn.Column will again register create_color_pickers as a callback, it’s likely it’s going to be called twice. That’s what I’ll try check later :slight_smile:

1 Like

Thank you so much :pray:

You are right and that’s what I get in the logs :sweat_smile: as it was working now I did not know what else to do I left it.

You can see the final version in context here https://github.com/Simulation-Decomposition/simdec-python/blob/main/panel/app.py

Seems like removing watch=True removes the warning and it works just fine.

Looks like with Panel 1.3 I can have a look at rewriting that using bind+rx :sweat_smile:

1 Like