Starting with the working Matplotlib animation code shown below, my goal is to embed this animation (which is just a circle moving across the screen) within a PyQT4 GUI.
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib import animation
fig,ax = plt.subplots()
ax.set_aspect('equal','box')
circle = Circle((0,0), 1.0)
ax.add_artist(circle)
ax.set_xlim([0,10])
ax.set_ylim([-2,2])
def animate(i):
circle.center=(i,0)
return circle,
anim = animation.FuncAnimation(fig,animate,frames=10,interval=100,repeat=False,blit=True)
plt.show()
I am able to accomplish this using the following code, but there is one hitch: I cannot get blitting to work.
import sys
from PyQt4 import QtGui, QtCore
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from matplotlib import animation
class Window(QtGui.QDialog): #or QtGui.QWidget ???
def __init__(self):
super(Window, self).__init__()
self.fig = Figure(figsize=(5,4),dpi=100)
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111) # create an axis
self.ax.hold(False) # discards the old graph
self.ax.set_aspect('equal','box')
self.circle = Circle((0,0), 1.0)
self.ax.add_artist(self.circle)
self.ax.set_xlim([0,10])
self.ax.set_ylim([-2,2])
self.button = QtGui.QPushButton('Animate')
self.button.clicked.connect(self.animate)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def animate(self):
self.anim = animation.FuncAnimation(self.fig,self.animate_loop,frames=10,interval=100,repeat=False,blit=False)
self.canvas.draw()
def animate_loop(self,i):
self.circle.center=(i,0)
return self.circle,
def main():
app = QtGui.QApplication(sys.argv)
ex = Window()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When I set blit=True, after pressing the Animate button I get the following error:
a.figure.canvas.restore_region(bg_cache[a])
KeyError: matplotlib.axes._subplots.AxesSubplot object at 0x00000000095F1D30
In searching this error, I find many posts about how blitting does not work on Macs, but I am using Windows 7. I have tried replacing self.canvas.draw() with self.canvas.update(), but this does not work.
After looking at the source code of the animation module, I realized that there is an error in the Animation class (the dictionary bg_cache is empty, when it is accessed for the first time with blitting switched on).
This is fixed in the git version of matplotlib; however, in the most recent stable version 1.5.1, the bug is still present. You can either fix the bug in the matplotlib code itself or you can make a subclass to FuncAnimation. I chose that way, because it should still work after updating matplotlib.
from matplotlib import animation
class MyFuncAnimation(animation.FuncAnimation):
"""
Unfortunately, it seems that the _blit_clear method of the Animation
class contains an error in several matplotlib verions
That's why, I fork it here and insert the latest git version of
the function.
"""
def _blit_clear(self, artists, bg_cache):
# Get a list of the axes that need clearing from the artists that
# have been drawn. Grab the appropriate saved background from the
# cache and restore.
axes = set(a.axes for a in artists)
for a in axes:
if a in bg_cache: # this is the previously missing line
a.figure.canvas.restore_region(bg_cache[a])
Then, simpy use MyFuncAnimation instead of animation.FuncAnimation.
Took me a while to figure it out, but I hope it helps anybody.
After some time I managed to recreate the animation by using the underlying functions directly and not using the animation wrapper:
import sys
from PyQt4 import QtGui, QtCore
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Circle
from matplotlib import animation
from time import sleep
class Window(QtGui.QDialog): #or QtGui.QWidget ???
def __init__(self):
super(Window, self).__init__()
self.fig = Figure(figsize=(5, 4), dpi=100)
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111) # create an axis
self.ax.hold(False) # discards the old graph
self.ax.set_aspect('equal', 'box')
self.circle = Circle((0,0), 1.0, animated=True)
self.ax.add_artist(self.circle)
self.ax.set_xlim([0, 10])
self.ax.set_ylim([-2, 2])
self.button = QtGui.QPushButton('Animate')
self.button.clicked.connect(self.animate)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
self.canvas.draw()
self.ax_background = self.canvas.copy_from_bbox(self.ax.bbox)
def animate(self):
self.animate_loop(0)
def animate_loop(self,begin):
for i in range(begin,10):
self.canvas.restore_region(self.ax_background)
self.circle.center=(i,0)
self.ax.draw_artist(self.circle)
self.canvas.blit(self.ax.bbox)
self.canvas.flush_events()
sleep(0.1)
def main():
app = QtGui.QApplication(sys.argv)
ex = Window()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Maybe this will be of use to you.
Related
I'm trying to draw a region of interest on a color map that is embedded in a pyqt5 gui. This is an example of what I want.
import sys
from PyQt5.QtWidgets import (QWidget, QPushButton,
QHBoxLayout, QVBoxLayout, QApplication)
from PyQt5 import QtCore
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import ROI_class as roi # ROI_class.py
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.drawButton.clicked.connect(self.draw_map_Callback)
self.roiButton.clicked.connect(self.choose_roi)
def initUI(self):
self.drawButton = QPushButton("draw map")
self.roiButton = QPushButton("roi")
self.hbox = QHBoxLayout()
self.hbox.addStretch(1)
self.hbox.addWidget(self.drawButton)
self.hbox.addWidget(self.roiButton)
self.vbox = QVBoxLayout()
self.vbox.addStretch(1)
self.vbox.addLayout(self.hbox)
self.setLayout(self.vbox)
self.setGeometry(500, 500, 500, 500)
self.setWindowTitle('ROI')
self.show()
def draw_map_Callback(self):
img = np.ones((100, 100)) * range(0, 100)
fig, ax1 = plt.subplots()
self.con_canvas = FigureCanvas(plt.figure(tight_layout=True))
self.con_canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
self.con_canvas.setFocus()
self.con_toolbar = NavigationToolbar(self.con_canvas, self)
self.vbox.addWidget(self.con_toolbar)
self.vbox.addWidget(self.con_canvas)
self._con_ax = self.con_canvas.figure.subplots()
self.con_img = self._con_ax.imshow(img, cmap ='jet')
self._con_ax.set_xlabel('xlabel')
self._con_ax.set_ylabel('ylabel')
self.con_cbar = self.con_canvas.figure.colorbar(self.con_img)
self._con_ax.set_aspect('equal')
def choose_roi(self):
y = roi.new_ROI(self.con_img)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
It will draw a colormap when I click "draw map". Then I want it to allow me to draw a region of interest with my mouse and get a mask using the code on this link below.
https://github.com/martindurant/misc/blob/master/ROI.py
The "ROI_class" that is imported is just a copy and paste of the code in the link above.
I can successfully draw the plot on the GUI but when I click "roi", it doesn't allow me to draw the region of interest.
When I mad a new file and paste the code in the link above with something like
fig, ax1 = plt.subplots()
s = ax1.imshow(img, cmap ='jet')
ax1.set_xlabel('subcolor')
ax1.set_ylabel('ylabel')
y = new_ROI(s)
at the end of the code, it worked just fine and I was able to draw the region of interest and get the mask of it.
But when I try to do this in the GUI, it wouldn't let me draw the region of interest. I'm very confused why this isn't working.
The problem is that picker (the variable "y") is a local variable that gets destroyed instantly causing the desired behavior not to be executed. The solution is to make it an attribute of the class:
self.y = roi.new_ROI(self.con_img)
I am pretty new to PyQt5. I would like to have a button that, when clicked, generates and shows a plot. I cannot seem to find a way to use the QPushButton to call the PlotCanvas function successfully.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5 import QtWidgets, Qt
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import random
import numpy as np
import math
class GUI_Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.plot_button = QtWidgets.QPushButton(self)
self.plot_button.setText('Plot')
self.plot_button.setToolTip('Plots the function')
self.plot_button.move(50,240)
self.plot_button.clicked.connect(self.updater)
def updater(self):
PlotCanvas(self, inputparam=5)
print('Function has been called.')
The code above does not work (PlotCanvas doesn't plot) even though I receive the 'function has been called' printout.
However if I simply call PlotCanvas in initUI, it plots it as expected.
def initUI(self):
self.plot_button = QtWidgets.QPushButton(self)
self.plot_button.setText('Plot')
self.plot_button.setToolTip('Plots the function')
self.plot_button.move(50,240)
self.plot_button.clicked.connect(self.updater)
PlotCanvas(self, inputparam=5)
The above code plots the function as expected.
Furthermore, if I simply call the updater function without the pushbutton (shown below), the plot is shown.
def initUI(self):
self.plot_button = QtWidgets.QPushButton(self)
self.plot_button.setText('Plot')
self.plot_button.setToolTip('Plots the function')
self.plot_button.move(50,240)
#self.plot_button.clicked.connect(self.updater)
self.updater()
def updater(self):
PlotCanvas(self, inputparam=5)
print('Function has been called.')
I sense I do not know how 'self' works and by passing a clicked signal I am somehow messing things up. Any help/insight would be appreciated.
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=1.5, height=2, dpi=100, inputparam=0):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
self.LLA_Plotter(inputparam)
# This function performs calculations and outputs x,y data points to plot
ax = self.figure.add_subplot(111)
for i in range(0,90):
ax.scatter(x[i], y[i], color='b', s=0.6)
ax.set_title('testing plot')
plt.show()
Regarding to this question and the answer here, is there a way to pass the wheel scroll event to the scrollbar when the mouse is located over the plots? I've tried using an event filter in the Main Widget, but it doesn't registered that the wheel is scrolled in the Main, only in the canvas/plots. I don't need the plots to know that it is being scrolled, only the GUI. Any help would be greatly appreciated, thank you.
One solution to scroll the FigureCanvas inside a QScrollArea in PyQt is to use matplotlib's "scroll_event" (see Event handling tutorial) and connect it to a function which scrolls the scrollBar of the QScrollArea.
The example (from my answer to this question) can be extended to connect to a function scrolling via
self.canvas.mpl_connect("scroll_event", self.scrolling)
inside this function the scrollbar value is updated.
import matplotlib.pyplot as plt
from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
class ScrollableWindow(QtGui.QMainWindow):
def __init__(self, fig):
self.qapp = QtGui.QApplication([])
QtGui.QMainWindow.__init__(self)
self.widget = QtGui.QWidget()
self.setCentralWidget(self.widget)
self.widget.setLayout(QtGui.QVBoxLayout())
self.widget.layout().setContentsMargins(0,0,0,0)
self.widget.layout().setSpacing(0)
self.fig = fig
self.canvas = FigureCanvas(self.fig)
self.canvas.draw()
self.scroll = QtGui.QScrollArea(self.widget)
self.scroll.setWidget(self.canvas)
self.nav = NavigationToolbar(self.canvas, self.widget)
self.widget.layout().addWidget(self.nav)
self.widget.layout().addWidget(self.scroll)
self.canvas.mpl_connect("scroll_event", self.scrolling)
self.show()
exit(self.qapp.exec_())
def scrolling(self, event):
val = self.scroll.verticalScrollBar().value()
if event.button =="down":
self.scroll.verticalScrollBar().setValue(val+100)
else:
self.scroll.verticalScrollBar().setValue(val-100)
# create a figure and some subplots
fig, axes = plt.subplots(ncols=4, nrows=5, figsize=(16,16))
for ax in axes.flatten():
ax.plot([2,3,5,1])
# pass the figure to the custom window
a = ScrollableWindow(fig)
I am using PyQt 4 for a basic GUI and matplotlib for a plot from which I want to read the coordinates of the plotted data points. Based on these examples (simple picking example), I have the simple problem that I cannot display the coordinates of a data point in a text field such as QtGui.QLabel(). I do not understand why I cannot call the instance Window.msg in the method onpick(). Probably it is because the instance it not given to the method. I only have a basic understanding of object oriented programming (but I am working on it), so the problem is my lack of knowledge.
My question: How to display the coordinates of chosen data (by clicking on it) from a matplotlib plot in my GUI based on PyQT (in that case in my label lbl)?
Also, it would be nice to highlight the chosen data point in the plot.
Here is my code (working):
import numpy as np
import matplotlib.pyplot as plt
from PyQt4 import QtGui
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
import matplotlib.pyplot as plt
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.initUI()
def initUI(self):
self.msg = '0'
# a figure instance to plot on
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
# a label
self.lbl = QtGui.QLabel(self.msg)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.lbl)
self.setLayout(layout)
self.plot()
def plot(self):
# random data
data = [np.random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.hold(False)
# plot data
line, = ax.plot(data, 'o', picker=5) # 5 points tolerance
self.canvas.draw()
self.canvas.mpl_connect('pick_event', Window.onpick)
def onpick(self):
thisline = self.artist
xdata = thisline.get_xdata()
ydata = thisline.get_ydata()
ind = self.ind
# show data
self.msg = (xdata[ind], ydata[ind])
print(self.msg)
# This does not work:
#Window.lbl.setText(self.msg)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
The self is being overlapped by the picker (not sure why). In any case this should work:
import numpy as np
import matplotlib.pyplot as plt
from PyQt4 import QtGui
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
import matplotlib.pyplot as plt
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.initUI()
def initUI(self):
self.msg = '0'
# a figure instance to plot on
self.figure = plt.figure()
self.canvas = FigureCanvas(self.figure)
self.toolbar = NavigationToolbar(self.canvas, self)
# a label
self.lbl = QtGui.QLabel(self.msg)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.lbl)
self.setLayout(layout)
self.plot()
def changelabel(arg):
main.lbl.setText(str(arg[0])+' '+str(arg[1]))
def plot(self):
# random data
data = [np.random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.hold(False)
# plot data
line, = ax.plot(data, 'o', picker=5) # 5 points tolerance
self.canvas.draw()
self.canvas.mpl_connect('pick_event', Window.onpick)
def onpick(self):
thisline = self.artist
xdata = thisline.get_xdata()
ydata = thisline.get_ydata()
ind = self.ind
# show data
self.msg = (xdata[ind], ydata[ind])
print(self.msg)
# Window.changelabel(self.msg)
main.lbl.setText(str(self.msg[0])+' '+str(self.msg[1]))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
, the change is in the setText function, since I call it directly from the variable (no self or Window).
main.lbl.setText(str(self.msg[0])+' '+str(self.msg[1]))
I am trying to draw graphs with the result of a thread using matplotlib. First I launch the thread using a pyqt button, all is ok. But the second time I press the button, subplots crash because I suppose tuples can't be modified. This is my simplfied code that you can try and see by yourself:
from PyQt4 import QtGui, QtCore
import matplotlib.pyplot as plt
import numpy as np
from PyQt4.Qt import *
import sys
class thread(QThread):
def __init__(self, parent=None):
QThread.__init__(self, parent)
def __del__(self):
self.wait()
def render(self):
self.start()
def run(self):
data = [(1, 10), (3, 100), (4, 1000), (5, 100000)]
data_in_array = np.array(data)
transposed = data_in_array.T
x, y = transposed
fig, ax = plt.subplots(1,1)
ax.plot(x, y, 'ro')
ax.plot(x, y, 'b-')
plt.show()
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.button = QtGui.QPushButton('Test', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
self.thread=thread()
def handleButton(self):
self.thread.render()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Please, how can I solve it to avoid crash and uses several times my button?
Thanks
This is not the right way to mix your Qt application and Matplotlib (which uses Qt in its backend).
See https://matplotlib.org/gallery/user_interfaces/embedding_in_qt_sgskip.html for a better explanation than I can provide.
I got the same problem, was searching solution.
By adding these 3 lines solved the issue. Please add these 3 lines before importing pyplot
import matplotlib as mpl
mpl.rcParams['backend'] = "qt4agg"
mpl.rcParams['backend.qt4'] = "PySide"