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