Interactive Elements, general advice needed

Hello Community,

I am currently writing my bachelors and one part of it is setting up an interactive dashboards where users can explore some data and build customized plots. For this I am heavily relying on Panel but I am both completly new to python and completly new to front-end development. I come from Java and R and am hopefully soon graduating in Data Science (with lots of math and using the computer as a huge calculator, not so much software development). So please be ‘soft’ haha.

One of my problems is the following: I have set up a procedure so that the user can specify certain columns from a certain database view. Given the certain columns a preview dataframe should be rendered. I’ve tried defining a callback that triggers when the column selection is changing but it does not work. The function itself works but the callback doesn’t update the actual view, only the object if that makes any sense. I’ve tried different approaches with @pn.depends and the callback but both didn’t work. Can anyone give a hint what I should try next? I am in the process of figuring out the param library but I feel that it won’t help me because my sets of parameters are not limited in the way they need to be in the moment when params parameters are initialized.

@pn.depends(var_of_int.param.value,checkboxes.param.value)
def preview_data(var_of_int,checkboxes):
    if len(var_of_int.value) == 0:
        empty= pd.DataFrame({}).to_html(classes=['example', 'panel-df'])
        empty= pn.pane.HTML(empty+script, sizing_mode='stretch_width')
        return empty
    else:
        preview_data = db_views.get(checkboxes.value)[var_of_int.value]
        preview_data = preview_data.to_html(classes=['example', 'panel-df'])
        preview_data = pn.pane.HTML(preview_data+script, sizing_mode='stretch_width')
    return preview_data

def update_preview(event):
    preview = preview_data(checkboxes, x_axis,y_axis,cat_selec) 
    return preview

preview_button = pn.widgets.Button(name='Preview Data',
                                   disabled=True,
                                   button_type='primary')

def toggle_prev_button(event):
    if x_axis.value is None or y_axis.value is None:
        preview_button.disabled = True
    else:
        preview_button.disabled = False

x_axis.param.watch(toggle_prev_button, 'value')
y_axis.param.watch(toggle_prev_button, 'value')

prev = pn.pane.HTML(pd.DataFrame({}).to_html(classes=['example', 'panel-df'])+script, sizing_mode='stretch_width')

def prev_button_click(event):
    prev = preview_data(checkboxes, x_axis, y_axis, cat_selec)
    
preview_button.on_click(prev_button_click)

Hi @thejonnyt

Welcome to the community and thanks for trying out Panel.

I don’t see any obvious mistakes. But without a minimum reprodible example it is really difficult to help.

Being on the other side the first thing I need to be able to do is to confirm that there actually is a problem. I need to be able to reproduce the problem. And without a minimal reproducible example that is hard. Then I need to understand your code. And without it being small it can be a time consuming task.

The way you create it is by

  • copying your code to a new file.
  • Running it to confirm that the problem is still there
  • Remove any code that is not really necessary and that still has the problem.
    • Do this in small steps and check a lot that the problem is still there. Using git stage every time you have minimized the problem a bit is a good process.

In the end you will either end up having understood where the problem is and maybe how it can be solved. Or you will have an minimum reproducible code example that is easy for others to help you with.

thanks.

Hey Marc,

Thanks for trying to help me.

I am working with jupyter right now and restarting the kernel and running all the cells doesn’t fix the issue so my guess is, that I am missing something somewhere. I think there is some issue with my sequencing and initializing the elements with values. Is there some way to control this, like the way I tried with disabeling the button?

Right now, when I restart the kernel, a huge error appears that is probably telling me that some error occurs because the values are empty. I tried to catch those but it seems one of them snuck by. When I randomly execute the elements one by one at some point it just starts working again to the point of my initial problem.

When I set up the widgets with content for the x and y axis and call the preview function it actually outputs the table in the format and the contents I am looking for but … it does not work inside some pn.pane()-ish Element (pn.Row, pn.Column and so on). It shows, as intended the empty table but after changing the elements that would call the preview function the new preview table isnt updated within my pane.

I’ll try to check if your tipps help me to conclude what the problem really is and hopefully have a more clear question following up :slight_smile: thanks!

1 Like

I found a crucial problem with my code, which is happening even earlier than the example I posted and now I am stuck again after trying to fix it. But I think I now somewhat understand how the @pn.depends operation works and what went wrong.

The documentation says that customized callback functions give a higher control for webapps build with panel but I’m wondering: do callbacks only work for parameterized objects and classes? The problem I am facing right now is that I have a lot of pieces of a huge puzzle and am trying to put them together each reacting to the other pieces. For example I have 4 database views and the User can choose between them. A certain view has different columns, so I created an overview table of what columsn there are in the specific view. And then from this very table I want to derive the columns that the user then can choose for x and y axis for a plot and so on… I think I even could use the pn.pipelin.Pipeline() and work out some Stages but I have the feeling that if I am using parameterized Classes I am going to hit a dead end, since it looks to me that I can only parameterize a variable if I beforehand know and can specify what values are possible for the variable, which I often don’t or want to chain from another object/piece of my puzzle.

So I’ve created a mini example that right now works (as a standalone programm) and I got a question about how to progress from there. When executing this I have the reactivity I want but I don’t have access to the elements I want. I would like to have acess to the values of the cross-selection element, how would i go about it?

## simulate data

import panel as pn
import pandas as pd

css = ['https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css']
js = {
    '$': 'https://code.jquery.com/jquery-3.4.1.slim.min.js',
    'DataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js'
}

pn.extension(css_files=css, js_files=js)


script = """
<script>
if (document.readyState === "complete") {
  $('.example').DataTable();
} else {
  $(document).ready(function () {
    $('.example').DataTable();
  })
}
</script>
"""



## load 4 different "views" of the main data.
basic = pd.DataFrame({'testA':[1,2,3,4,5,6,7,8,9,10]})
chain = pd.DataFrame({'testB':[1,2,3,4,5,6,7,8,9,10]})
ligand = pd.DataFrame({'testC':[1,2,3,4,5,6,7,8,9,10]})

db_views = {
    'Meta-Information': basic,
    'Chain': chain,
    'Ligand': ligand
}

## get additional information about the relevant tables
def get_meta_data(df):
    ## compute standard dataframe information
    rel_miss = pd.DataFrame({'variable': pd.isna(df).sum().index.values,
                             'relative_missings': pd.isna(df).sum() / df.shape[0],
                             'dtype': df.dtypes})

    ## compute number of unique instanes in a variabe for each column. 
    no_of_unique_values = []
    for col in df.columns:
        no_of_unique_values.append({"variable": col, 'nunique': df[col].nunique()})
    no_of_unique_values = pd.DataFrame.from_dict(no_of_unique_values)
    return no_of_unique_values.merge(rel_miss, on='variable')

# Interactive Element: RadioButton
# shall provide the user options at what kind of data he wants to look at.
data_views = pn.widgets.ToggleGroup(options=list(db_views.keys()), behavior='radio')

@pn.depends(data_views.param.value)
def get_additional_information(data_key):
#     html = db_views.get(data_key).head(5).to_html(classes=['example', 'panel-df'])
#     values = pn.pane.HTML(html+script, sizing_mode='stretch_width')
    html_meta = get_meta_data(db_views.get(data_key)).to_html(classes=['example', 'panel-df'])
    meta = pn.pane.HTML(html_meta + script, sizing_mode='stretch_width')
    return meta


# this function returns an CrossSelector Object with the variable names from a given table as its 
# Options.
@pn.depends(data_views.param.value)
def var_of_interest(data_key):
    return pn.widgets.CrossSelector(name='Variables of Interest',
                                                  value=[],
                                                  options= list(db_views.get(data_key).columns))


pn.Column(data_views,
          get_additional_information,
          var_of_interest).servable()

I would really like to add something like

@pn.depends(var_of_interest.param.value)
    def x_axis(columns):
        return pn.widgets.Select(name='X-Axis',
                                        options=columns,
                                        value='')

but functions dont have param.value or can I modify this in some way to make it possible? Or should I in general use a different approach for something like this? Thanks for your time!

Edit: humble push to @Marc to maybe take another look (if you find the time that is). Maybe another hint or two already help me to get going again.

Cheers,

Johannes

Not sure to understand fully
but something like this?

# this function returns an CrossSelector Object with the variable names from a given table as its 
# Options.
csw = pn.widgets.CrossSelector(name='Variables of Interest')
@pn.depends(data_views.param.value)
def var_of_interest(data_key):
    csw.options = list(db_views.get(data_key).columns)

@pn.depends(csw)
def value_of_csw(csw_value):
    return pn.pane.Str(csw_value, width=300)

pn.Column(data_views,
          get_additional_information,
          csw,
          var_of_interest,
          value_of_csw).servable()

Okay - so this already is something that isn’t fully clear to me yet and thus new. There is both the possibilty for objects and functions to ‘depend’ on each other? Puh, this is going to get messy real quick but I think I can work something out.

Do the return-statements of @pn.depends() functions always have to return something inside a pn.pane (Row, Column and so on)?

Thanks!

@thejonnyt No, functions decorated with @pn.depends do not necessarily have to return something inside a Panel. Is this what you’re after?

  • var_of_interest is now declared outside of your function
  • @pn.depends now includes watch=True
    • this is used when the function updates something but does not return something (at least I think so, I’m still learning)
  • renamed the function to reflect what it now does, which is just update an existing object
1 Like

If your app become big you should may be look for the parameterized class

class DatabaseManager(param.Parameterized):
    data_selection = param.Selector(default='Meta-Information', objects=list(db_views.keys()))
    var_of_interest = param.List(default=[])
    df_selected = param.DataFrame()
    
    def __init__(self, **params):
        super().__init__(**params)
        widgets={
            "data_selection": {
                "type": pn.widgets.RadioButtonGroup,
            },
            "var_of_interest": {
                "type": pn.widgets.CrossSelector,
                "name": 'Variables of Interest'
            }
        }
        self._tgw, self._csw = pn.Param(self, parameters=["data_selection", "var_of_interest"],
                                 widgets=widgets, sizing_mode='stretch_width', show_name=False)
        self._on_data_selection_change()
    
    def layout(self):
        return pn.Column(
            self._tgw,
            self.get_additional_information,
            self._csw,
        )
    
    @param.depends("data_selection", watch=True) # no return => watch=True
    def _on_data_selection_change(self):
        self.df_selected = db_views[self.data_selection]
        self._csw.options = list(self.df_selected.columns)
    
    @param.depends("var_of_interest", watch=True) # no return => watch=True
    def _on_var_of_interest_change(self):
        print(self.var_of_interest)
    
    @param.depends("df_selected") # side effect => watch=False
    def get_additional_information(self):
        html_meta = self.get_meta_data(self.df_selected).to_html(classes=['example', 'panel-df'])
        return pn.pane.HTML(html_meta + script, sizing_mode='stretch_width')

    ## get additional information about the relevant tables
    @staticmethod
    def get_meta_data(df):
        ## compute standard dataframe information
        rel_miss = pd.DataFrame({'variable': pd.isna(df).sum().index.values,
                                 'relative_missings': pd.isna(df).sum() / df.shape[0],
                                 'dtype': df.dtypes})

        ## compute number of unique instanes in a variabe for each column. 
        no_of_unique_values = []
        for col in df.columns:
            no_of_unique_values.append({"variable": col, 'nunique': df[col].nunique()})
        no_of_unique_values = pd.DataFrame.from_dict(no_of_unique_values)
        return no_of_unique_values.merge(rel_miss, on='variable')


db = DatabaseManager(data_selection='Chain')
db.layout()
2 Likes

Thanks! I am going to try this if I find the time. Things are working nicely right now, thanks to everybody, now but using classes would definitly improve the readability and performance big time.

The examples in the documentation are nice already but I feel it always helps to talk to people about it and I had no arround one working on a similar thing so big big thanks to all of you.

2 Likes