I have a matplotlib 3D scatter plot embedded in PyQt5, it can be rotated around but I cannot update it.
The UI is created in Qt Designer and converted to py class, with several other plots, areas, and buttons. So, I cannot simply destroy the layouts and widgets and override them with new ones as many tutorials did.
I used the following code to embed the 3D scatter plot into PyQt:
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # Ui_MainWindow is the converted class
def __init__(self, *args, obj=None, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
self.cc_rgb = ColorCube('rgb')
FigureCanvas.setSizePolicy(self,
QSizePolicy.Expanding,
QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
layout_rgb = self.RGB_Area.layout() # RGB_Area is a Qt group box
if layout_rgb is None:
layout_rgb = QVBoxLayout(self.RGB_Area)
ax_rgb = self.cc_rgb.ShowPlot()
canvas_rbg = FigureCanvas(ax_rgb.figure)
ax_rgb.mouse_init()
layout_rgb.addWidget(canvas_rbg)
# There are other button connect and associated functions,
# but so far non of them worked to update the plot,
# so I did not put any of them here
And the 3D scatter plot class is defined as follows:
class ColorCube(FigureCanvas): # FigureCanvas is FigureCanvasQTAgg from matplotlib.backends.backend_qt5agg
def __init__(self, mode='rgb'):
# Other parameters
self.fig = Figure(figsize=(self.plotSize, self.plotSize))
self.fig.clear()
self.axes = self.fig.add_subplot(111, projection='3d')
self.axes.clear()
self.axes.axis('off')
self.ShowPlot()
plt.draw()
super(ColorCube, self).__init__(self.fig)
def ShowPlot(self):
# Unrelated codes omitted here
self.axes.clear()
self.axes.axis('off')
self.axes.scatter(self.pixelColor[:, 0],
self.pixelColor[:, 1],
self.pixelColor[:, 2])
return self.axes
Is there a way to make the 3D scatter plot able to update? Preferably without losing the ability to be rotated around.
Related
I am plotting some data in cartopy. I would like to be able to zoom in on a region of the map and have the latitude/longitude axes update to reflect the zoomed in region. Instead, they just dissapear altogether when I zoom in. How do I fix this?
Here is my code for generating the axes
plt.figure()
ax = plt.axes(projection=cartopy.crs.PlateCarree())
ax.add_feature(cartopy.feature.LAND, edgecolor='black')
gl = ax.gridlines(crs=cartopy.crs.PlateCarree(), draw_labels=True,
linewidth=2, color='gray', alpha=0.5, linestyle='--')
# plot some stuff here
It is possible to update the cartopy gridliners in interactive mode, but you need to subclass the Navigation toolbar.
In this example below I have used a PySide/QT5 example code that allows me to substitute a subclassed toolbar, then merged in the gridliner example code. The overloaded toolbar callbacks recreate the gridlines everytime zoom/pan/home is used.
I used python3.8, matplotlib-3.4.2, cartopy-0.20
import sys
from PySide2 import QtWidgets
from PySide2.QtWidgets import QVBoxLayout
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import cartopy.crs as ccrs
class CustomNavigationToolbar(NavigationToolbar):
toolitems = [t for t in NavigationToolbar.toolitems if t[0] in ('Home', 'Pan', 'Zoom', 'Save')]
def __init__(self, canvas, parent, coordinates=True, func_recreate_gridlines=None):
print('CustomNavigationToolbar::__init__')
super(CustomNavigationToolbar, self).__init__(canvas, parent, coordinates)
self.func_recreate_gridlines = func_recreate_gridlines
def home(self, *args):
print('CustomNavigationToolbar::home')
super(CustomNavigationToolbar, self).home(*args)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
def release_pan(self, event):
print('CustomNavigationToolbar::release_pan')
super(CustomNavigationToolbar, self).release_pan(event)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
def release_zoom(self, event):
print('CustomNavigationToolbar::release_zoom')
super(CustomNavigationToolbar, self).release_zoom(event)
if self.func_recreate_gridlines is not None:
self.func_recreate_gridlines()
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
print('ApplicationWindow::__init__')
super().__init__()
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
self.layout = QVBoxLayout(self._main)
self.fig = Figure()
self.canvas = FigureCanvas(self.fig)
self.toolbar = CustomNavigationToolbar(self.canvas, self,
coordinates=True,
func_recreate_gridlines=self.recreate_gridlines)
self.layout.addWidget(self.canvas)
self.addToolBar(self.toolbar)
# figure setup taken from gridlines example at
# https://scitools.org.uk/cartopy/docs/latest/matplotlib/gridliner.html
projection = ccrs.RotatedPole(pole_longitude=120.0, pole_latitude=70.0)
self.ax = self.canvas.figure.add_subplot(1, 1, 1, projection=projection)
self.ax.set_extent([-6, 3, 48, 58], crs=ccrs.PlateCarree())
self.ax.coastlines(resolution='10m')
self._gl = None
self.recreate_gridlines()
def recreate_gridlines(self):
print('ApplicationWindow::recreate_gridlines')
print(' remove old gridliner artists')
if self._gl is not None:
for artist_coll in [self._gl.xline_artists, self._gl.yline_artists, self._gl.xlabel_artists, self._gl.ylabel_artists]:
for a in artist_coll:
a.remove()
self.ax._gridliners = []
print(' self.ax.gridlines()')
self._gl = self.ax.gridlines(crs=ccrs.PlateCarree(),
draw_labels=True, dms=True, x_inline=False, y_inline=False)
if __name__ == "__main__":
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
qapp.exec_()
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)
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.
I try to embed matplotlib into PyQT GUI made in Qt designer. In .ui on a widget tab I defined:
GraphWidget, that is a QWidget
and
Graph1Layout, that is a QVBoxLayout.
The code is below. I took some parts of the code from other application written by other person (class ParentCanvas and class PlotCanvas) so I do not completely understand what is going on there, but as far as I googled, that is the almost standard way to include matplotlib widget in GUI. I shortened my existing code and existing GUI to do a little example.
What is intended: I want to plot some points whenever I click run and delete the graph when I click stop (I tried just to clear the points from graph but without success).
Is this correct implementation?
Maybe somebody can make it more simple or better?
Is there a better way to delete widget?
How to delete only points on the plot?
I am a beginner in OOP and python in general. Any other comments for improving the code are welcome.
from __future__ import division
import sys,os
from PyQt4 import QtGui, QtCore, uic # Used to create and modify PyQt4 Objects.
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas # Used in creating the plots and graphs.
from matplotlib.figure import Figure
class ParentCanvas(FigureCanvas):
"""Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
fig.set_edgecolor('w')
fig.set_facecolor('w')
self.axes = fig.add_subplot(111)
self.axes.hold(False)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
class PlotCanvas(ParentCanvas):
""" Creates a matplotlib canvas QWidget inheriting the features set out in ParentCanvas."""
def compute_initial_figure(self):
self.axes.set_xbound(lower=0, upper=1)
self.axes.set_ybound(lower=0, upper=1)
self.axes.set_ylabel('Best Fitness Function')
self.axes.set_xlabel('Population')
class Main(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
uic.loadUi('Test.ui', self)
self.SignalsAndSlots()
self.firstrun = True
self.iter=0
def SignalsAndSlots(self): # Function setting up the signals and slots.
self.btnStopOpt.clicked.connect(self.clickStop)
self.btnRun.clicked.connect(self.clickRun)
def clickStop(self): # Function that runs when the "Stop" button is clicked.
self.firstrun = True
self.iter =0
self.gb.setParent(None)
def clickRun(self): # Function that runs when the "Run Optimization" button is clicked.
if self.firstrun == True :
#graph setup
self.gb = PlotCanvas(self.Graph1Widget, width=10, height=7, dpi=70)
self.gb.axes.set_ylabel('Objective Value')
self.gb.axes.set_xlabel('Generation')
self.Graph1Layout.addWidget(self.gb)
self.firstrun = False
self.UpdateGraph(self.iter)
self.iter+=1
def UpdateGraph(self, iter): # Function that creates a graph
best = [0,0,0,3,5,9,12,30]
average = [0,0,0,1,2,3,14,20]
t = range(iter+1) # Will be used to store the generation numbers.
s = best[:iter+1] # Will be used to store the global best values.
self.gb.axes.hold(True) # Holds the graph so we can plot the global best and average scores as two separate scatter plots on to one graph.
self.gb.axes.scatter(t, s, c='b', label='Best objective value')
if iter == 0: # In the first generation we need to make a legend for the graph, this only need to be made once.
self.gb.axes.legend(loc='upper center', bbox_to_anchor=(0.5, 1.09), fancybox=True, shadow=True, ncol=5) # Legend content is determined by the labels above. Location is determined by bbox_to_anchor.
self.gb.axes.autoscale_view(True,False,True)
self.gb.draw()
return
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Main(None) # instantiation
app.setActiveWindow(window)
window.show() # show window
sys.exit(app.exec_()) # Exit from Python
I am learning matplotlib with python. the task is to embed a plot in a UI. the plot is to be redrawn upon reception of some event.
The UI application takes a QtDesigner generated class, whic is basically 4000 lines of
self.BRIGHTNESS = QtGui.QSlider(ZenMainForm)
self.BRIGHTNESS.setGeometry(QtCore.QRect(463, 73, 32, 131))
etcetera, generates some other objects and appedns them to the generated class, before it gets drawn.
I have identified this process and been able to add sliders, radio buttons and other standard QWidget-derived objects.
However, now I need to embed the said graphic. There are plenty of tutorials, but they create a Picture on a Canvas and then add Axes to it. Unfortunately, I do not understand this process, and, above all, do not understand how can I create a QWidget, containing a mutable plot. From there on, it is one line to integrate it in the application.
I deleted everything not relevant from the tutorial. Then I started integrating my code into the tutorial code, until it broke. This highlighted my mistake. Thanks everyone for the invaluable comments!
Below is the modified minimal version of the tutorial. Just use DynamicMplCanvas as an ordinary QWidget.
# Copyright (C) 2005 Florent Rougon
# 2006 Darren Dale
#
# This file is an example program for matplotlib. It may be used and
# modified with no restriction; raw copies as well as modified versions
# may be distributed without limitation.
from __future__ import unicode_literals
import sys, os, random
from PyQt4 import QtGui, QtCore
from numpy import arange, sin, pi
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class MplCanvas(FigureCanvas):
"""Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
# We want the axes cleared every time plot() is called
self.axes.hold(False)
self.compute_initial_figure()
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
class DynamicMplCanvas(MplCanvas):
"""A canvas that updates itself every second with a new plot."""
def __init__(self, *args, **kwargs):
MplCanvas.__init__(self, *args, **kwargs)
timer = QtCore.QTimer(self)
QtCore.QObject.connect(timer,
QtCore.SIGNAL("timeout()"),
self.update_figure)
timer.start(1000)
def compute_initial_figure(self):
self.axes.plot([0, 1, 2, 3], [1, 2, 0, 4], 'r')
def update_figure(self):
# Build a list of 4 random integers between 0 and 10 (both inclusive)
l = [ random.randint(0, 10) for i in range(4) ]
self.axes.plot([0, 1, 2, 3], l, 'r')
self.draw()