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
Related
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.
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 want to connect a QPushButton in my QMainWindow with a class that i created using Matplotlib so i can show a grid when i push the button. This is a part of the code:
class Ventana(QMainWindow):
def __init__(self):
super(QMainWindow, self).__init__()
self.channel = ViewWidget()
#I add a toolbar and i put the button in here
self.toolbar2.addWidget(self.btn_showgrid)
self.btn_showgrid.setEnabled(True)
self.connect(self.btn_showgrid, SIGNAL("clicked()"), self.showGrid)
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
class ViewWidget(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.mainWidget = QWidget()
self.setCentralWidget(self.mainWidget)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
My method called showGrid set the button to "disable" (is what i want to when the button is pushed) but it does not shows the grid. What am i doing wrong?
Hope you can help me. Thanks for your answers
------------------------- EDIT --------------------------------
I´ve made a few changes. I created the QPushButton and i added it to the toolbar of the plot.
# create a simple widget to extend the navigation toolbar
self.btn_showgrid = QPushButton("Show Grid")
self.btn_showgrid.setEnabled(True)
self.btn_hidegrid = QPushButton("Hide Grid")
self.btn_hidegrid.setEnabled(False)
layout = QVBoxLayout()
self.mainWidget.setLayout(layout)
self.figure_canvas = FigureCanvas(Figure())
layout.addWidget(self.figure_canvas, 10)
self.axes = self.figure_canvas.figure.add_subplot(111)
self.axes.grid(False)
x = np.arange(0,5,0.1)
y = np.sin(x)
self.axes.plot(x,y)
I also put a line: self.axes.grid(False) as you can see above. And at last i created this method:
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.btn_hidegrid.setEnabled(True)
self.axes.grid(True)
self.axes.draw()
The problem now is that when i push the button, it only hides like it should, but the grid does no show. But if create a new plot in the same `QMainWindow, it works!!!!
I think i need to refresh the plot at the moment i make self.axes.grid(True), but the draw() does not work. How can i accomplish this? I mean, refresh the plot?
You need to tell the canvas to re-draw. Drawing can be expensive so updating the state of the artists does not trigger a re-draw (the pyplot API does do that but you should not use that here). I think
def showGrid(self):
self.btn_showgrid.setEnabled(False)
self.channel.axes.grid(True)
self.channel.canvas.draw_idle()
will do the trick. The call to draw_idle tells Qt to, the next time it re-paints window to also trigger an Agg redraw for mpl, and to please schedule a repaint 'soon'.
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()
My program (developed with GTK using glade) receives some data and has the option to display a seperate window containing a matplotlib scatterplot that represents the data.
My problem is that if the user closes the graph window and reopens it, no graph is displayed. It is just a blank GTK Window. I'm sure there is a simple fix, but there aren't many resources available that are relevant to my issue (or GTK and matlplotlib integration for that matter).
I have created a Module for my scatterplot so I can easily reuse it. I am just trying to get it to work, so the code isn't structured perfectly.
##Scatterplot Module:
import gtk
import matplotlib
from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
from matplotlib.figure import Figure
class ScatterPlot:
def __init__(self):
self.window = gtk.Window()
self.window.connect("destroy", lambda x: self.destroy())
self.window.set_default_size(500,400)
self.is_hidden = False
self.figure = Figure(figsize = (5,4), dpi=100)
self.ax = self.figure
self.ax = self.ax.add_subplot(111)
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
self.Xs = list()
self.Ys = list()
def set_axis(self, xLimit = (0,384) , yLimit = (0,100)):
self.ax.set_xlim(xLimit)
self.ax.set_ylim(yLimit)
def plot(self, xs, ys):
self.Xs.extend([xs])
self.Ys.extend([ys])
self.ax.plot(xs,ys,'bo')
def update(self):
self.window.add(self.canvas)
def set_title(self, title):
self.ax.set_title(title)
def show(self):
self.window.show_all()
self.is_hidden = False
def hide(self):
self.window.hide()
self.is_hidden = True
def destroy(self):
self.window.destroy()
I call the module like so:
class GUI:
def __init__(self):
self.scatterplot = scatterplot.ScatterPlot()
#When the user presses the "Graph" button it calls the following function
def graph():
self.scatterplot.plot(someDataX, someDataY)
self.scatterplot.set_axis()
self.scatterplot.set_title("Some Title")
self.scatterplot.show()
(This was just an example of what my code looks like.)
When the scatterplot is closed, I am calling self.window.destroy instead of self.window.hide. When reopening is attempted, I call the same graph() function but, as stated above, the GTK Window does not display the graph. (When I first open it, it displays perfectly)
My speculations:
Should I be calling .hide() instead of .destroy()?
Is there a piece of code in scatterplot's constructor that needs to be called again to create the plot?
Or should I just re-instantiate the plot every time graph() is called?
My Solution:
From:
class ScatterPlot:
def __init__(self):
#remove the following two lines
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
Move the two lines of code to show()
def show(self):
self.canvas = FigureCanvas(self.figure)
self.window.add(self.canvas)
self.window.show_all()
self.is_hidden = False
Moving these two lines of code allows the graph to be displayed when re-opening the window.
Sidenote: Calling both .destroy() or .show() when closing the window will work. I'm not sure which one is better though.