Interactive Parameterized Dictionary

Hi all,

I am building a Panel app and I have been using Param to create a parameterized DataHolder class.

One of the features of this class I would like is for users to be able to define group_name : members_list which will be used to filter a param.DataFrame attribute of my class.

The members list is made up of a selection from the unique values on one of the dataframe columns and the group_name is user defined with text input.

I have read lots of the conversations on this topic via #598 but I still can’t decipher what is the correct way to write an updater function such that users can define and edit the available options for groups.

class Data(param.Parameterized):

  df = param.DataFrame(precedence=-1)
  groups = param.Dict(default={}) # this doesn't seem to have the parameterized functionality I expect
# Perhaps something like below instead?
# groups = param.Selector(default = {}, check_on_set=False, instantiate=True)
  group_members = param.ListSelector(default=[])
  group_name = param.String(default="")
  
  add_group_button = param.Action(
      lambda x: x.param.trigger("add_group_button"), label="Create Group"
  )

  @param.depends("add_group_button", watch=True)
  def add_selection_to_group(
      self, group_name: str = None, group_members: list = None
  ):
      if group_name is not None and group_members is not None:
          self.groups[group_name] = group_members
      else:
          self.groups[self.group_name] = self.group_members

      self.param.trigger("groups")
      self.group_members = []
      self.group_name = ""

This is my work in progress but It’s not really allowing me to access the keys and values to populate widgets for User editing.

Please let me know if anything is unclear.

Any help is greatly appreciated.

DG

Hi
Take a Look at the region/country example at
https://panel.holoviz.org/how_to/param/dependencies.html

Its approach should work for yours as well.
I personally would go with a different approach though, wrapping the group name and the list in a param class and add a list of these objects to your class via classselector.
May sketch something up when I’m back in front of a computer,but may take a few days.
Johann

DG,
It’s raining, … so thought about this a little more.

On a high level you seem to want the user to be able to do 2 separate things via widgets on a UI:

  1. create Groups (having a group_name and a list of column values to filter for)
  2. select one of the Group filters defined and apply it to the DF

I tweaked your example along these lines so you can play a little bit more.
The main challenge i think you have is that although the param.Selector accepts a dict for the list of objects it does so only at init (internally it then populates .names and .objects attributes). So not easy to update later
Hence I splitted your self.groups into an internal self._groups (that holds the actual group_name/members) and a self.use_group that allows the user to select a group.

import param
import panel as pn
pn.extension()

class Data(param.Parameterized):

    # dataframe and user-defined filters (empty at start)
    df = param.DataFrame(precedence=-1)
    _groups = param.Dict(precedence=-1, default={}, doc='Holds available Groups details') 

    # parameters and buttons to allow user to create filter groups
    group_name = param.String()
    group_members = param.ListSelector(
                        default=[], # empty by default, letting user to select some
                        objects=['A', 'B', 'C', 'D']) # unique values of the DF column
    create_group = param.Action(lambda self: self._cb_create_group())

    # parameter to allow user to select one of the existing Groups and apply
    use_group = param.Selector(default=None, doc='Group to apply')

    def _cb_create_group(self):
        ''' Will set a new group or overwrite an existing one. 
            It will also trigger a filtering of the data with the new/updated filter 
        '''
        if not self.group_name or not self.group_members:
            print('Specify group_name and group_members')
            return
        if self.group_name in self._groups:
            print(f'group_name "{self.group_name}" already exists. Ignored')
            return

        print(f'Create group: {self.group_name}, {self.group_members}')
        # update currently defined lists with the user input
        self._groups[self.group_name] = self.group_members

        # update the use_groups available list of groups and select the newly added
        # one (which will trigger a rerun of the DF filtering)
        # doing it in the sequence below will ensure the widget gets updated as well
        # (doing value first and then objects will update the actual use_group parameter
        # but not the widget :-()
        self.param.use_group.objects = list(self._groups.keys())
        self.use_group = self.group_name
        print(f'updated use_group parameter/widget: {self.use_group}, {self.param.use_group.objects}')

        
    @param.depends('use_group', watch=True)
    def filter_df(self):
        ''' filters DF based on filter selected in groups '''

        print(f'Filter with: {self.use_group}: {self._groups[self.use_group]}')
       
    def view(self):
        ''' Show widgets to allow user to update filters and select a filter '''
        return pn.Row(
                    pn.Param(self, parameters=['group_name', 'group_members', 'create_group'],
                             name='Create Filter Group'), 
                    pn.widgets.Select.from_param(self.param.use_group))

data_ui = Data()
data_ui.view()