I have a written a large data processing and plotting application using PyQt5 with Matplotlib and I am plagued by old plots reappearing when creating new ones. I have seen many questions answered regarding this but none of the suggested methods work for me.
I have reduced the problem down to the following simplified code:
from PyQt5.QtWidgets import (QApplication, QMainWindow, QDialog, QPushButton,
QVBoxLayout, QGroupBox)
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import sys
class PlotCanvas(FigureCanvas):
""" Subclass of the MPL FigureCanvas class. """
def __init__(self, parent=None):
self.parent = parent
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
self.fig.set_tight_layout(True)
FigureCanvas.__init__(self, self.fig)
class PlotWindow(QDialog):
""" Create plot in window and plot supplied data. """
def __init__(self, parent, x, y):
super().__init__()
self.parent = parent
self.x = x
self.y = y
self.set_up_gui()
self.l2d = self.canvas.ax.plot(self.x, self.y)[0]
def set_up_gui(self):
""" Create GUI. """
vbl = QVBoxLayout()
self.canvas = PlotCanvas(self)
vbl.addWidget(self.canvas)
self.setLayout(vbl)
def closeEvent(self, event):
""" Delete figure on closing window. """
plt.close(self.canvas.fig)
super(PlotWindow, self).closeEvent(event)
class MainApp(QMainWindow):
""" My main application class """
def __init__(self, parent=None):
super(QMainWindow, self).__init__()
# Create some arbitrary data
self.x = [1, 2, 3, 4, 5, 6, 7, 8, 9]
self.y = [2, 4, 6, 4, 2, 4, 6, 8, 6]
self.set_up_gui()
def set_up_gui(self):
""" Create GUI with 2 buttons for creating 2 plots. """
btn1 = QPushButton()
btn1.setText('Button 1')
btn1.clicked.connect(self.plot1)
btn2 = QPushButton()
btn2.setText('Button 2')
btn2.clicked.connect(self.plot2)
vbl = QVBoxLayout()
vbl.addWidget(btn1)
vbl.addWidget(btn2)
gb = QGroupBox()
gb.setLayout(vbl)
self.setCentralWidget(gb)
def plot1(self):
""" Plot using my PlotWindow class. """
p = PlotWindow(self, self.x, self.y)
p.show()
def plot2(self):
""" Plot using regular MPL plot call. """
fig = plt.figure()
plt.plot([10, 20, 30], [4, 8, 2])
plt.show()
plt.close(fig)
if __name__=='__main__':
app = QApplication(sys.argv)
main = MainApp()
main.show()
sys.exit(app.exec_())
The odd behavior is this:
Press button 1, plot 1 appears, press red X to close window.
Press button 2, plot 2 appears, press red X to close window.
Press button 1, plot 1 appears, press red X to close window.
Press button 2, plot 2 appears but so does plot 1, press red X to close plot 2, plot 1 also closes.
From here on, button 1 brings up plot 1, and button 2 brings up both plots.
Based on answers to similar questions (such as matplotlib.pyplot will not forget previous plots - how can I flush/refresh?), I have tried various things, such as following plt.show() with plt.cla() plt.clf() and plt.close() but none of them fix the problem. As you can see in my code, I have added the the plt.close() to the PyQt5 window close event and also the regular MPL plot but this seems to do nothing.
EDIT: Now that the question is answered, I have cleaned up some of the musings that followed and reduced it to the pertinent information.
I tried this code out on my work Linux machine (originally developed on Mac) and don't get the same double plot behavior. Which shows it's platform-dependent.
On Linux I can actually remove the plt.close() from the plot2 method and I can remove the closeEvent method from the PlotWindow class and it works fine without any double plots showing.
Ok, through trial end error, I have fixed this problem. I don't claim to know exactly the mechanics of how it is fixed. Perhaps seeing how I fixed it, some clever person can explain why it fixes it.
If I change the closeEvent method in PlotCanvas to the following (where I have marked the added lines with asterisks):
def closeEvent(self, event):
""" Delete figure on closing window. """
self.canvas.ax.cla() # ****
self.canvas.fig.clf() # ****
plt.close(self.canvas.fig)
super(PlotWindow, self).closeEvent(event)
and change the plot2 method to the following:
def plot2(self):
""" Plot using regular MPL call. """
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([10, 20, 30], [4, 8, 2])
plt.show()
ax.cla() # ****
fig.clf() # ****
plt.close(fig)
I now get no multiple plots appearing. I would have thought that axes would be closed along with the figure container when it is closed, but apparently not. I would also hope that I would get the same behavior independent of platform but it seems the MacOSX backend acts differently to others.
Related
I created a subplot in a figure for displaying an image with imshow, and I add a colorbar. With no animation, I can change the colormap changing the value of the ComboBox and the colorbar is updated correctly.
But if I add an animation, the colorbar disappear each time I change the colormap. I have to click on another window (other software, etc) or resize the GUI to see the colorbar again.
Here is an example to understand the problem :
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib import animation
class FenetrePrincipale(QWidget):
def __init__(self, parent=None):
super(FenetrePrincipale, self).__init__(parent)
self.setupUi(self)
# Fonction de configuration de la classe
def setupUi(self, Form):
self.Form = Form
Form.setMinimumSize(1220, 850)
self.creation_GUI()
self.creation_figure()
self.creation_layout()
self.tabWidget.setCurrentIndex(0)
self.Bouton_quitter.clicked.connect(self.close)
self.anim = animation.FuncAnimation(self.figure, self.animate, interval=10, blit=True)
self.Widget_choixPalette_ComboBox.currentIndexChanged.connect(self.changementPalette)
def changementPalette(self, onglet):
self.image.set_cmap('binary')
self.canvas.draw()
def animate(self, i):
# a = self.thread_1.img
self.image.set_array(self.imageInit)
return [self.image]
def resizeEvent(self, QResizeEvent):
self.tabWidget.setMinimumSize(QSize(self.width() - 20, self.height() - 60))
def creation_GUI(self):
self.tabWidget = QTabWidget()
self.tab1 = QWidget()
self.tabWidget.addTab(self.tab1, " Tab1 ")
self.Widget_choixPalette_Label = QLabel(self.tab1)
self.Widget_choixPalette_Label.setText("Text1")
self.Widget_choixPalette_ComboBox = QComboBox(self.tab1)
self.Widget_choixPalette_ComboBox.addItem("Try1")
self.Widget_choixPalette_ComboBox.addItem("Try2")
self.Bouton_quitter = QPushButton(self.tab1)
self.Bouton_quitter.setText("Quit")
def creation_layout(self):
LayoutForm = QGridLayout(self)
LayoutForm.addWidget(self.tabWidget, 0, 0, 1, 1)
LayoutTab1 = QGridLayout(self.tab1)
LayoutTab1.addWidget(self.Widget_choixPalette_Label, 0, 1, 1, 1)
LayoutTab1.addWidget(self.Widget_choixPalette_ComboBox, 1, 1, 1, 1)
self.Widget_choixPalette_ComboBox.setMinimumWidth(200)
LayoutTab1.addWidget(self.canvas, 2, 0, 1, 3)
LayoutTab1.addWidget(self.Bouton_quitter, 2, 3, 1, 1, Qt.AlignRight | Qt.AlignBottom)
LayoutTab1.setRowStretch(2, 1)
LayoutTab1.setColumnStretch(0, 1)
LayoutTab1.setColumnStretch(2, 1)
def creation_figure(self):
# Create figure (transparent background)
self.figure = plt.figure()
# self.figure.patch.set_facecolor('None')
self.canvas = FigureCanvas(self.figure)
self.canvas.setStyleSheet("background-color:transparent;")
# Adding one subplot for image
self.axe0 = self.figure.add_subplot(111)
self.axe0.get_xaxis().set_visible(False)
self.axe0.get_yaxis().set_visible(False)
# Data for init image
self.imageInit = [[255] * 320 for i in range(240)]
self.imageInit[0][0] = 0
# Init image and add colorbar
self.image = self.axe0.imshow(self.imageInit, interpolation='none')
divider = make_axes_locatable(self.axe0)
cax = divider.new_vertical(size="5%", pad=0.05, pack_start=True)
self.colorbar = self.figure.add_axes(cax)
self.figure.colorbar(self.image, cax=cax, orientation='horizontal')
plt.subplots_adjust(left=0, bottom=0.05, right=1, top=1, wspace=0, hspace=0)
self.canvas.draw()
if __name__ == '__main__':
app = QApplication(sys.argv)
# QApplication.setStyle(QStyleFactory.create("plastique"))
form = FenetrePrincipale()
form.show()
sys.exit(app.exec_())
When I change of colormap selecting any choice in combobox :
What I am waiting to see :
Operating system: Windows 7 Pro
Matplotlib version: 2.1.0
Matplotlib backend: Qt5Agg
Python version: 3.6
a] blit=False
Use blit=False in the animation. Otherwise only the image itself will be updated and the redrawing of the complete canvas is posponed to some event that makes this redrawing necessary, e.g. a resize event.
b] pause animation
In case you cannot affort using blit=False, you can pause the animation, change the colormap, draw the canvas, then continue the animation.
def changementPalette(self, onglet):
self.anim.event_source.stop()
if onglet==0:
self.image.set_cmap('viridis')
else:
self.image.set_cmap('binary')
self.canvas.draw_idle()
self.anim.event_source.start()
I'm brand new to matplotlib. I have found many examples of embedding bar graphs and plots into a wxPython panel. When I try to replace a graph or plot with a pie chart, the pie chart displays and only when it closes does the panel displays. It doesn't embed. I've been searching for 2 days.
I would like to embed this or this pie chart into this example.
I can't take the code for the pie chart and replace the code for the graph in the example. (Which means I'm missing something pretty big)
This is what I have tried:
import wx
import matplotlib
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.pyplot as plt
class MainFrame(wx.Frame):
"""docstring for MainFrame"""
def __init__(self, parent):
wx.Frame.__init__(self, parent, title=">>>> by www.UmarYusuf.com", size=(800, 580))
# Add SplitterWindow panels
self.split_win = wx.SplitterWindow(self)
self.top_split = MatplotPanel(self.split_win)
self.bottom_split = wx.Panel(self.split_win, style=wx.SUNKEN_BORDER)
self.split_win.SplitHorizontally(self.top_split, self.bottom_split, 480)
# Add some contrls/widgets (StaticText and Buttons)
# Add Text control to the bottom_split window
self.text1 = wx.StaticText(self.bottom_split, -1, u"You can also plot from file", size=(250, 30), pos=(510, 10), style=wx.ALIGN_CENTER)
self.text1.SetBackgroundColour('Gray')
font = wx.Font(15, wx.SWISS, wx.NORMAL, wx.NORMAL)
self.text1.SetFont(font)
# Add Buttons to the bottom_split window and bind them to plot functions
self.Button1 = wx.Button(self.bottom_split, -1, "Plot1", size=(80, 40), pos=(10, 10))
self.Button1.Bind(wx.EVT_BUTTON, self.plot1)
self.Button2 = wx.Button(self.bottom_split, -1, "Plot2", size=(80, 40), pos=(110, 10))
self.Button2.Bind(wx.EVT_BUTTON, self.plot2)
self.Button3 = wx.Button(self.bottom_split, -1, "Plot3", size=(80, 40), pos=(210, 10))
self.Button3.Bind(wx.EVT_BUTTON, self.plot3)
def plot1(self, event):
pass
def plot2(self, event):
pass
def plot3(self, event):
pass
def plot4(self, event):
pass
def plot5(self, event):
pass
class MatplotPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent,-1,size=(50,50))
self.figure = Figure()
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs')
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.show()
self.canvas = FigureCanvas(self, -1, self.figure)
app = wx.App()
frame = MainFrame(None).Show()
app.MainLoop()
Thank you.
There is a question about embedding matplotlib into wxpython, where the answer is showing a minimal example. Several examples are also available on the matplotlib examples page.
The main problem you are facing here is that you are creating two different figures. The first one, self.figure = Figure(), is the one you connect to the canvas, but the second one, fig1 is the one you plot your graph to. The second one is the one shown in a new window when plt.show() gets called.
However, calling plt.show() does not make any sense when embedding into GUIs since the GUI should take care of showing the figure.
Therefore the MatplotPanel class from the question should look like this:
class MatplotPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent,-1,size=(50,50))
self.figure = Figure()
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
ax1 = self.figure.add_subplot(111)
ax1.pie(sizes, labels=labels, autopct='%1.1f%%')
ax1.axis('equal')
self.canvas = FigureCanvas(self, -1, self.figure)
This question already has answers here:
How to draw vertical lines on a given plot
(6 answers)
Closed 12 months ago.
I would like to draw a vertical line with Matpotlib and I'm using axvline, but it doesn't work.
import sys
import matplotlib
matplotlib.use('Qt4Agg')
from ui_courbe import *
from PyQt4 import QtGui
from matplotlib import pyplot as plt
class Window(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.setupUi(self)
self.boutonDessiner.clicked.connect(self.generatePlot)
def generatePlot(self):
# generate the plot
ax = self.graphicsView.canvas.fig.add_subplot(111)
ax.plot([1,3,5,7],[2,5,1,-2])
plt.axvline(x=4)
self.graphicsView.canvas.draw()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
I can see my plot, but no vertical line. Why?
Your example is not self contained, but I think you need to replace:
plt.axvline(x=4)
with:
ax.axvline(x=4)
You are adding the line to an axis that you are not displaying. Using plt. is the pyplot interface which you probably want to avoid for a GUI. So all your plotting has to go on an axis like ax.
matplotlib.pyplot.vlines
The difference is that you can pass multiple locations for x as a list, while matplotlib.pyplot.axvline only permits one location.
Single location: x=37
Multiple locations: x=[37, 38, 39]
If you're plotting a figure with something like fig, ax = plt.subplots(), then replace plt.vlines or plt.axvline with ax.vlines or ax.axvline, respectively.
import numpy as np
import matplotlib.pyplot as plt
xs = np.linspace(1, 21, 200)
plt.vlines(x=[37, 38, 39], ymin=0, ymax=len(xs), colors='purple', ls='--', lw=2, label='vline_multiple')
plt.vlines(x=40, ymin=0, ymax=len(xs), colors='green', ls=':', lw=2, label='vline_single')
plt.axvline(x=36, color='b', label='avline')
plt.legend()
plt.show()
I am writing an image processing module in Python using matplotlib.pyplot and numpy backend.
The images will largely be in tiff format, so the code below uses tifffile to convert a 3D image file to a 4D array in numpy. The below code aims to move through the z-plane of the 3D image, one image at a time, using z and x as hotkeys. My problem is quite interesting and I can't figure it out: the time between event and action (pressing x and displaying z+1 image) gets twice as long with each event.
I timed it, results below:
1st z-press: 0.124 s
2nd z-prss: 0.250 s
3rd z-press: 0.4875 s
It is a bonafide linear increase, but I can't find where in my code the bug could be.
import matplotlib.pyplot as plt
import numpy as np
import tifffile as tiff
class Image:
def __init__ (self, fname):
self.fname = fname
self.fig = plt.figure()
self.z = 0
self.ax = self.fig.add_subplot(111)
self.npimg = tiff.imread(self.fname)
self.plotimg()
self.connect()
def plotimg(self):
plt.imshow(self.npimg[self.z][0])
plt.show()
def connect(self):
self.cidkeypress = self.fig.canvas.mpl_connect('key_press_event',self.keypress)
def disconnect(self):
self.fig.canvas.mpl_disconnect(self.cidkeypress)
def keypress(self, event):
if event.key == 'x':
self.z += 1
self.plotimg()
elif event.key == 'z':
self.z -= 1
self.plotimg()
Since you didn't provide an example file, I can't test your code. But I think the problem is, that you call plt.imshow() repeatedly. Each time a new AxesImage object is added to the Figure. This is why the timings increase linearly.
So the solution is to only have one AxesImage object and only update the data. Adapt __init__ and plotimg as follows:
def __init__ (self, fname):
self.fname = fname
self.fig = plt.figure()
self.z = 0
self.ax = self.fig.add_subplot(111)
self.npimg = tiff.imread(self.fname)
self.pltim = plt.imshow(self.npimg[self.z][0])
self.connect()
plt.show()
def plotimg(self):
self.pltim.set_data(self.npimg[self.z][0])
plt.draw()
Beware: Untested code!
EDIT:
Difference between plt.imshow and plt.show:
Despite their similar name, they do very different things: plt.imshow is a plotting function, which draws an image to the current axes (similar to plt.plot, plt.scatter, etc.) plt.show however, dispays the figure on the screen and enters the backends mainloop (i.e. blocks your code). This answer explains it in more detail
I am working on a Tkinter-GUI to interactively generate Matplotlib-plots, depending on user-input. For this end, it needs to re-plot after the user changes the input.
I have gotten it to work in principle, but would like to include the NavigationToolbar. However, I cannot seem to get the updating of the NavigationToolbar to work correctly.
Here is a basic working version of the code (without the user input Entries):
# Import modules
from Tkinter import *
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# global variable: do we already have a plot displayed?
show_plot = False
# plotting function
def plot(x, y):
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax1.plot(x,y)
return fig
def main():
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x)
fig = plot(x, y)
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
global show_plot
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar_frame = Frame(root)
toolbar_frame.grid(row=1,column=1)
root.mainloop()
Pressing "Plot!" once generates the plot and the NavigationToolbar.
Pressing it a second time replots, but generates a second NavigationToolbar (and another every time "Plot!" is pressed). Which sounds like grid_forget() is not working.
However, when I change
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
to
if show_plot:
#remove existing plot and toolbar widgets
canvas.get_tk_widget().grid_forget()
toolbar_frame.grid_forget()
else:
toolbar_frame.grid(row=1,column=1)
canvas.get_tk_widget().grid(row=0,column=1)
show_plot=True
then the NavigationToolbar does vanish when "Plot!" is pressed a second time (but then there is, as expected, no new NavigationToolbar to replace the old). So grid_forget() is working, just not as expected.
What am I doing wrong? Is there a better way to update the NavigationToolbar?
Any help greatly appreciated!
Lastalda
Edit:
I found that this will work if you destroy the NavigationToolbar instead of forgetting it. But you have to completely re-create the widget again afterwards, of course:
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar_frame = Frame(root)
global show_plot
if show_plot: # if plot already present, remove plot and destroy NavigationToolbar
canvas.get_tk_widget().grid_forget()
toolbar_frame.destroy()
toolbar_frame = Frame(root)
toolbar = NavigationToolbar2TkAgg(canvas, toolbar_frame)
toolbar_frame.grid(row=21,column=4,columnspan=3)
canvas.get_tk_widget().grid(row=1,column=4,columnspan=3,rowspan=20)
show_plot = True
However, the updating approach showed by Hans below is much nicer since you don't have to destroy and recreate anything. I just wanted to highlight that the issue with my approach (apart from the inelegance and performance) was probably that I didn't use destroy().
A slightly different approach might be to reuse the figure for subsequent plots by clearing & redrawing it. That way, you don't have to destroy & regenerate neither the figure nor the toolbar:
from Tkinter import Tk, Button
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
# plotting function: clear current, plot & redraw
def plot(x, y):
plt.clf()
plt.plot(x,y)
# just plt.draw() won't do it here, strangely
plt.gcf().canvas.draw()
# just to see the plot change
plotShift = 0
def main():
global plotShift
x = np.arange(0.0,3.0,0.01)
y = np.sin(2*np.pi*x + plotShift)
plot(x, y)
plotShift += 1
# GUI
root = Tk()
draw_button = Button(root, text="Plot!", command = main)
draw_button.grid(row=0, column=0)
# init figure
fig = plt.figure()
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2TkAgg(canvas, root)
canvas.get_tk_widget().grid(row=0,column=1)
toolbar.grid(row=1,column=1)
root.mainloop()
When you press the "Plot!" button it calls main. main creates the navigation toolbar. So, each time you press the button you get a toolbar. I don't know much about matplot, but it's pretty obvious why you get multiple toolbars.
By the way, grid_forget doesn't destroy the widget, it simply removes it from view. Even if you don't see it, it's still in memory.
Typically in a GUI, you create all the widgets exactly once rather than recreating the same widgets over and over.