Some years ago, I already experimented with embedding live matplotlib
plots in a PyQt5
GUI. Live plots show a data-stream real-time, captured from a sensor, some process, ... I got that working, and you can read the related posts here:
Now I need to do the same thing again. I remember my previous approach worked, but couldn't keep up with fast datastreams. I found a couple of example codes on the internet, that I'd like to present to you. One of them is clearly faster than the other, but I don't know why. I'd like to gain more insights. I believe a deeper understanding will enable me to keep my interactions with PyQt5
and matplotlib
efficient.
1. First example
This example is based on this article:
https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html
The article is from the official matplotlib
website, and explains how to embed a matplotlib figure in a PyQt5
window.
I did a few minor adjustments to the example code, but the basics are still the same. Please copy-paste the code below to a Python file and run it:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# EXAMPLE 1 #
# ------------------------------------ #
# This code is inspired on: #
# https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import numpy as np
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt - example 1")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
super().__init__(mpl.figure.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
self._x_ = list(range(0, x_len))
self._y_ = [0] * x_len
# Store a figure ax
self._ax_ = self.figure.subplots()
# Initiate the timer
self._timer_ = self.new_timer(interval, [(self._update_canvas_, (), {})])
self._timer_.start()
return
def _update_canvas_(self) -> None:
'''
This function gets called regularly by the timer.
'''
self._y_.append(round(get_next_datapoint(), 2)) # Add new datapoint
self._y_ = self._y_[-self._x_len_:] # Truncate list _y_
self._ax_.clear() # Clear ax
self._ax_.plot(self._x_, self._y_) # Plot y(x)
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self.draw()
return
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
You should see the following window:
2. Second example
I found another example of live matplotlib
graphs here:
https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation
However, the author doesn't use PyQt5
to embed his live plot. Therefore, I've modified the code a bit, to get the plot in a PyQt5
window:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# EXAMPLE 2 #
# ------------------------------------ #
# This code is inspired on: #
# https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import numpy as np
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt - example 2")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
FigureCanvas.__init__(self, mpl_fig.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
x = list(range(0, x_len))
y = [0] * x_len
# Store a figure and ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
self._line_, = self._ax_.plot(x, y)
# Call superclass constructors
anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
return
def _update_canvas_(self, i, y) -> None:
'''
This function gets called regularly by the timer.
'''
y.append(round(get_next_datapoint(), 2)) # Add new datapoint
y = y[-self._x_len_:] # Truncate list _y_
self._line_.set_ydata(y)
return self._line_,
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
The resulting live plot is exactly the same. However, if you start playing around with the interval
parameter from the MyFigureCanvas()
constructor, you will notice that the first example won't be able to follow. The second example can go much faster.
3. Questions
I've got a couple of questions I'd like to present to you:
The
QtCore
andQtWidgets
classes can be imported like this:
from matplotlib.backends.qt_compat import QtCore, QtWidgets
or like this:
from PyQt5 import QtWidgets, QtCore
Both work equally well. Is there a reason to prefer one over the other?
The
FigureCanvas
can be imported like this:
from matplotlib.backends.backend_qt5agg import FigureCanvas
or like this:from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
But I already figured out why. Thebackend_qt5agg
file seems to defineFigureCanvas
as an alias forFigureCanvasQTAgg
.
Why exactly is the second example so much faster than the first one? Honestly, it surprises me. The first example is based on a webpage from the official matplotlib website. I'd expect that one to be better.
Do you have any suggestions to make the second example even faster?
4. Edits
Based on the webpage:
https://bastibe.de/2013-05-30-speeding-up-matplotlib.html
I modified the first example to increase its speed. Please have a look at the code:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# EXAMPLE 1 (modified for extra speed) #
# -------------------------------------- #
# This code is inspired on: #
# https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html #
# and on: #
# https://bastibe.de/2013-05-30-speeding-up-matplotlib.html #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import numpy as np
class ApplicationWindow(QtWidgets.QMainWindow):
'''
The PyQt5 main window.
'''
def __init__(self):
super().__init__()
# 1. Window settings
self.setGeometry(300, 300, 800, 400)
self.setWindowTitle("Matplotlib live plot in PyQt - example 1 (modified for extra speed)")
self.frm = QtWidgets.QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
self.lyt = QtWidgets.QVBoxLayout()
self.frm.setLayout(self.lyt)
self.setCentralWidget(self.frm)
# 2. Place the matplotlib figure
self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=1)
self.lyt.addWidget(self.myFig)
# 3. Show
self.show()
return
class MyFigureCanvas(FigureCanvas):
'''
This is the FigureCanvas in which the live plot is drawn.
'''
def __init__(self, x_len:int, y_range:List, interval:int) -> None:
'''
:param x_len: The nr of data points shown in one plot.
:param y_range: Range on y-axis.
:param interval: Get a new datapoint every .. milliseconds.
'''
super().__init__(mpl.figure.Figure())
# Range settings
self._x_len_ = x_len
self._y_range_ = y_range
# Store two lists _x_ and _y_
self._x_ = list(range(0, x_len))
self._y_ = [0] * x_len
# Store a figure ax
self._ax_ = self.figure.subplots()
self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1]) # added
self._line_, = self._ax_.plot(self._x_, self._y_) # added
self.draw() # added
# Initiate the timer
self._timer_ = self.new_timer(interval, [(self._update_canvas_, (), {})])
self._timer_.start()
return
def _update_canvas_(self) -> None:
'''
This function gets called regularly by the timer.
'''
self._y_.append(round(get_next_datapoint(), 2)) # Add new datapoint
self._y_ = self._y_[-self._x_len_:] # Truncate list y
# Previous code
# --------------
# self._ax_.clear() # Clear ax
# self._ax_.plot(self._x_, self._y_) # Plot y(x)
# self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
# self.draw()
# New code
# ---------
self._line_.set_ydata(self._y_)
self._ax_.draw_artist(self._ax_.patch)
self._ax_.draw_artist(self._line_)
self.update()
self.flush_events()
return
# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
global i
i += 1
if i > 499:
i = 0
return d[i]
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
qapp.exec_()
The result is pretty amazing. The modifications make the first example definitely much faster! However, I don't know if this makes the first example equally fast now to the second example. They're certainly close to each other. Anyone an idea who wins?
Also, I noticed that one vertical line on the left, and one horizontal line on top is missing:
It's not a big deal, but I just wonder why.