Automatically set value of a Multichoice when nothing is selected using set_param()

I want to automatically set a value in a Multichoice when no value is selected using the set_param() method (because I want to access to the selector object using its name).

In the following Minimum Reproducible Example, when unselecting a field in a Multichoice, the value updates itself with [“ALL”] using the add_all_if_nothing() function in the backend but nothing happens graphically.

import pandas as pd
import time
import panel as pn
import holoviews as hv
import numpy as np
import holoviews.plotting.bokeh
from bokeh.models import (
    ColumnDataSource,
    DataTable,
    HTMLTemplateFormatter,
    NumberFormatter,
    TableColumn,
)
import param

pn.extension(loading_spinner='dots', loading_color='#00aa41', sizing_mode="stretch_width")
hv.extension('bokeh')
pn.param.ParamMethod.loading_indicator = True

class ExclusiveSelector(param.Parameterized):
    
    countries_and_continents = {'countries': {0: 'Abkhazia',
      1: 'Afghanistan',
      2: 'Akrotiri and Dhekelia',
      3: 'Albania',
      4: 'Algeria',
      5: 'American Samoa',
      6: 'Andorra',
      7: 'Angola',
      8: 'Anguilla',
      9: 'Antarctica',
      10: 'Antigua and Barbuda',
      11: 'Argentina',
      12: 'Armenia',
      13: 'Aruba',
      14: 'Australia',
      15: 'Austria',
      16: 'Austria-Hungary',
      17: 'Azerbaijan',
      18: 'Baden',
      19: 'Bahamas'},
     'continents': {0: 'Asia',
      1: 'Asia',
      2: 'Asia',
      3: 'Europe',
      4: 'Africa',
      5: 'Oceania',
      6: 'Europe',
      7: 'Africa',
      8: 'North America',
      9: 'Antarctica',
      10: 'North America',
      11: 'South America',
      12: 'Asia',
      13: 'North America',
      14: 'Oceania',
      15: 'Europe',
      16: 'Europe',
      17: 'Asia',
      18: 'Europe',
      19: 'North America'}}

    button = pn.widgets.Button(name="UPDATE", button_type="primary")
    
    df_continents_countries = pd.DataFrame(countries_and_continents)

    continents_values = list(df_continents_countries["continents"].unique())
    countries_values = list(df_continents_countries["countries"].unique())
    
    continents = param.ListSelector(default=[continents_values[0]], objects= ["ALL"] + continents_values)
    countries = param.ListSelector(default=[countries_values[0]], objects= ["ALL"] + countries_values)
    
    selector_values = {
            "continents": continents,
            "countries": countries
        }

    def __init__(self, **params):
        super().__init__(**params)
        self.current_df = self.df_continents_countries
        self.settings_panel = pn.Param(
            self,
            parameters=[
                "continents",
                "countries"
            ],
            widgets={
                "continents": {"type": pn.widgets.MultiChoice},
                "countries": {"type": pn.widgets.MultiChoice},
            },
        )
        self.component = pn.Column(self.button, self.settings_panel, 
                                   pn.panel(pn.bind(self.update_plot, self.button), 
                                            loading_indicator=True))
        
    def get_filters(self):
        continents_val = self.continents
        countries_val = self.countries
        filters = {
            "continents": continents_val,
            "countries": countries_val,
        }
        return filters
    
    def add_all_if_nothing(self):
        for elem in self.selector_values:
            current_f = getattr(self, elem)
            print(current_f)
            if len(current_f)<1:
                print("nothin")
                setattr(self, elem, ["ALL"])
                self.set_param(elem, ["ALL"])
        

    @param.depends("countries", "continents", watch=True)
    def update_fields(self):
        self.add_all_if_nothing()
        filters = self.get_filters()
        df = self.df_continents_countries

        for key, inp in filters.items():
            if inp != ["ALL"]:
                df = df[df[key].isin(inp)]
        
        self.current_df = df
                    
        self.build_distinct_fields(df)
        return True

    def build_distinct_fields(self, df):
        self.param.continents.objects = ["ALL"] + list(df["continents"].unique())
        self.param.countries.objects = ["ALL"] + list(df["countries"].unique())

    def update_plot(self, event):
        selected_filters = self.current_df
        source = ColumnDataSource(data=selected_filters.to_dict("list"))
        columns = []
        if event: 
            time.sleep(2)
            for val in self.countries_and_continents:
                columns.append(
                        TableColumn(
                            field=val,
                            title=val,
                        ))
        return DataTable(source=source, columns=columns)

exc_sel = ExclusiveSelector()
pn.template.FastListTemplate(
    site="Panel", title="MRE Exclusive Selector", 
    main=[exc_sel.component]).servable();

How could this update could be displayed in the Multichoice field so that I have “ALL” selected when I unselect everything ?

Your example could have been even more minimal to remove unnecessary noise. But the problem you are having is that you have to change the values in the setting_panel component and not the parameters.

import pandas as pd
import panel as pn
import param


class ExclusiveSelector(param.Parameterized):

    countries_and_continents = {
        "countries": {
            0: "Abkhazia",
            1: "Afghanistan",
            2: "Akrotiri and Dhekelia",
            3: "Albania",
            4: "Algeria",
            5: "American Samoa",
            6: "Andorra",
            7: "Angola",
            8: "Anguilla",
            9: "Antarctica",
            10: "Antigua and Barbuda",
            11: "Argentina",
            12: "Armenia",
            13: "Aruba",
            14: "Australia",
            15: "Austria",
            16: "Austria-Hungary",
            17: "Azerbaijan",
            18: "Baden",
            19: "Bahamas",
        },
        "continents": {
            0: "Asia",
            1: "Asia",
            2: "Asia",
            3: "Europe",
            4: "Africa",
            5: "Oceania",
            6: "Europe",
            7: "Africa",
            8: "North America",
            9: "Antarctica",
            10: "North America",
            11: "South America",
            12: "Asia",
            13: "North America",
            14: "Oceania",
            15: "Europe",
            16: "Europe",
            17: "Asia",
            18: "Europe",
            19: "North America",
        },
    }

    df_continents_countries = pd.DataFrame(countries_and_continents)

    continents_values = list(df_continents_countries["continents"].unique())
    countries_values = list(df_continents_countries["countries"].unique())

    continents = param.ListSelector(
        default=[continents_values[0]], objects=["ALL"] + continents_values
    )
    countries = param.ListSelector(
        default=[countries_values[0]], objects=["ALL"] + countries_values
    )

    def __init__(self, **params):
        super().__init__(**params)

        self.settings_panel = pn.Param(
            self,
            parameters=["continents", "countries"],
            widgets={
                "continents": pn.widgets.MultiChoice,
                "countries": pn.widgets.MultiChoice,
            },
        )

    @param.depends("countries", "continents", watch=True)
    def add_all_if_nothing(self):
        for elem in self.settings_panel[1:]:  # First value is the title
            if not len(elem.value):
                elem.value = ["ALL"]

        with param.discard_events(self):
            if not len(self.continents):
                self.continents = ["ALL"]

            if not len(self.countries):
                self.countries = ["ALL"]


exc_sel = ExclusiveSelector()
exc_sel.settings_panel.servable()

exclusive

If it was me who designed the app I would properly just remove “ALL” completely and just incorporate no value in your filtering.

You should also move button = pn.widgets.Button in to __init__ to not have it sync between multiple sessions.

Update: Also sets parameter in add_all_if_nothing, discard_events is to not trigger function twice.

1 Like