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:
Matplotlib animation inside your own GUI
How do I plot in real-time in a while loop using matplotlib?
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 and QtWidgets 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. The backend_qt5agg file seems to define FigureCanvas as an alias for FigureCanvasQTAgg.
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.
The second case (using FuncAnimation) is faster because it uses "blitting", which avoids redrawing things that do not change between frames.
The example provided on the matplotlib website for embedding in qt was not written with speed in mind, hence the poorer performance. You'll notice that it calls ax.clear() and ax.plot() at each iteration, causing the whole canvas to be redrawn everytime. If you were to use the same code as in the code with FuncAnimation (that is to say, create an Axes and an artist, and update the data in the artist instead of creating a new artists every time) you should get pretty close to the same performance I believe.
Related
I am trying to create a polygon selector in my PySide2 application. Selector is working fine, but then I want to add functionality to reset/start new polygon when escape button is pressed. Something similar to this PolygonSelector example, when escape is pressed.
https://matplotlib.org/stable/gallery/widgets/polygon_selector_demo.html
I tried method .clear() but it does not seems to work for me.
import sys
import numpy as np
from PySide2 import QtWidgets, QtGui
from matplotlib.backends.backend_qtagg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
from matplotlib.widgets import PolygonSelector
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
shortcut_clear_selection = QtWidgets.QShortcut(QtGui.QKeySequence("Escape"), self._main)
shortcut_clear_selection.activated.connect(self.callback_clear_selection)
layout = QtWidgets.QVBoxLayout(self._main)
static_canvas = FigureCanvas(Figure(figsize=(5, 3)))
layout.addWidget(NavigationToolbar(static_canvas, self))
layout.addWidget(static_canvas)
ax = static_canvas.figure.subplots()
t = np.linspace(0, 10, 501)
ax.plot(t, np.tan(t), ".")
self.poly = PolygonSelector(ax, self.onselect)
def onselect(self, verts):
pass
def callback_clear_selection(self):
# HERE should be the reset part
self.poly.clear()
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
app.activateWindow()
app.raise_()
qapp.exec_()
Problem is, that ESC key release event is not handled by PolygonSelector but Your callback. Therefore, to clear polygon and restart creation of polygon You have to clear polygon data and show selector again. clear method was just hiding selector, but polygon data stayed unchanged.
Change Your callback code into this:
def callback_clear_selection(self):
# HERE should be the reset
self.poly._xs, self.poly._ys = [0], [0]
self.poly._selection_completed = False
self.poly.set_visible(True)
Now, when You press ESC, polygon should be removed and You can start selection of new one.
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?
For an old answer I had to do calculation, which in your case is:
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)
Note:
The calculations take into account what is painted, so in the first moments it is still being painted so the results are correct but our eyes cannot distinguish them. If you want to obtain the correct size without the animation you must calculate that value when the painting is stabilized, which is difficult to calculate, a workaround is to use a QTimer to make the measurement a moment later:
# ...
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)
QtCore.QTimer.singleShot(100, self.calculate_length)
# ...
def calculate_length(self):
y_fake = 0
x_min, x_max = 0, 200
x_pixel_min, _ = self._ax_.transData.transform((x_min, y_fake))
x_pixel_max, _ = self._ax_.transData.transform((x_max, y_fake))
print(
f"The length in pixels between x_min: {x_min} and x_max: {x_max} is: {x_pixel_max - x_pixel_min}"
)
I'm writing a medium-sized application to review some data. The structure is that plots will be held in a QTabWidget interface, with a plot control widget to adjust x limits (and later, there may be more features in the control widget). I have included a minimum reproducible example below.
Currently, I pass the axis of a figure to my control widget, and within that widget change the x limits after clicking a button. I have verified that the axis is being passed to the widget (with a print statement). I can programmatically set the axis limits (see line 91: self.Fig_ax.set_xlim(5, 20000) ) in the widget __init__ function and it works, but in the button click function, that same syntax does not do anything. However, with print statements, I verified that the axis is still being passed to the button click function.
I am very confused as to why the set_xlims method works in __init__ but not upon button press. Use: Run the code, enter a number in the X Min and X Max fields, click the Apply X Limits button. For the sake of the example, I hardcoded the button click axis shift to have defined limits rather than use what is entered into the fields, but those fields do get printed to the console for debugging purposes.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created
"""
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
## import matplotlib and animation
import functools
import random as rd
import numpy as np
from numpy import array, sin, pi, arange
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import pandas as pd
## import threading
import time
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
## New:
from PyQt5.QtWebEngineWidgets import *
######################################################8
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'DDBRT'
self.setWindowTitle(self.title)
self.DDBRT_Widget = DDBRT(self) # Call the DDBRT
self.setCentralWidget(self.DDBRT_Widget) # set it as the central widget in the window
self.show()
####
####
''' End AppWindow '''
# August 27 2019 Start building a custom QWidget that can be put into the tool in multiple instances to adjust xlims.
# This may also serve as a templatge for other custom widgets that can go in
class XLimControlWidget(QWidget):
def __init__(self, parent, **kwargs):
super(QWidget, self).__init__(parent)
# set layout:
self.XLCWLayout = QVBoxLayout(self)
# Insert X Min Box Label
self.XMinSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMinSelectLbl)
# Insert X Min Entry Field
self.XMinEntryField = QLineEdit('X Min')
self.XLCWLayout.addWidget(self.XMinEntryField)
# Insert X Max Box Label
self.XMaxSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMaxSelectLbl)
# Insert X Max Box Entry Field
self.XMaxEntryField = QLineEdit('X Max')
self.XLCWLayout.addWidget(self.XMaxEntryField)
# Insert Set Button
self.SetXLimsBtn = QPushButton('Apply X Limits')
self.XLCWLayout.addWidget(self.SetXLimsBtn)
# Adjust layout so this widget is compact:
self.XLCWLayout.setSpacing(0)
self.XLCWLayout.setContentsMargins(0, 0, 0, 0)
# Note, that doesn't actually work and it still looks ugly
# That's annoying, but not worth figuring out how to fix right now.
# Need to focus on programming the behavior.
# Try out the kwargs pass to make sure passing something works.
for key, value in kwargs.items():
print('%s = %s' %(key, value))
####
self.Fig_ax = kwargs['Fig_ax_Key']
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
# Try the fig ax set xlim, which does work but doesn't.
self.Fig_ax.set_xlim(5, 20000)
self.SetXLimsBtn.clicked.connect(self.SetXLimsBtnClkd)
####
def SetXLimsBtnClkd(self): # Define what happens when the button is clicked.
self.xmin = float(self.XMinEntryField.text())
print('X Min will be ', self.xmin, ' of type ', type(self.xmin))
self.xmax = float(self.XMaxEntryField.text())
print('X Max will be ', self.xmax, ' of type ', type(self.xmax))
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
self.Fig_ax.set_xlim(20, 45)
# End desired goal:
# self.Fig_ax.set_xlim(self.xmin, self.xmax)
####
####
class DDBRT(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
#%% Set up multithreading
self.threadpool = QThreadPool() # Set up QThreadPool for multithreading so the GIL doesn't freeze the GUI
print("Multithreading with maximum %d theads" % self.threadpool.maxThreadCount())
#%% Layout:
## Set layout
self.MainLayout = QGridLayout(self)
## Create and embed CheckBox Placeholder
self.ChkBx_Placeholder = QCheckBox('ChkBxPlcholdr1');
self.MainLayout.addWidget(self.ChkBx_Placeholder, 3, 0)
## Create and embed tab container to hold plots, etc.
# Initialize tab container
self.TabsContainer = QTabWidget()
# Initialize tabs
self.tab0 = QWidget()
# Add tabs
self.TabsContainer.addTab(self.tab0, "Tab 0")
# Populate 0th tab
self.tab0.layout = QGridLayout(self)
self.pushButton0 = QPushButton("PyQt5 button")
self.tab0.layout.addWidget(self.pushButton0)
self.tab0.setLayout(self.tab0.layout)
# Add TabsContainer to widget
self.MainLayout.addWidget(self.TabsContainer, 3, 1) # self.MainLayout.addWidget(self.TabsContainer, 2, 2) # Works just fine too, but I can worry about layout finessing later because it's not that difficult, important, or urgent right now
self.setLayout(self.MainLayout)
#%% Plot XLs (accelerations)
XL_t = np.arange(0, 200000, 1)
XL_X = np.sin(XL_t/20000)
XL_Y = np.sin(XL_t/2000)
XL_Z = np.sin(XL_t/200)
self.tab8 = QWidget()
self.TabsContainer.addTab(self.tab8, "Tab 8: Acceleration mpl subplots")
self.tab8.layout = QHBoxLayout(self)
self.XL_Fig = Figure()
self.XL_X_ax = self.XL_Fig.add_subplot(3, 1, 1)
self.XL_X_ax.plot(XL_t, XL_X)
self.XL_X_ax.set_title('Acceleration X')
# self.XL_X_ax.grid(True)
self.XL_X_ax.set_xlabel('Time (s)')
self.XL_X_ax.set_ylabel('Acceleration')
#
self.XL_Y_ax = self.XL_Fig.add_subplot(3, 1, 2, sharex=self.XL_X_ax)
self.XL_Y_ax.plot(XL_t, XL_Y)
self.XL_Y_ax.set_title('Acceleration Y')
# self.XL_Y.grid(True)
self.XL_Y_ax.set_xlabel('Time (s)')
self.XL_Y_ax.set_ylabel('Acceleration')
#
self.XL_Z_ax = self.XL_Fig.add_subplot(3, 1, 3, sharex=self.XL_X_ax)
self.XL_Z_ax.plot(XL_t, XL_Z)
self.XL_Z_ax.set_title('Acceleration Z')
# self.XL_Z.grid(True)
self.XL_Z_ax.set_xlabel('Time (s)')
self.XL_Z_ax.set_ylabel('Acceleration')
#
self.XL_Canvas = FigureCanvas(self.XL_Fig)
self.XL_Canvas.print_figure('test')
# # Create an XLPlot container widget and add the canvas and navigation bar to it
self.XL_PlotContainer = QWidget()
self.XL_PlotContainer.layout = QVBoxLayout(self)
self.XL_PlotContainer.layout.addWidget(self.XL_Canvas)
self.XLMPLToolbar = NavigationToolbar(self.XL_Canvas, self)
self.XL_PlotContainer.layout.addWidget(self.XLMPLToolbar, 3)
self.XL_PlotContainer.setLayout(self.XL_PlotContainer.layout) # Looks redundant but it's needed to display the widgets
# add XLPlotContainer Widget to tab
self.tab8.layout.addWidget(self.XL_PlotContainer, 1)
self.tab8.setLayout(self.tab8.layout)
# add XLCWidget to tab
self.kwargs = {"Fig_ax_Key": self.XL_X_ax}
self.XLXLCW = XLimControlWidget(self, **self.kwargs)
self.tab8.layout.addWidget(self.XLXLCW)
####
####
#%%
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = AppWindow()
sys.exit(app.exec_())
I expect the button click to change the axes on all subplots (since I linked the subplots when I set them up), but the x limits do not change at all. The button click function does run, as shown by the print statements.
In case anyone else finds this question later, here is the solution that worked: I passed the entire figure to my control widget, used a = self.Fig.get_axes() to get a list of my axes, and a[0].set_xlim(20, 200) ; self.Fig.canvas.draw_idle() to update the figure with the button click.
I'm trying to do something similar to what is done in the pyqtgraph example 'Crosshair/Mouse Interaction'. Basically I want to connect a linear region item on one plot, to the x-axis on another plot. then one plot will show the data that's in the linearregionitem, and you can zoom in and out by changing the linearregionitem, and vice-versa.
My problem is that it crashes with:
RecursionError: maximum recursion depth exceeded while calling a
Python object
Here is the code from the example if you want to try it to give you an idea of what I want to do...
"""
Demonstrates some customized mouse interaction by drawing a crosshair that follows
the mouse.
"""
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
#generate layout
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
win.setWindowTitle('pyqtgraph example: crosshair')
label = pg.LabelItem(justify='right')
win.addItem(label)
p1 = win.addPlot(row=1, col=0)
p2 = win.addPlot(row=2, col=0)
region = pg.LinearRegionItem()
region.setZValue(10)
# Add the LinearRegionItem to the ViewBox, but tell the ViewBox to exclude this
# item when doing auto-range calculations.
p2.addItem(region, ignoreBounds=True)
#pg.dbg()
p1.setAutoVisible(y=True)
#create numpy arrays
#make the numbers large to show that the xrange shows data from 10000 to all the way 0
data1 = 10000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
data2 = 15000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
p1.plot(data1, pen="r")
p1.plot(data2, pen="g")
p2.plot(data1, pen="w")
def update():
region.setZValue(10)
minX, maxX = region.getRegion()
p1.setXRange(minX, maxX, padding=0)
region.sigRegionChanged.connect(update)
def updateRegion(window, viewRange):
rgn = viewRange[0]
region.setRegion(rgn)
p1.sigRangeChanged.connect(updateRegion)
region.setRegion([1000, 2000])
#cross hair
vLine = pg.InfiniteLine(angle=90, movable=False)
hLine = pg.InfiniteLine(angle=0, movable=False)
p1.addItem(vLine, ignoreBounds=True)
p1.addItem(hLine, ignoreBounds=True)
vb = p1.vb
def mouseMoved(evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
if p1.sceneBoundingRect().contains(pos):
mousePoint = vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index > 0 and index < len(data1):
label.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>, <span style='color: green'>y2=%0.1f</span>" % (mousePoint.x(), data1[index], data2[index]))
vLine.setPos(mousePoint.x())
hLine.setPos(mousePoint.y())
proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
#p1.scene().sigMouseMoved.connect(mouseMoved)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
If you don't want to read all that, the linearregionitem and the plotitem are connected via the lines...
def update():
region.setZValue(10)
minX, maxX = region.getRegion()
p1.setXRange(minX, maxX, padding=0)
region.sigRegionChanged.connect(update)
def updateRegion(window, viewRange):
rgn = viewRange[0]
region.setRegion(rgn)
p1.sigRangeChanged.connect(updateRegion)
Here's a minimal working example of my code...I'm doing pretty much the same thing, but I'm doing it in a class...
When you run it, it will crash if you adjust the linearregionitem, or if you change the axis of plotA. If you comment out either of the 'connect' lines, then the program will work (half-way).
import pyqtgraph as pg
import sys
# PyQt5 includes
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication
class MyApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.__buildUI()
def __buildUI(self):
plotWidget = pg.GraphicsLayoutWidget()
self.PlotA = pg.PlotItem()
self.PlotA.setXRange(10, 20)
self.PlotB = pg.PlotItem()
self.PlotB.setXRange(0, 100)
self.lri = pg.LinearRegionItem()
self.lri.setRegion((10, 20))
self.PlotB.addItem(self.lri)
# The following two connections set up a recursive loop
self.lri.sigRegionChanged.connect(self.update)
self.PlotA.sigRangeChanged.connect(self.update_lri)
plotWidget.addItem(self.PlotA)
plotWidget.nextRow()
plotWidget.addItem(self.PlotB)
self.setCentralWidget(plotWidget)
self.show()
def update(self):
minX, maxX = self.lri.getRegion()
self.PlotA.setXRange(minX, maxX)
def update_lri(self, window, viewRange):
A_xrange = viewRange[0]
self.lri.setRegion(A_xrange)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MyApplicationWindow()
sys.exit(app.exec_())
What's happening? Can anyone tell me how to get this working? This is in Python 3.6
+1 for proving a good MVCE. This allowed me to experiment a bit and I found the issue. Couldn't have solved it without it.
You must set the padding to zero when updating the x range of the plot. So change the update method to:
def update(self):
minX, maxX = self.lri.getRegion()
self.PlotA.setXRange(minX, maxX, padding=0)
Typically with QT these infinite signal loops are prevented by only updating a variable (and emitting the corresponding signal) when the new value is different from the old value. Somewhere in Qt/PyQtGraph this check is also done. But since your padding isn't zero, the new xrange will be a little bigger than the old xrange every iteration, and the loop doesn't end.
BTW, it is common in Python to let variable names start with a lower case character, and class names with upper case. I recommend to rename self.PlotA to self.plotA. This makes your code better readable for other Python programmers. Also it will give better syntax highlighting here on Stack Overflow.
I am interested in real time graph. My aim is to update graph with another definition callback. I tried to debug but I don't see anythink after exec_() command. I tried to call update insteaded of Qtimer.
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
from multiprocessing import Process, Manager,Queue
def f(name):
app2 = QtGui.QApplication([])
win2 = pg.GraphicsWindow(title="Basic plotting examples")
win2.resize(1000,600)
win2.setWindowTitle('pyqtgraph example: Plotting')
p2 = win2.addPlot(title="Updating plot")
curve = p2.plot(pen='y')
def updateInProc(curve):
t = np.arange(0,3.0,0.01)
s = np.sin(2 * np.pi * t + updateInProc.i)
curve.setData(t,s)
updateInProc.i += 0.1
QtGui.QApplication.instance().exec_()
updateInProc.i = 0
timer = QtCore.QTimer()
timer.timeout.connect(lambda: updateInProc(curve))
timer.start(50)
if __name__ == '__main__':
m=f()
m
I want to use another definition like
def UpdateCallback():
for x in range(1,100):
m.updateInProc(x,time)
I deleted Qtimer then I tried to send data but I did not see at graph