Put a Matplotlib plot as a QGraphicsItem/into a QGraphicsView - python

So I have a very basic plot layout described below (with x and y values changed for brevity):
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import numpy as np
figure = Figure()
axes = figure.gca()
axes.set_title(‘My Plot’)
x=np.linspace(1,10)
y=np.linspace(1,10)
y1=np.linspace(11,20)
axes.plot(x,y,’-k’,label=‘first one’)
axes.plot(x,y1,’-b’,label=‘second one’)
axes.legend()
axes.grid(True)
And I have designed a GUI in QT designer that has a GraphicsView (named graphicsView_Plot) that I would like to put this graph into and I would like to know how I would go about putting this graph into the GraphicsView. Barring starting over and using the QT based graphing ability I don’t really know how (if possible) to put a matplotlib plot into this graphics view. I know it would be a super simple thing if I can convert it into a QGraphicsItem as well, so either directly putting it into the GraphicsView or converting it to a QGraphicsItem would work for me.

You have to use a canvas that is a QWidget that renders the matplotlib instructions, and then add it to the scene using addWidget() method (or through a QGraphicsProxyWidget):
import sys
from PyQt5 import QtWidgets
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import numpy as np
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(scene)
figure = Figure()
axes = figure.gca()
axes.set_title("My Plot")
x = np.linspace(1, 10)
y = np.linspace(1, 10)
y1 = np.linspace(11, 20)
axes.plot(x, y, "-k", label="first one")
axes.plot(x, y1, "-b", label="second one")
axes.legend()
axes.grid(True)
canvas = FigureCanvas(figure)
proxy_widget = scene.addWidget(canvas)
# or
# proxy_widget = QtWidgets.QGraphicsProxyWidget()
# proxy_widget.setWidget(canvas)
# scene.addItem(proxy_widget)
view.resize(640, 480)
view.show()
sys.exit(app.exec_())

Related

PyQt6 / Matplotlib - multiple FigureCanvasQTAgg using one single Figure object

I have an application where the user can open / close many windows containing different plots. At first I attempted to use a new Matplotlib Figure for each window but I had trouble releasing the memory when closing a window with a plot.
An option I am looking at is using a separate FigureCanvasQTAgg for each window and having them share one single Figure. The problem then becomes that if I clear and replot the figure for one window it affects all others.
My question is the following - is there a way to "freeze" a FigureCanvasQTAgg once it has been drawn so that it is not updated when the Figure is cleared / replotted?
One idea I had was to use an EventFilter to catch and stop the paint event for the FigureCanvasQTAgg once it has been drawn so that it is no longer repainted. However I have not been able to make it work. I have not used EventFilter before so I may be doing it incorrectly.
Here is the code, using a simple example with 2 windows using one single Figure. When the second window is plotted the first window gets cleared, which is what I want to avoid. If anyone can spot what's wrong or suggest another way to prevent the first window to be redrawn when the second one is plotted it would be great.
import sys
import random
import matplotlib
matplotlib.use('Qt5Agg')
from PyQt6 import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class Canvas(FigureCanvas):
def __init__(self,fig):
self.paint_enabled = True
super().__init__(fig)
self.installEventFilter(self)
def eventFilter(self, object, event):
if object == self and event.type() == QtCore.QEvent.Type.Paint and not self.paint_enabled:
return False
return super().eventFilter(object,event)
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow()
fig = Figure(figsize=(5, 4), dpi=100)
ax = fig.add_subplot(111)
xdata = list(range(50))
ydata = [random.randint(0, 10) for i in range(50)]
ax.plot(xdata,ydata)
c1 = Canvas(fig)
c1.move(100,100)
c1.draw()
c1.show()
c1.paint_enabled = False
def second_plot():
ydata = [random.randint(0, 10) for i in range(50)]
ax.clear()
ax.plot(xdata,ydata)
c2 = Canvas(fig)
c2.move(600,600)
c2.draw()
c2.show()
timer = QtCore.QTimer()
timer.setSingleShot(True)
timer.timeout.connect(second_plot)
timer.start(1000)
app.exec()

How to collect the axis object of the last mouse click in matplotlib canvas drawn with pyqt5?

I have an interactive window and I need to know which subplot was selected during the interaction. When I was using matplotlib alone, I could use plt.connect('button_press_event', myMethod). But with pyqt5, I am importing FigureCanvasQTAgg and there is a reference to the figure itself but not an equivalent of pyplot. So, I am unable to create that reference.
Minimal reproducible example:
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas, NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
# list to store the axis last used with a mouseclick
currAx = []
# detect the currently modified axis
def onClick(event):
if event.inaxes:
currAx[:] = [event.inaxes]
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.canvas = FigureCanvas(plt.Figure())
self.axis = self.canvas.figure.subplots(3)
for i, ax in enumerate(self.axis):
t = np.linspace(-i, i + 1, 100)
ax.plot(t, np.sin(2 * np.pi * t))
self.listOfSpans = [SpanSelector(
ax,
self.onselect,
"horizontal"
)
for ax in self.axis]
plt.connect('button_press_event', onClick)
# need an equivalent of ^^ to find the axis interacted with
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
toolbar = NavigationToolbar(self.canvas, self)
layout.addWidget(toolbar)
layout.addWidget(self.canvas)
self.setLayout(layout)
self.show()
def onselect(self, xmin, xmax):
if xmin == xmax:
return
# identify the axis interacted and do something with that information
for ax, span in zip(self.axis, self.listOfSpans):
if ax == currAx[0]:
print(ax)
print(xmin, xmax)
self.canvas.draw()
def run():
app = QApplication([])
mw = MyWidget()
app.exec_()
if __name__ == '__main__':
run()
Apparently, there is a method for connecting canvas as well - canvas.mpl_connect('button_press_event', onclick).
Link to the explanation: https://matplotlib.org/stable/users/explain/event_handling.html
Found the link to the explanation in this link :Control the mouse click event with a subplot rather than a figure in matplotlib.

Why can't I successfully draw the region of interest in a matplotlib color map embedded in a pyqt5 gui?

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)

Getting blitting to work in funcAnimation embedded in PyQT4 GUI

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.

Embed matplotlib widget in multiple places

I have a matplotlib graph which I want repeated in two separate windows, under PyQt4. I've tried adding the widget to the layout of both, but then the widget vanishes from the first one. Is there any way to do this except creating two identical graphs and keeping them in sync?
The problem is that you can't add the same qt widget to two differents parents widgets because in the process of adding a widget Qt also make a reparent process which does what you see:
... the widget vanishes from the first one[window]...
So the solution is to make two canvas that share the same figure.
Here is an example code, this will show you two main windows each with two canvas and the four plots will be syncronized:
import sys
from PyQt4 import QtGui
import numpy as np
import numpy.random as rd
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
class ApplicationWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.main_widget = QtGui.QWidget(self)
vbl = QtGui.QVBoxLayout(self.main_widget)
self._fig = Figure()
self.ax = self._fig.add_subplot(111)
#note the same fig for two canvas
self.fc1 = FigureCanvas(self._fig) #canvas #1
self.fc2 = FigureCanvas(self._fig) #canvas #1
self.but = QtGui.QPushButton(self.main_widget)
self.but.setText("Update") #for testing the sync
vbl.addWidget(self.fc1)
vbl.addWidget(self.fc2)
vbl.addWidget(self.but)
self.setCentralWidget(self.main_widget)
#property
def fig(self):
return self._fig
#fig.setter
def fig(self, value):
self._fig = value
#keep the same fig in both canvas
self.fc1.figure = value
self.fc2.figure = value
def redraw_plot(self):
self.fc1.draw()
self.fc2.draw()
qApp = QtGui.QApplication(sys.argv)
aw1 = ApplicationWindow() #window #1
aw2 = ApplicationWindow() #window #2
aw1.fig = aw2.fig #THE SAME FIG FOR THE TWO WINDOWS!
def update_plot():
'''Just a random plot for test the sync!'''
#note that the update is only in the first window
ax = aw1.fig.gca()
ax.clear()
ax.plot(range(10),rd.random(10))
#calls to redraw the canvas
aw1.redraw_plot()
aw2.redraw_plot()
#just for testing the update
aw1.but.clicked.connect(update_plot)
aw2.but.clicked.connect(update_plot)
aw1.show()
aw2.show()
sys.exit(qApp.exec_())
While it's not a perfect solution, matplotlib has a built-in way to keep the limits, ticks, etc of two separate plots in sync.
E.g.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 4 * np.pi, 100)
y = np.cos(x)
figures = [plt.figure() for _ in range(3)]
ax1 = figures[0].add_subplot(111)
axes = [ax1] + [fig.add_subplot(111, sharex=ax1, sharey=ax1) for fig in figures[1:]]
for ax in axes:
ax.plot(x, y, 'go-')
ax1.set_xlabel('test')
plt.show()
Notice that all 3 plots will stay in-sync as you zoom, pan, etc.
There's probably a better way of doing it though.

Categories