matplotlib and PyQt: Dynamic figure runs slow after several loads or looks messy

EDIT:

I decided to rewrite this to include a working example of my problem. Although this is pretty long, I hope that it proves to be useful for many in the future.

import sys

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from PyQt4.QtGui import *
from PyQt4.QtCore import *

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setGeometry(100, 100, 640, 480)
        showButton = QPushButton('Show')

        toolbarShowButton = self.addToolBar('Show button toolbar')

        toolbarShowButton.addWidget(showButton)

        self.connect(showButton, SIGNAL('clicked()'), self.showButtonClicked)
        self.graphLabel = GraphCanvas(self);
        self.setCentralWidget(self.graphLabel)

    def showButtonClicked(self):  
        self.graphLabel.drawGraph()

    def resizeEvent(self, event):
        try:
            self.graphLabel.setFig()
        except AttributeError:
            pass

class GraphCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):

        self.fig = Figure(figsize=(width, height), dpi=dpi)

        self.axes = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

        self.background = None

    def drawGraph(self):
        self.axes.cla()
        self.someplot = self.axes.plot(range(1,5), range(1,5))
        self.redVert, = self.axes.plot(None, None, 'r--')
        self.greenVert, = self.axes.plot(None, None, 'g--')
        self.yellowVert, = self.axes.plot(None, None, 'y--')

        self.verticalLines = (self.redVert, self.greenVert, self.yellowVert)

        self.fig.canvas.mpl_connect('motion_notify_event', self.onMove)

        self.draw()
        self.background = self.fig.canvas.copy_from_bbox(self.axes.bbox)

    def onMove(self, event):

        # cursor moves on the canvas
        if event.inaxes:

            # restore the clean background
            self.fig.canvas.restore_region(self.background)
            ymin, ymax = self.axes.get_ylim()
            x = event.xdata - 1

            # draw each vertical line
            for line in self.verticalLines:
                line.set_xdata((x,))
                line.set_ydata((ymin, ymax))
                self.axes.draw_artist(line)
                x += 1

            self.fig.canvas.blit(self.axes.bbox)

    def setFig(self):
        '''
        Draws the canvas again after the main window
        has been resized.
        '''

        try:
            # hide all vertical lines
            for line in self.verticalLines:
                line.set_visible(False)

        except AttributeError:
            pass

        else:
            # draw canvas again and capture the background
            self.draw()
            self.background = self.fig.canvas.copy_from_bbox(self.axes.bbox)

            # set all vertical lines visible again
            for line in self.verticalLines:
                line.set_visible(True)

def main():
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

if __name__ == '__main__': main()

Description of the code

I have A basic QMainWindow with a toolbar that has a "show" button. The main window also creates a canvas for a matplotlib figure and sets it to be the central widget.

When the user hits the "show" button, some data is shown by calling the drawGraph() method of GraphCanvas. In a real program that data changes depending on what the user has selected to be shown prior to clicking the button. The method resizeEvent() basically draws the figure again to accommodate the new central widget size.

The drawGraph() method creates four plots of which the first one has data, so it's visible. The last two lines draw the figure and saves the static background to the variable self.background.

When the user moves the mouse on the canvas, the background is first loaded. I wanted to save and load the static background to make the figure draw faster. After that, the last three plots get their data dynamically and are shown as 3 vertical lines that move with the mouse cursor.

The problem

1) The figure becomes gradually slower as you keep clicking the "show" button. If you try hitting it like 50 times and move the mouse on the figure, you see that the vertical lines are much more laggy. When there's much more dynamic plots and annotations etc. in a real figure, the program becomes unusable after just a few clicks.

Someone wiser can probably tell why this slowdown is happening, but I guess that the previously loaded figures are kept somewhere in the memory and maybe drawn underneath the newly created figure. And the stack just keeps getting bigger and bigger.

2) The figure is shown right after starting the program. Not a huge deal, but I would prefer just a blank area there until the button is clicked.

A solution tried

What I tried was that I moved these two lines from def __init__(self) of class MainWindow to def showButtonClicked(self):

self.graphLabel = GraphCanvas(self);
self.setCentralWidget(self.graphLabel)

So it looks now like this:

def showButtonClicked(self):  
    self.graphLabel = GraphCanvas(self);
    self.setCentralWidget(self.graphLabel)
    self.graphLabel.drawGraph()

So I created the figure only after the button is pressed. That solves the slowdown problem but brings in another problem. Now when you hit the "show" button and move the mouse on the canvas, the saved background of the figure is loaded but in the original size of 5 by 4 inches and we have a mess. So basically the background was saved not in the size the figure was drawn, but in the size it was created in.

If I resize the window, however, everything works nicely. But the next time I click the "show" button, the problem reappears and I need to resize the window again.

What I need

I need to make this thing work fluidly and to look as it should no matter how many times the "show" button is clicked. Also, I would prefer if the figure didn't show until the "show" button is clicked for the first time and from that point on be visible until the program is closed.

A few hacks come to mine like resizing the window one pixel when the "show" button is clicked, but that's not the right approach, is it?

Any ideas and suggestions are more than welcome. Thank you.


Solution 1:

I have found a decent solution for the problem which will do until a better one is found.

The reason why the solution I tried caused a mess is that once an instance of GraphCanvas is created and set as a QCentralWidget, the QCentralWidget shrinks to the size of the GraphCanvas instance which is 500*400 in this case, and a Bbox of that size is saved. However, the figure itself uses the whole available space.

When you create a GraphCanvas and set is as the QCentralWidget, the widget uses the size of the GraphCanvas instance until the method it was created in (and all its parents) has finished executing. After that they both line up.

If we create the canvas in the __init__ method, it doesn't cause a mess, because in the drawGraph method the size of QCentralWidget and GraphCanvas match and the right bbox size is used. However, when we create it in showButtonClick, the bbox will be in 'wrong' size until the method has finished.

In addition, if we create a QCentralWidget in the __init__ method of QMainWindow, the size of it will match the size of the whole window set by self.setGeometry(). After the method as finished, the size of QCentralWidget will be calculated to fit in the window and usually becomes smaller.

To solve this problem, I decided to create a dummy QWidget in the __init__ method and add that as QCentralWidget. Then in the showButtonClicked method I grab the width and height of the QCentralWidget and create a GraphCanvas using the saved width and height. That way the sizes match right from the beginning.

So here's the relevant code:

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setGeometry(100, 100, 640, 480)
        showButton = QPushButton('Show')

        toolbarShowButton = self.addToolBar('Show button toolbar')

        toolbarShowButton.addWidget(showButton)
        self.connect(showButton, SIGNAL('clicked()'), self.showButtonClicked)

        # dummy QWidget
        tempWidget = QWidget()
        self.setCentralWidget(tempWidget)

    def showButtonClicked(self):

        width = self.centralWidget().width()
        height = self.centralWidget().height()

        # convert to float (python 2) to prevent
        # flooring in the following divisions
        dpi = float(100)

        # create the canvas and replace the central widget
        self.graphLabel = GraphCanvas(self, width=width/dpi, height=height/dpi, dpi=dpi);
        self.setCentralWidget(self.graphLabel)

        self.graphLabel.drawGraph()