Related
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.
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}"
)
Is it possible to make chaco plot automatically show full output and not hiding the parts of ticks and labels? E.g. this is the output of standard example:
from chaco.api import ArrayPlotData, Plot
from enable.component_editor import ComponentEditor
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
class MyPlot(HasTraits):
plot = Instance(Plot)
traits_view = View(Item('plot', editor = ComponentEditor(), show_label = False),
width = 500, height = 500, resizable = True)
def __init__(self, x, y, *args, **kw):
super(MyPlot, self).__init__(*args, **kw)
plotdata = ArrayPlotData(x=x,y=y)
plot = Plot(plotdata)
plot.plot(("x","y"), type = "line", color = "blue")
self.plot = plot
import numpy as np
x = np.linspace(-300,300,10000)
y = np.sin(x)*x**3
lineplot = MyPlot(x,y)
lineplot.configure_traits()
As you see the part of tick labels are hidden.. the only thing I can do is to manually adjust left padding of the plot. But this becomes extremely incovinient when you plot different data and different scales or fonts with the plot in application. Is it possible somehow to make padding automatically adjusted to include ALL related info?
UPD.: I've found ensure_labels_bounded property for the axis, but seems it has no effect.
Chaco does not support advanced layout features like these. If you use Chaco, you should use it for its speed, not for nice graphs or features. That being said, here's a version as close as I could get. It requires you to re-size the window with the mouse at least once for the padding correction to take place. Maybe you can find a way to refresh the window without having to manually resize it, I didn't have any luck with that. Anyways hope that gets you on the right track.
from chaco.api import ArrayPlotData, Plot
from enable.component_editor import ComponentEditor
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
class MyPlot(HasTraits):
plot = Instance(Plot)
traits_view = View(Item('plot', editor = ComponentEditor(), show_label = False),
width = 500, height = 500, resizable = True)
def __init__(self, x, y, *args, **kw):
super(MyPlot, self).__init__(*args, **kw)
plotdata = ArrayPlotData(x=x,y=y)
plot = Plot(plotdata, padding=25)
plot.plot(("x","y"), type = "line", color = "blue", name='abc')
self.plot = plot
# watch for changes to the bounding boxes of the tick labels
self.plot.underlays[2].on_trait_change(self._update_size, '_tick_label_bounding_boxes')
self.plot.underlays[3].on_trait_change(self._update_size, '_tick_label_bounding_boxes')
def _update_size(self):
if len(self.plot.underlays[2]._tick_label_bounding_boxes) > 0:
self.plot.padding_bottom = int(np.amax(np.array(self.plot.underlays[2]._tick_label_bounding_boxes),0)[1]+8+4)
if len(self.plot.underlays[3]._tick_label_bounding_boxes) > 0:
self.plot.padding_left = int(np.amax(np.array(self.plot.underlays[3]._tick_label_bounding_boxes),0)[0]+8+4)
import numpy as np
x = np.linspace(-300,300,10000)
y = np.sin(x)*x**3
lineplot = MyPlot(x,y)
lineplot.configure_traits()
Is it possible to create a chaco plot with latex text? For example, if we wanted latex symbols in the title of this exampe:
from traits.api import HasTraits, Instance
from traitsui.api import View, Item
from chaco.api import Plot, ArrayPlotData
from enable.component_editor import ComponentEditor
from numpy import linspace, sin
class LinePlot(HasTraits):
plot = Instance(Plot)
traits_view = View(
Item('plot',editor=ComponentEditor(), show_label=False),
width=500, height=500, resizable=True, title="Chaco Plot")
def __init__(self):
super(LinePlot, self).__init__()
x = linspace(-14, 14, 100)
y = sin(x) * x**3
plotdata = ArrayPlotData(x=x, y=y)
plot = Plot(plotdata)
plot.plot(("x", "y"), type="line", color="blue")
plot.title = "sin(x) * x^3"
self.plot = plot
if __name__ == "__main__":
LinePlot().configure_traits()
I tried replacing title with $sin(x)^3$ to no avail, and wondered if this was possible? Screenshot below:
No, it is not (it is a matplotlib feature). But you could try to use unicode symbols for easy cases.
I have large time-traces that must be inspected visually, so I need a fast scrolling tool.
How can I achieve the fastest Maplotlib/Pyside scrolling?
Right know, I added a PySide scroll-bar to a MPL figure and update the x-range of the plot with set_xlim() method. This is not fast enough especially because in the final application I have at least 8 time-traces in different subplots that must all scroll together. A figure of the plot is attached.
Is there room for improvement?
Here I attach the demo code that demonstrate the relatively low scrolling. It's long but it's almost all boiler-plate code. The interesting bit (that needs improvement) is in xpos_changed() method where the plot xlimits are changed.
EDIT: Below I incorporated some micro-optimizations suggested by tcaswell, but the update speed is not improved.
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.title("1000s Timetrace \n (use the slider to scroll and the spin-box to set the width)")
plt.xlabel('Time (s)')
plt.legend(fancybox=True)
q = ScrollingToolQT(plt.gcf(), scroll_step=10)
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig, scroll_step=10):
# Setup data range variables for scrolling
self.fig = fig
self.scroll_step = scroll_step
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.width = 1 # axis units
self.pos = 0 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Save some MPL shortcuts
self.ax = self.fig.axes[0]
self.draw = self.fig.canvas.draw
#self.draw_idle = self.fig.canvas.draw_idle
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.ax.set_xlim(self.pos,self.pos+self.width)
self.draw()
def set_slider(self, parent):
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((self.xmax-self.xmin)/10.*self.scale)
self.slider.setMinimum(self.xmin*self.scale)
self.slider.setMaximum((self.xmax-self.width)*self.scale)
self.slider.setSingleStep(self.width*self.scale/4.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
self.slider.setValue(self.pos*self.scale) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001,3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.width) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
pos /= self.scale
self.ax.set_xlim(pos, pos+self.width)
self.draw()
def xwidth_changed(self, width):
#pprint("Width (axis units) %f\n" % step)
if width <= 0: return
self.width = width
self.slider.setSingleStep(self.width*self.scale/5.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0]*self.scale)
if __name__ == "__main__":
q = test_plot()
plt.show()
As requested in the comments, here is a pyqtgraph demo which scrolls two large traces together (via mouse).
The documentation isn't complete for the pyqtgraph project but there are some good examples you can view with python -m pyqtgraph.examples which should point you in the right direction. The crosshair.py example might be particularly interesting for you.
If you go with pyqtgraph, connect your slider widget to the setXRange method in the last line of this demo.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
x = np.arange(1e5)
y1 = np.random.randn(x.size)
y2 = np.random.randn(x.size)
p1 = win.addPlot(x=x, y=y1, name='linkToMe')
p1.setMouseEnabled(x=True, y=False)
win.nextRow()
p2 = win.addPlot(x=x, y=y2)
p2.setXLink('linkToMe')
p1.setXRange(2000,3000)
This seems a bit faster/more responsive:
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.legend(fancybox=True)
plt.title("Use the slider to scroll and the spin-box to set the width")
q = ScrollingToolQT(plt.gcf())
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig):
# Setup data range variables for scrolling
self.fig = fig
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.step = 1 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.set_xlim = self.fig.axes[0].set_xlim
self.draw_idle = self.fig.canvas.draw_idle
self.ax = self.fig.axes[0]
self.set_xlim(0, self.step)
self.fig.canvas.draw()
def set_slider(self, parent):
# Slider only support integer ranges so use ms as base unit
smin, smax = self.xmin*self.scale, self.xmax*self.scale
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((smax-smin)/10.)
self.slider.setMinimum(smin)
self.slider.setMaximum(smax-self.step*self.scale)
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
self.slider.setValue(0) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001, 3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.step) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
# self.pos = pos/self.scale
pos /= self.scale
self.set_xlim(pos, pos + self.step)
self.draw_idle()
def xwidth_changed(self, xwidth):
#pprint("Width (axis units) %f\n" % step)
if xwidth <= 0: return
self.step = xwidth
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0] * self.scale)
# self.set_xlim(self.pos,self.pos+self.step)
# self.fig.canvas.draw()
if __name__ == "__main__":
q = test_plot()
plt.show()