pythonmatplotlib

Problem clearing errorbar artists from dynamic matplotlib figure


My goal is to clear and redraw errorbar data on a matplotlib plot while reusing the same background and axes. This means I cannot just clear the figure. I can do this trivially with plot by storing and removing the artists, but once I start using errorbar, I run into problems.

As an example, the below script will generate a plot of random data, and provides a button that clears that data without clearing the figure and generates/displays new random data.

import mplcursors
import matplotlib.pyplot as plt
from matplotlib.widgets import Button

class RandomDataPlotter:
    def __init__(self):
        self.fig, self.ax = plt.subplots()
        self.fig.subplots_adjust(bottom=0.2)
        self.current_data = None
        self.current_errorbars = None
        self.cursor = None

        # Add a button to toggle data
        self.button_ax = self.fig.add_axes([0.7, 0.05, 0.1, 0.075])
        self.button = Button(self.button_ax, 'Toggle Data')
        self.button.on_clicked(self.toggle_data)

        self.plot_random_data()

    def plot_random_data(self):
        # Clear existing artists
        if self.current_data:
            self.current_data.remove()
        if self.current_errorbars:
            for artist in self.current_errorbars:
                artist.remove()
        if self.cursor:
            self.cursor.remove()

        # Generate random data
        x = np.linspace(0, 10, 100)
        y = np.random.rand(100)
        y_err = np.random.rand(100) * 0.1

        # Plot data with error bars
        self.current_data, _, self.current_errorbars = self.ax.errorbar(x, y, yerr=y_err, fmt='o', color='blue')

        # Attach hover cursor
        self.cursor = mplcursors.cursor(self.ax, hover=True)
        self.cursor.connect("add", lambda sel: sel.annotation.set_text(f"x: {sel.target[0]:.2f}\ny: {sel.target[1]:.2f}"))

        # Redraw the canvas
        self.ax.relim()
        self.ax.autoscale_view()
        self.fig.canvas.draw_idle()

    def toggle_data(self, event):
        self.plot_random_data()

if __name__ == "__main__":
    plotter = RandomDataPlotter()
    plt.show()

This works on the first iteration/button press, however, something happens after that which generates the following error:

AttributeError: 'NoneType' object has no attribute 'canvas'
Traceback (most recent call last):
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/cbook.py", line 361, in process
    func(*args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 244, in <lambda>
    return self._observers.connect('clicked', lambda event: func(event))
                                                            ~~~~^^^^^^^
  File "/Users/atom/hemanpro/HeMan/test_files/test2.py", line 49, in toggle_data
    self.plot_random_data()
    ~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/atom/hemanpro/HeMan/test_files/test2.py", line 40, in plot_random_data
    self.cursor = mplcursors.cursor(self.ax, hover=True)
                  ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/mplcursors/_mplcursors.py", line 744, in cursor
    return Cursor(artists, **kwargs)
  File "/Users/atom/hemanpro/HeMan/.venv/lib/python3.13/site-packages/mplcursors/_mplcursors.py", line 264, in __init__
    for canvas in {artist.figure.canvas for artist in artists}]
                   ^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'canvas'

My best guess at what's happening here is that there are leftover artists that no longer have a figure/canvas attached to them, which are generating an error when we try to attach the hover cursor. I have no idea how to clear this, however. Any assistance would be greatly appreciated.


Solution

  • The errorbar method returns an ErrorbarContainer instance which is itself an artist containing the lines, etc. Currently you are only removing the child artists and not the ErrorbarContainer, and I think it is that leftover container causing the error. Calling the remove method on the container should remove itself and all its children, so you can just keep track of that, which makes things simpler. However, there is currently a bug in Matplotlib and the method is not working as it should. We can work around that by directly removing it from the axes' containers list.

    import mplcursors
    import matplotlib.pyplot as plt
    from matplotlib.widgets import Button
    import numpy as np
    
    class RandomDataPlotter:
        def __init__(self):
            self.fig, self.ax = plt.subplots()
            self.fig.subplots_adjust(bottom=0.2)
            self.current_errorbar_container = None
    
            # Add a button to toggle data
            self.button_ax = self.fig.add_axes([0.7, 0.05, 0.1, 0.075])
            self.button = Button(self.button_ax, 'Toggle Data')
            self.button.on_clicked(self.toggle_data)
    
            self.plot_random_data()
    
        def plot_random_data(self):
            # Clear existing artists
            if self.current_errorbar_container:
                self.current_errorbar_container.remove()
                
                # Explicitly remove errorbar from containers list to workaround bug
                # https://github.com/matplotlib/matplotlib/issues/25274
                self.ax.containers.remove(self.current_errorbar_container)
    
            # Generate random data
            x = np.linspace(0, 10, 100)
            y = np.random.rand(100)
            y_err = np.random.rand(100) * 0.1
    
            # Plot data with error bars
            self.current_errorbar_container = self.ax.errorbar(x, y, yerr=y_err, fmt='o', color='blue')
    
            # Attach hover cursor
            self.cursor = mplcursors.cursor(self.ax, hover=True)
            self.cursor.connect("add", lambda sel: sel.annotation.set_text(f"x: {sel.target[0]:.2f}\ny: {sel.target[1]:.2f}"))
    
            # Redraw the canvas
            self.ax.relim()
            self.ax.autoscale_view()
            self.fig.canvas.draw_idle()
    
        def toggle_data(self, event):
            self.plot_random_data()
    
    if __name__ == "__main__":
        plotter = RandomDataPlotter()
        plt.show()