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.
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()