Search code examples
pythonmatplotlibpyqt6

Updating a matplotlib canvas in PyQt6 by providing a figure object


I am building a GUI with PyQt6 which has a part for graph visualization based on matplotlib.

The graphs to be visualized are entirely built inside functions that return an already assembled and ready to be plotted matplotlib's figure object - in this case, a matplotlib.figure.Figure object not natively managed by pyplot (and I preferrable wouldn't change this). Now, I am struggling to discover how can I (or even if it is possible) to plot a figure object in the same matplotlib canvas that a previous figure object has been plotted, without erasing the previous plot.

Here is a minimum working example of the matplotlib widget I have. The method "update_figure" is used to plot the first figure object to the canvas (and it is already working). Then, the method "add_figure" would be used to plot the subsequent figure objects. In both methods, the variable "new_figure" is a matplotlib figure object.

# ------------------------------------------------------
# -------------------- mplwidget.py --------------------
# ------------------------------------------------------
from PyQt6.QtWidgets import*
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
from PyQt6.QtGui import QFont, QFontInfo
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Qt5Agg')
    
class MplWidget(QWidget):
    
    def __init__(self, parent = None):
        super(QWidget, self).__init__()
        self.canvas = FigureCanvasQTAgg(Figure()) 
        vertical_layout = QVBoxLayout()
        vertical_layout.addWidget(self.canvas)
        self.canvas.axes = self.canvas.figure.add_subplot(111)
        self.canvas.draw()
        
        #Get the default font style from the system and apply to the canvas
        self.resetFontStyle()
        self.setLayout(vertical_layout)
    
    def update_figure(self, new_figure):
        # new_figure is a matplotlib figure object
        # Clear the existing figure content
        self.canvas.figure.clf()
        self.canvas.axes.cla()
        # Copy the contents of the new figure onto the canvas
        self.canvas.figure = new_figure
        # Redraw the canvas
        self.canvas.draw()

    def add_figure(self, new_figure):
        # new_figure is a matplotlib figure object
        #This is what I want to implement

I tried to see if there would be some way to copy the entire information of the figure object and inserted in the current axis of the canvas, but had no luck with that.


Solution

  • In case anyone come across this post, I have devised two alternatives to deal with this problem:

    1. Use the copy module and the deepcopy method to copy the new_figure object into self.canvas.figure. This appears to take some time to update the figure
    2. Simply erase self.canvas.figure.clf() and self.canvas.axes.cla(). This appears (up until now in my tests) to allow the canvas to be refreshed.

    My original problem is that I did not notice that when I was passing new_figure to the function update_figure, and "assigning" it to self.canvas.figure, what I was really doing was making self.canvas.figure refer to the same matplotlib.figure.Figure object. Then, when I was trying to update the canvas with a new_figure, the methods self.canvas.figure.clf() and self.canvas.axes.cla() were acting upon the same object as referred by new_figure, virtually erasing its data.

    The reason the first solution above works is because when deepcoping an object, we decouple self.canvas.figure from new_figure, and then can perform .clf() and .clear()

    The second solution is, however, my preferred because it does not involve duplicating objects. I don't recall why now, but I assumed that simple calling self.canvas.draw() would stack up old plots and I would have to clear the figure first. But now I notice this was not the case and the GUI seems to work as intended.