I've got a live matplotlib graph in a PyQt5 window:
You can read more about how I got this code working here:
How to make a fast matplotlib live plot in a PyQt5 GUI
Please copy-paste the code below to a python file, and run it with Python 3.7:
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
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")
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)
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
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_()
1. The problem
I need to know the nr of pixels from x_min
to x_max
:
Please notice that the x-axis actually goes beyond the x_min
and x_max
borders. I don't need to know the total length. Just the length from x_min
to x_max
.
2. What I tried so far
I already found a way to get the graph's bounding box. Notice the following codelines in the _update_canvas_()
function:
# Print size of bounding box (in pixels)
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
print(f"bbox size in pixels = {width} x {height}")
That gave me a bounding box size of 778.0 x 378.0 pixels. It's a nice starting point, but I don't know how to proceed from here.
I also noticed that this bounding box size isn't printed out correctly from the first go. The first run of the _update_canvas_()
function prints out a bouding box of 640.0 x 480.0 pixels, which is just plain wrong. From the second run onwards, the printed size is correct. Why?
Edit
I tried two solutions. The first one is based on a method described by @ImportanceOfBeingErnes (see Axes class - set explicitly size (width/height) of axes in given units) and the second one is based on the answer from @Eyllanesc.
#####################################################################################
# #
# PLOT A LIVE GRAPH IN A PYQT WINDOW #
# #
#####################################################################################
from __future__ import annotations
from typing import *
import sys
import os
from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import matplotlib.style as style
import numpy as np
style.use('ggplot')
def get_width_method_a(ax, dpi, canvas):
l = float(ax.figure.subplotpars.left)
r = float(ax.figure.subplotpars.right)
x, y, w, h = ax.figure.get_tightbbox(renderer=canvas.get_renderer()).bounds
return float(dpi) * float(w - (l + r))
def get_width_eyllanesc(ax):
""" Based on answer from @Eyllanesc"""
""" See below """
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = ax.transData.transform((x_min, y_fake))
x_pixel_max, _ = ax.transData.transform((x_max, y_fake))
return x_pixel_max - x_pixel_min
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")
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)
self._line_.set_ydata(y)
print("")
print(f"width in pixels (first call, method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (first call, method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
# 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)
print("")
print(f"width in pixels (method is 'method_a') = {get_width_method_a(self._ax_, self.figure.dpi, self)}")
print(f"width in pixels (method is 'eyllanesc') = {get_width_eyllanesc(self._ax_)}")
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_()
Conclusions:
The correct answer is 550 pixels, which is what I measured on a printscreen. Now, I get the following output printed when I run the program:
width in pixels (first call, method is 'method_a') = 433.0972222222222
width in pixels (first call, method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 433.0972222222222
width in pixels (method is 'eyllanesc') = 453.1749657377798
width in pixels (method is 'method_a') = 540.0472222222223
width in pixels (method is 'eyllanesc') = 550.8908177249887
...
The first call for both methods gives the wrong result. From the third(!) call onwards, they both give pretty good results, with the method from @Eyllanesc being the winner.
How do I fix the problem of the wrong result for the first call?