Button triggers function which triggers plot update

I am new to panel. I’ve set up the following workflow in an ipynb notebook and am able to serve it successfully:

  1. call a long running process which returns a dataframe
  2. create a plot of that dataframe

All very simple.

I am having a lot of trouble trying to add the following:

The long running function has 5 parameters. I have widgets for each of the parameters. However, I don’t want the function to run when a param is changed (since it is long running and the user may want to change multiple params before triggering a re-run). I want the function to run with the new parameters when a button is clicked, and when the function is done, the plot should refresh.

Is there a demo somewhere of this kind of functionality?

An example using a Parameterized class is here.

The “Load Structure” button runs a function based on values of other widgets.

site: NGL Molecule Viewer (awesome-panel.org)
code: awesome-panel/ngl_molecule_viewer.py at master · MarcSkovMadsen/awesome-panel · GitHub

1 Like

If you would rather just use widgets. Then take a look at the `Button" reference guide Button — Panel 0.11.3 documentation.

You just need to specify a function to the the Button.on_click method. Then the function specified can use the values from the other widgets.

1 Like

Thank you so much for replying!

I am trying to mirror the NGL Molecule Viewer example:

import numpy as np
import matplotlib.pyplot as plt
import panel as pn
import param

class ButtonApp(param.Parameterized):
    run_btn = param.Action(label="Click Me!")
    
    def __init__(self, **params):
        super().__init__(**params)
        self.run_btn = self._run_btn
        self._run_btn(100)
        self._make_plot()
    
    def _run_btn(self, n, *args):
        self.rands = np.random.random(n)
    
    def _make_plot(self):
        fig, ax = plt.subplots()
        ax = plt.hist(self.rands)
        plt.close()
        self.fig = fig
        
    def view(self):
        app = pn.template.MaterialTemplate()
        pn.config.sizing_mode = 'stretch_width'
        
        settings = pn.Column(
            *pn.Param(
                self.param, widgets={'run_btn': {"button_type": "primary"}}, show_name=False
            ),
        )

        app.sidebar[:] = [settings]
        app.main[:] = [pn.Card(self.fig)]
        return app

pn.extension()
viewer = ButtonApp()
viewer.view().servable()

This loads fine:

However, when I click “Click Me” I get the following error thrown:

2021-06-11 12:34:26,005 Exception in callback functools.partial(<function wrap.<locals>.null_wrapper at 0x7fc8ba84af80>, <Task finished coro=<_needs_document_lock.<locals>._needs_document_lock_wrapper() done, defined at /home/jovyan/.local/lib/python3.7/site-packages/bokeh/server/session.py:51> exception=TypeError('expected sequence object with len >= 0 or a single integer')>)
Traceback (most recent call last):
  File "/home/jovyan/.local/lib/python3.7/site-packages/tornado/ioloop.py", line 758, in _run_callback
    ret = callback()
  File "/home/jovyan/.local/lib/python3.7/site-packages/tornado/stack_context.py", line 300, in null_wrapper
    return fn(*args, **kwargs)
  File "/home/jovyan/.local/lib/python3.7/site-packages/tornado/ioloop.py", line 779, in _discard_future_result
    future.result()
  File "/home/jovyan/.local/lib/python3.7/site-packages/bokeh/server/session.py", line 71, in _needs_document_lock_wrapper
    result = await result
  File "/home/jovyan/.local/lib/python3.7/site-packages/tornado/gen.py", line 307, in wrapper
    result = func(*args, **kwargs)
  File "/opt/conda/lib/python3.7/types.py", line 277, in wrapped
    coro = func(*args, **kwargs)
  File "/home/jovyan/.local/lib/python3.7/site-packages/panel/reactive.py", line 252, in _change_coroutine
    self._change_event(doc)
  File "/home/jovyan/.local/lib/python3.7/site-packages/panel/reactive.py", line 262, in _change_event
    self._process_events(events)
  File "/home/jovyan/.local/lib/python3.7/site-packages/panel/reactive.py", line 245, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/home/jovyan/.local/lib/python3.7/site-packages/param/parameterized.py", line 1472, in set_param
    self_._batch_call_watchers()
  File "/home/jovyan/.local/lib/python3.7/site-packages/param/parameterized.py", line 1611, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/home/jovyan/.local/lib/python3.7/site-packages/param/parameterized.py", line 1573, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/home/jovyan/.local/lib/python3.7/site-packages/panel/param.py", line 448, in action
    value(self.object)
  File "/home/jovyan/panel-repro/Button.ipynb", line 28, in _run_btn
    "        self._make_plot()\n",
  File "mtrand.pyx", line 430, in numpy.random.mtrand.RandomState.random
  File "mtrand.pyx", line 421, in numpy.random.mtrand.RandomState.random_sample
  File "_common.pyx", line 256, in numpy.random._common.double_fill
TypeError: expected sequence object with len >= 0 or a single integer

What am I doing wrong with my code?

I was able to achieve what you seem to be looking for if you want to use only param.

import panel as pn
import hvplot.pandas
import pandas as pd
import numpy as np
import param

pn.extension()

def create_df(nb_point,amplitude,frequencies):
    dt=4*np.pi/nb_point
    index=np.array([dt*i for i in range(nb_point)])
    datas=amplitude*np.sin(frequencies*index)
    df_sin=pd.DataFrame(data=datas,index=index,columns=['val'])
    return df_sin

class basicApp(param.Parameterized):
    wdgt_point = param.Integer(default=100, bounds=(0, 1000))
    wdgt_ampl = param.Integer(default=2, bounds=(0, 5))
    wdgt_freq = param.Number(1.0, bounds=(0, 6.28))
    update = param.Action(lambda x: x.param.trigger('update'), label='Click to update')

    @param.depends('update')
    def view(self):
        df=create_df(self.wdgt_point,self.wdgt_ampl,self.wdgt_freq)
        return df.hvplot().opts(width=500,height=300)

basic_app=basicApp()
pn.Row(basic_app.param,basic_app.view)

I used param.Action to trigger the update and the function used the values of the other param to compute the dataframe.

Hope it can help you

2 Likes

Thanks @KevinMttn . I’ll try that approach.

2 Likes

@Marc , I tried the simple approach as well with no luck:

import numpy as np
import matplotlib.pyplot as plt
import panel as pn

app = pn.template.MaterialTemplate()
pn.config.sizing_mode = 'stretch_width'

button = pn.widgets.Button(name='Click me', button_type='primary')

def make_fig():
    fig, ax = plt.subplots()
    ax = plt.hist(np.random.random(10))
    plt.close()
    return fig

fig = make_fig()

settings = pn.Column(button)
app.sidebar[:] = [settings]
app.main[:] = [pn.Card(fig)]

def b(event):
    fig = make_fig()
    app.main[:] = [pn.Card(fig)]
    
button.on_click(b)

This serves up fine. However when I click the button, nothing happens (no error thrown either). I am lost as to why this is not working.

Hi @jlarkin

The problem is that the template parameters like sidebar and main cannot update for technical reasons once the template has been served. Only layouts, panes and widgets can do that.

So the trick is to place the fig inside a pane like pn.pane.Matplotlib that can be updated dynamically.

import matplotlib.pyplot as plt
import numpy as np
import panel as pn
from matplotlib.backends.backend_agg import \
    FigureCanvas  # not needed for mpl >= 3.1

pn.extension(sizing_mode="stretch_width")

def make_fig():
    fig, _ = plt.subplots()
    plt.hist(np.random.random(10))
    plt.close()
    FigureCanvas(fig) # not needed for mpl >= 3.1
    return fig

fig_container = pn.pane.Matplotlib(make_fig())

button = pn.widgets.Button(name="Click me", button_type="primary")
def b(event):
    fig_container.loading = True
    fig = make_fig()
    fig_container.object = fig
    fig_container.loading = False

button.on_click(b)

app = pn.template.MaterialTemplate(
    site="Awesome Panel",
    title="Updating Matplotlib",
    sidebar = [button],
    main = [pn.Card(fig_container)],
)

app.servable()
1 Like

Fantastic. Thank you! :sunglasses:

1 Like

One last question :slight_smile:

I am trying to “mix” the styles of updates. When the user clicks the button, the function runs and the plot updates; also when the user changes a slider the plot updates. I have used the decorator @pn.depends(…) before, but unclear about how to use it in this case. If I put the @pn.depends(…) on make_fig() then of course the container doesn’t update. Is there a recommended way to make this happen?

1 Like

Hi @jlarkin,

I think it is much easier to use Python class with Param Action as the button, as shown in the link below and the example by @KevinMttn above, and you can combine it with other Param parameter or Panel widget objects.
https://panel.holoviz.org/gallery/param/action_button.html

Modifying slightly the @Marc code,

import matplotlib.pyplot as plt

import numpy as np
import panel as pn
from matplotlib.backends.backend_agg import \
    FigureCanvas  # not needed for mpl >= 3.1

pn.extension(sizing_mode="stretch_width")

slider = pn.widgets.FloatSlider(name='Amplitude', start=0, end=100)

@pn.depends(slider, watch=True)
def make_fig(value):
    print (value)
    fig, _ = plt.subplots()
    plt.hist(np.random.random(10)*value)
    plt.close()
    FigureCanvas(fig) # not needed for mpl >= 3.1
    fig_container.object = fig

fig_container = pn.pane.Matplotlib(plt.figure(), sizing_mode='stretch_width')

button = pn.widgets.Button(name="Click me", button_type="primary")
def b(event):
    fig_container.loading = True
    fig = make_fig(slider.value)
    fig_container.loading = False

button.on_click(b)

app = pn.template.MaterialTemplate(
    site="Awesome Panel",
    title="Updating Matplotlib",
    sidebar = [pn.Column(button,slider)],
    main = [pn.Card(fig_container)],
)

app.servable()
2 Likes