Updating matplotlib figure in Parameterized class

I have a matplotlib figure in my Parmeterized class which I’d like to draw once initially and then update the axes and display. I have a MWE as shown below.

My problem is I cannot get the mpl figure to update. When I press the button I can confirm by checking the output of mpl.fig in another jupyter notebook cell that the figure has been updated and now has data in it. When I adjust the max_value parameter I can see the ‘view trigger’ print so I know that a new pane was returned. However, the figure does not update.

How can I get the figure to trigger an update? I do not want to redraw the whole figure. I feel like I need some kind of mpl_pane.param.trigger('object') trigger but I do not know how to trigger this since I cannot acces the mpl pane instance within my MPLMEW class with the current implementation.
In general, I’d like to update the figure when the _action_update method is done, is there any way of hooking up some kind of event trigger for this?

Thanks!

import numpy as np
import param
import panel as pn
import matplotlib.pyplot as plt
import matplotlib
pn.extension()
matplotlib.use('agg')

class MPLMWE(param.Parameterized):
    max_value = param.Number(default=20)
    update = param.Action(lambda self: self._action_update())
    
    def __init__(self, **params):
        super(MPLMWE, self).__init__(**params)
        self.fig, self.ax = plt.subplots()
        
    
    def _action_update(self):
        # long calculation
        print('update')
        y = np.arange(10)**2
        y = np.clip(y, None, self.max_value)
        self.ax.clear()
        self.ax.plot(y)
        print(self.ax.lines)
        
    @param.depends('max_value')
    def view(self):
        print('view trigger')
        return pn.pane.Matplotlib(self.fig)
            
    def panel(self):
        return pn.Column(self.param, self.view)
    
mpl = MPLMWE()
p = mpl.panel()
p.app()

Software info

Panel 0.7.0
Bokeh 1.4.0
Tornado 6.0.3
Jupyter Notebook
Mozilla Firefox
Windows 10

Here’s how I would write this, instead of creating a new Pane each time you can simply tell the existing MPL pane to rerender.

class MPLMWE(param.Parameterized):
    max_value = param.Number(default=20)
    update = param.Action(lambda self: self._action_update())
    
    def __init__(self, **params):
        super(MPLMWE, self).__init__(**params)
        self.fig, self.ax = plt.subplots()
        self.mpl = pn.pane.Matplotlib(self.fig)
        self.ax.autoscale()

    @param.depends('update')    
    def _action_update(self):
        # long calculation
        y = np.arange(10)**2
        y = np.clip(y, None, self.max_value)
        self.ax.clear()
        self.ax.plot(y)
        self.mpl.param.trigger('object')
            
    def panel(self):
        return pn.Column(self.param, self.mpl)

Let me know if this helps or whether you need more clarification.

Great, that solved my problem. I was somehow under the imporession that the Matplotlib pane had to originate from a view function with a @depends wrapper.

Thanks!

Thanks Philipp, that got me a big step further as well. I have a similarly structured app, but without the long calculation. Hence I would like continuous updates. But simply changing

    @param.depends('update')    

into

    @param.depends('max_value')    

doesn’t seem to cut it. Although the Update button still works, no automatic plotting happens if I change the number. What am I missing here?

Add watch=True for a param parameter change to take effect in an update function that doesn’t return anything.

class MPLMWE(param.Parameterized):
    max_value = param.Number(default=20)
    
    def __init__(self, **params):
        super(MPLMWE, self).__init__(**params)
        self.fig, self.ax = plt.subplots()
        self.mpl = pn.pane.Matplotlib(self.fig);
        self.ax.autoscale()

    @param.depends('max_value', watch=True)    
    def _action_update(self):
        # long calculation
        y = np.arange(10)**2
        y = np.clip(y, None, self.max_value)
        self.ax.clear()
        self.ax.plot(y)
        self.mpl.param.trigger('object')
            
    def panel(self):
        return pn.Column(self.param, self.mpl)

For reference, watch=True wasn’t required in the solution given by @philippjfr.
The callback specified with the button ensured the update.

update = param.Action(lambda self: self._action_update())

You can leave out the @param.depends('update').

1 Like

You’re right, of course. Thank you! I always forget about the watch

1 Like