Enable param.ObjectSelector to be defined below the init method and to fetch custom instance variables

Hi,
I am eager to utilize the parametrized classes for dashboarding. I am on a learning curve, but it often works very nicely. The challenge I got is when I want to provide the class with some custom instance vaiables and have these update the objects of a param.ObjectSelector. One purpose of this example is to have the class to enableing showing a holoviews plot where the user can choose between pairs of dataframe columns to crossplot (this can be achieved othervise more simply, but I need a simple example here).

First example - hard coded options

First I will show a working example with hard_coded_options and then show a so far failing attempt in enabling custom instance variables.

# Imports
from io import StringIO
import pandas as pd
import param
import holoviews as hv
import panel as pn
from collections import defaultdict
hv.extension()
pn.extension()

# Setting up test data
data_text = (
    """
      sensor|    a|    b|      c|     e
          s1|   7.|   2.|     3.|   11.
          s1|   3.|   4.|     4.|    2.
          s1|   .5|   2.|     3.|    5.
          s2|   2.|   3.|     2.|    1.
          s2|   1.|   7.|     2.|   -2.
          s2|   1.|   2.|     1.|    0.
    """) 
df = pd.read_csv(StringIO(data_text), sep='|', skipinitialspace=True)

Preview of df below:

# setting up predefined options as a list of lists where each inner list is a pair of column names from dataframe. 
pairs_custom = [
    ['a', 'b'],
    ['b', 'c'],
    ['a', 'c']
]

# Hard coding the option list into the class is straight forward.
class Viewer(param.Parameterized):
    pair = param.ObjectSelector(objects=pairs_custom, default=pairs_custom[0])
    @param.depends('pair')
    def view(self):
        return hv.Points(df, kdims=self.pair)

viewer_firm_options = Viewer()
pn.Row(viewer_firm_options.param, viewer_firm_options.view)

# And it works as expected: 
![image|690x336](upload://79lQYSUAvWQ4G2RzyxqAvjXllvR.png)

Second example - user provide instance variable on options

Now the tricky bit comes. I want the class to be usable in a situation where the user will define which pairs that are usable. I would want the user to provide options in the instance variables to a init method.
Very well: While sitting and writing this post I notice this is my the time I actually had this second example working. I so far rely on #440 to update the options of the param.ObjectSelectori inside the init. I post this anyways as it might be useful for others and to discuss one issue.

 1 #  My best attempt so far (trying to read up on different challenges.
 2 class Viewer(param.Parameterized):
 3     pair = param.ObjectSelector()  # (objects={'A': ['a', 'c']}, default=['a', 'c'])
 4     def __init__(self, pairs, **params):
 5         super().__init__(**params, name='Test viewer')
 6         new_opts = {', '.join(v): v for v in pairs}
 7         self.param.pair.names = new_opts
 8         self.param.pair.objects = list(new_opts.values())
 9         # self.param.pair.default = list(new_opts.values())[1]
10    # pair = param.ObjectSelector(objects=self.pairs_custom)  # This one can not reach to self and cannot be used it seems
11    @param.depends('pair')
12    def view(self):
13          return hv.Points(df, kdims=self.pair)
14 
15 viewer_flexible = Viewer(pairs=pairs_custom)
16 pn.Row(viewer_flexible.param, viewer_flexible.view)

Working, after a lot of attempts!
But actually, paying attention to the axis labels (xlabel is initiated with ‘sensor’ which is unexpected) and location of points in x-dim these are not correct until the widget has been updated at least one time. Any suggestion here on how to solve it?
2023-01-31_10-07-37

Third example - A simpler desired future solution?:

I do not know if it is possible at all, but it would have been a great simplification if line 10 above (5 below) could have been enabled to fetch the init instance variable ‘pairs’.

 1 #  My 'wish-it-was this-easy' way of writing this
 2 class Viewer(param.Parameterized):
 3    def __init__(self, pairs, **params):
 4         super().__init__(**params, name='Test viewer')
 5    pair = param.ObjectSelector(objects=self.pairs)  # This one can not reach to self and cannot be used it seems
 6    @param.depends('pair')
 7    def view(self):
 8        return hv.Points(df, kdims=self.pair)
 9 
10 viewer_flexible = Viewer(pairs=pairs_custom)
11 pn.Row(viewer_flexible.param, viewer_flexible.view)

Currently this last example returns an error indicating that self cannot be reached in line 5

Do anyone know if the last example at all could be possible in terms of how python classes and ‘param’ works?

And do anyone know how to avoid issues with initiating the pair widget in the second example (see animation where xlabel = ‘sensor’ and values are located in the ‘sensor’ space initially which is wrong).

Hi @geoviz,

I feel like I’ve just fudged your work here don’t know if what your looking for but maybe something like the below is of help,

# Imports
from io import StringIO
import pandas as pd
import param
import holoviews as hv
import panel as pn
from collections import defaultdict
hv.extension()
pn.extension()

# Setting up test data
data_text = (
    """
      sensor|    a|    b|      c|     e
          s1|   7.|   2.|     3.|   11.
          s1|   3.|   4.|     4.|    2.
          s1|   .5|   2.|     3.|    5.
          s2|   2.|   3.|     2.|    1.
          s2|   1.|   7.|     2.|   -2.
          s2|   1.|   2.|     1.|    0.
    """) 
df = pd.read_csv(StringIO(data_text), sep='|', skipinitialspace=True)

# setting up predefined options as a list of lists where each inner list is a pair of column names from dataframe. 
pairs_custom = [
    [], # list doesn't select until new option is selected, self.pair if you check runs through as None initially that's giving the unwanted graph here I believe, so give list empty as default, therefore first user selection from drop down will be used, otherwise if don't want to start with empty you'd have to set a default maybe
    ['a', 'b'],
    ['b', 'c'],
    ['a', 'c']
]


 #  My 'wish-it-was this-easy' way of writing this
class Viewer(param.Parameterized):
    def __init__(self, **params):
        super().__init__(**params, name='Test viewer')
    pair = param.ObjectSelector(objects=dict()) #give it default here perhaps, not sure if you can use an index as apposed to an actual option in the provided dict
    @param.depends('pair')
    def view(self):
        if self.pair is None:
            return # or empty plot here maybe
        else:
            return hv.Points(df, kdims=self.pair)

viewer_flexible = Viewer()
viewer_flexible.param.pair.objects=pairs_custom #define param dict
pn.Row(viewer_flexible.param, viewer_flexible.view)

Hope of some help, Carl.

Thanks @carl for nice tricks there in order to tweak and simplify the process. I would prefer being able to initiate with required input arguments to the class instead of modifying values after class instance is created. And would really like to tackle the root cause of why not the

 7         self.param.pair.names = new_opts
 8         self.param.pair.objects = list(new_opts.values())

in second example is not updating the default pair params.

When running the second example…

#  My best attempt so far (trying to read up on different challenges.
class Viewer2(param.Parameterized):
    pair = param.ObjectSelector()  # (objects={'A': ['a', 'c']}, default=['a', 'c'])
    def __init__(self, pairs, **params):
        super().__init__(**params, name='Test viewer second example')
        new_opts = {', '.join(v): v for v in pairs}
        self.param.pair.names = new_opts
        self.param.pair.objects = list(new_opts.values())
        self.param.pair.default = list(new_opts.values())[0]
 
    # pair = param.ObjectSelector(objects=self.pairs_custom)  # This one can not reach to self and cannot be used it seems
    @param.depends('pair')
    def view(self):
        return hv.Points(df, kdims=self.pair)

viewer_flexible = Viewer2(pairs=pairs_custom)

…and then

viewer_flexible.param.get_param_values()

it will print:

[('name', 'Test viewer second example'), ('pair', None)]

indicating the default is not set for ‘pair.ObjectSelector’.
Also very curious to understand whether the ‘pair.ObjectSelector’ could in any way access instance arguments when created below where the init function is defined (not havin to update it’s objects and default manually).

Setting it before the super init then it all seems to click,

# Imports
from io import StringIO
import pandas as pd
import param
import holoviews as hv
import panel as pn
from collections import defaultdict
hv.extension()
pn.extension()

# Setting up test data
data_text = (
    """
      sensor|    a|    b|      c|     e
          s1|   7.|   2.|     3.|   11.
          s1|   3.|   4.|     4.|    2.
          s1|   .5|   2.|     3.|    5.
          s2|   2.|   3.|     2.|    1.
          s2|   1.|   7.|     2.|   -2.
          s2|   1.|   2.|     1.|    0.
    """) 
df = pd.read_csv(StringIO(data_text), sep='|', skipinitialspace=True)

# setting up predefined options as a list of lists where each inner list is a pair of column names from dataframe. 

pairs_custom = [
    #[], # list doesn't select until new option is selected, give it empty as default
    ['a', 'b'],
    ['b', 'c'],
    ['a', 'c']
]

#  My best attempt so far (trying to read up on different challenges.
class Viewer2(param.Parameterized):
    pair = param.ObjectSelector()  # (objects={'A': ['a', 'c']}, default=['a', 'c'])
    def __init__(self, pairs, **params):
        
        new_opts = {', '.join(v): v for v in pairs}
        self.param.pair.names = new_opts
        self.param.pair.objects = list(new_opts.values())
        self.param.pair.default = list(new_opts.values())[0]
        
        super().__init__(**params, name='Test viewer second example')
        
        
 
    # pair = param.ObjectSelector(objects=self.pairs_custom)  # This one can not reach to self and cannot be used it seems
    @param.depends('pair')
    def view(self):
        return hv.Points(df, kdims=self.pair)

viewer_flexible = Viewer2(pairs=pairs_custom)

I think I’ve read material from maybe @Marc on this before somewhere, it’s beyond me to be able to explain

I think this is it https://discourse.holoviz.org/t/placement-of-super/2996/7

Really greatful @carl!
This gives a workable solution to example 2!! :slightly_smiling_face:

1 Like