Handling returns from button.on_click

Hello,

In my dashboard, I am trying to create a button that gets data from a number of sources and saves it into a Pandas DataFrame, like this:

import pandas as pd
import panel as pn

df = 'Whatever'
button = pn.widgets.Button(name='Get data', button_type='primary')

def getData(event):
    df = pd.read_csv('mydata.csv')
    return df

df = button.on_click(getData)

button

With the code above, pushing the button leaves the df variable empty, since on_click doesn’t actually return anything.

I have worked around the problem by using a global variable to change df directly from within the getData function, like this:

def getData(event):
    global df 
    df = pd.read_csv('mydata.csv')

button.on_click(getData)

I am quite new to Python and even newer to coding UI:s, but I have read that using global variables should be avoided as much as possible, so I am wondering whether there is a better way to do this?

UPDATE: More recent version below

HI @dabbatelli

Welcome to the community. Good question.

There are several ways to accomplish what you want to do. I love to used param.Parameterized classes. The reason being

  • You will get code that can be used from code and without the UI. But of course also from the UI.
  • You will get code that can be maintained and further developed over time. I.e. a solution that can scale.

So I will provide a solution based on param.Parameterized.

I know there are a few things to learn if you are a new user to Python and Panel. Feel free to ask questions.

I hope others will contribute their preferred solution.

image

"""Answer to [Discourse Question]\
(https://discourse.holoviz.org/t/handling-returns-from-button-on-click/733)"""
import pandas as pd
import panel as pn
import param

class MyDataframeExtractorApp(param.Parameterized):
   """Enables a user to extract and view a DataFrame"""

   data = param.DataFrame(precedence=2)
   extract = param.Action(label="Get data", precedence=1)
   view = param.Parameter()

   def __init__(self, **params):
       params["data"] = pd.DataFrame()
       params["extract"] = self._extract
       params["view"] = self._get_view()

       super().__init__(**params)

   def _extract(self, event=None):
       # self.data = pd.read_csv('mydata.csv')
       self.data = pd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 6]})

   def _get_view(self):
       top_app_bar = pn.Row(
           pn.pane.Markdown("# Panel Data Extractor"),
           sizing_mode="stretch_width",
       )

       content = pn.Param(
           self,
           parameters=["extract", "data"],
           widgets={
               "extract": {"button_type": "success", "width": 150, "sizing_mode": "fixed"},
               "data": {
                   "height": 600
               },  # Needed due to https://github.com/holoviz/panel/issues/919
           },
           show_name=False,
       )

       return pn.Column(
           top_app_bar,
           content,
           max_width=1000,
           sizing_mode="stretch_width",
       )

app = MyDataframeExtractorApp()
app.view # In notebook
# app.view.servable() # with panel serve

Hello!

I had been reading about parameterized classes in the documentation, but I had not looked into it closely: I guess now it is a good time to start reading more about it.

Thanks for the great example!

1 Like

I get a:
AttributeError: ‘MyDataframeExtractorApp’ object has no attribute ‘_param_watchers’

when I run this in a notebook.

Hi @gbl

Try changing the __init__ method to

def __init__(self, **params):
        params["data"] = pd.DataFrame()
        params["extract"] = self._extract

        super().__init__(**params)

        self.view = self._get_view()

It works for me

Panel 1.1.1 version

With Panel 1.1.1 I would probably do something like

"""Answer to [Discourse Question]\
(https://discourse.holoviz.org/t/handling-returns-from-button-on-click/733)"""
import pandas as pd
import panel as pn
import param


class MyDataframeExtractorApp(pn.viewable.Viewer):
    """Enables a user to extract and view a DataFrame"""

    data = param.DataFrame(precedence=2)
    extract = param.Action(label="Get data", precedence=1)

    def __init__(self, **params):
        params["data"] = pd.DataFrame(columns=["x", "y"])
        params["extract"] = self._extract

        super().__init__(**params)

        self._view = None

    def _extract(self, event=None):
        # self.data = pd.read_csv('mydata.csv')
        self.data = pd.DataFrame({"x": list(range(0, 100)), "y": list(range(0, 100))})

    def _get_view(self):
        if not self._view:
            top_app_bar = pn.Row(
                pn.pane.Markdown("# Panel Data Extractor"),
                sizing_mode="stretch_width",
            )
            extract_button = pn.widgets.Button.from_param(
                self.param.extract, width=150, sizing_mode="fixed", button_type="primary"
            )
            table = pn.pane.Perspective(object=self.param.data, height=600)
            self._view = pn.Column(
                top_app_bar,
                extract_button,
                table,
                max_width=1000,
                sizing_mode="stretch_width",
            )

        return self._view

    def __panel__(self):
        return self._get_view()


# In notebook:
# pn.extension("perspective", sizing_mode="stretch_width", design="bootstrap")
# app = MyDataframeExtractorApp()
# pn.panel(app)

if pn.state.served:  # with panel serve
    pn.extension("perspective", sizing_mode="stretch_width", template="fast")
    app = MyDataframeExtractorApp()

    app.servable()

That fixes it :+1:

1 Like

Thanks for the update to Panel 1.1.1

1 Like