wxpython + matplotlib: closing figure hangs python - python

This python program plots a figure in a wxpython window. It runs fine in Enthought Python Distribution 7.3-2, but when I close the figure, python keeps running and doesn't exit.
What am I missing? When I create wxpython GUIs that don't use matplotlib, they exit correctly after I close the window. I guess there must be something about message loops in wxpython that I don't understand.
# adapted from:
# http://wiki.wxpython.org/Getting%20Started
# http://www.cs.colorado.edu/~kena/classes/5448/s11/presentations/pearse.pdf
import wx
import pylab as pl
import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
class GUIPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.parent = parent
# create some sizers
sizer = wx.BoxSizer(wx.VERTICAL)
# A button
self.button =wx.Button(self, label="Tada!")
self.Bind(wx.EVT_BUTTON, self.OnClick,self.button)
# put up a figure
self.figure = pl.figure()
self.axes = self.drawplot(self.figure)
self.canvas = FigureCanvas(self, -1, self.figure)
sizer.Add(self.canvas, 0, wx.ALIGN_CENTER|wx.ALL)
sizer.Add(self.button, 0, wx.ALIGN_CENTER|wx.ALL)
self.SetSizerAndFit(sizer)
def log(self, fmt, *args):
print (fmt % args)
def OnClick(self,event):
self.log("button clicked, id#%d\n", event.GetId())
def drawplot(self, fig):
ax = fig.add_subplot(1,1,1)
t = pl.arange(0,1,0.001)
ax.plot(t,t*t)
ax.grid()
return ax
app = wx.App(False)
frame = wx.Frame(None)
panel = GUIPanel(frame)
frame.Fit()
frame.Center()
frame.Show()
app.MainLoop()

Huh. Per the discussion in comments from tcaswell, I changed
self.figure = pl.figure()
to
self.figure = matplotlib.figure.Figure()
and it fixed the problem. (python exits when I close the window)

Related

Embedding matplotlib FuncAnimation in wxPython: Unwanted figure pop-up

I have tried to modify the following example for a live plot.
Embedding a matplotlib figure inside a WxPython panel
I am trying to read the serial data coming from Arduino and plot/update the collected data. The problem is that the figure comes up before the wx App and I need to close the figure in order to see the wx App.
I believe that the problem is related with the following lines but I don't know why.
self.figure = plt.figure(figsize=(20,20))
self.ax = plt.axes(xlim=(0, 1000), ylim=(0, 5000))
The script is as follows.
import wx
from matplotlib.figure import Figure as Fig
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar
from collections import deque
import serial
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mlp
import numpy as np
# Class that inherits wx.Panel. The purpose is to embed it into
# a wxPython App. That part can be seen in main()
class Serial_Plot(wx.Panel):
def __init__(self, parent, strPort, id=-1, dpi=None, **kwargs):
super().__init__(parent, id=id, **kwargs)
self.figure = plt.figure(figsize=(20,20))
self.ax = plt.axes(xlim=(0, 1000), ylim=(0, 5000))
self.plot_data, = self.ax.plot([], [])
self.canvas = FigureCanvas(self, -1, self.figure)
self.toolbar = NavigationToolbar(self.canvas)
self.toolbar.Realize()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas, 1, wx.EXPAND)
sizer.Add(self.toolbar, 0, wx.RIGHT | wx.EXPAND)
self.SetSizer(sizer)
# Serial communication
self.ser = serial.Serial(strPort, 115200)
# Serial data initialized as deque. The serial readings from arduino
# are set to be one value per line.
self.vals = deque()
# matplotlib function animation
anim = animation.FuncAnimation(self.figure, self.update,
interval=20)
plt.show()
self.close
def update(self, i):
try:
# read serial line
data = float(self.ser.readline().decode('utf-8'))
self.vals.append(data)
# update plot data
self.plot_data.set_data(range(len(self.vals)), self.vals)
except:
pass
return self.plot_data
def close(self):
# close serial
self.ser.flush()
self.ser.close()
def main():
app = wx.App(False)
frame = wx.Frame(None, -1, "WX APP!")
demo_plot = Serial_Plot(frame,'COM3')
frame.Show()
app.MainLoop()
if __name__ == "__main__":
main()
Here is the figure popping up before the GUI.
After closing the figure wx app is visible.
I am trying to get rid of the figure that is popping up and only see the figure embedded in wx app. I would really appreciate any help.
I think that you may have got hold of the wrong end of the stick by using animation.FuncAnimation because I think that that is a matplotlib function which will be expecting to be controlled by matplotlib's main loop but you are using wxpython which has its own. (I reserve the right, at this point, to be horribly wrong :) )
Below is your code, reworked to use random to avoid a serial port and including a wx.Timer to perform the updates.
import wx
from matplotlib.figure import Figure as Fig
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar
from collections import deque
#import serial
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mlp
import numpy as np
import random
# Class that inherits wx.Panel. The purpose is to embed it into
# a wxPython App. That part can be seen in main()
class Serial_Plot(wx.Panel):
def __init__(self, parent, strPort, id=-1, dpi=None, **kwargs):
super().__init__(parent, id=id, **kwargs)
self.figure = plt.figure(figsize=(20,20))
self.ax = plt.axes(xlim=(0, 10), ylim=(0, 50))
self.plot_data, = self.ax.plot([], [])
self.canvas = FigureCanvas(self, -1, self.figure)
self.toolbar = NavigationToolbar(self.canvas)
self.toolbar.Realize()
#
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.update, self.timer)
#
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas, 1, wx.EXPAND)
sizer.Add(self.toolbar, 0, wx.RIGHT | wx.EXPAND)
self.SetSizer(sizer)
# Serial communication
# self.ser = serial.Serial(strPort, 115200)
# Serial data initialized as deque. The serial readings from arduino
# are set to be one value per line.
self.vals = deque()
# matplotlib function animation
#anim = animation.FuncAnimation(self.figure, self.update,
# interval=2)
#plt.show()
plt.ion() #Turn on interactive plot
#self.close
#
self.timer.Start(1000)
def update(self,event):
#try:
# read serial line
#data = float(self.ser.readline().decode('utf-8'))
data = float(random.randint(1, 50))
self.vals.append(data)
# update plot data
length = len(self.vals)
self.plot_data.set_data(range(length), self.vals)
#Update x axis to follow interactive plot
self.ax.set_xlim(0.0,float(length + 1))
#except:
# pass
#return self.plot_data
plt.plot()
def close(self):
# close serial
self.ser.flush()
self.ser.close()
def main():
app = wx.App(False)
frame = wx.Frame(None, -1, "WX APP!")
demo_plot = Serial_Plot(frame,'COM3')
frame.Show()
app.MainLoop()
if __name__ == "__main__":
main()
N.B.
self.ax.set_xlim(0.0,float(length + 1)) could be adjusted to something like self.ax.set_xlim(float(length - 10), float(length + 1)) which would follow the current values, not just constantly extend the x axis.

Update wxpython figure canvas with new figure

In a wxPython application I have an embedded a FigureCanvas with a matplotlib figure. I want to be able to switch the figure by loading a new one. However, the figure is not being updated.
Answers to similar topics suggests that panel.canvas.draw() and panel.Refresh() should do the trick, but I've also tried panel.Update() and panel.canvas.Refresh(). I fear that this only works if you want to redraw the canvas with the same figure?
So my question is: how do you replace the figure inside a canvas and make it update?
Below is a small (non-working) example. First a figure is loaded with a single axis. If you from the embedded shell type panel.LoadFigure() a new figure with 2x2 subplots is created and put into the canvas. But the new figure is not shown.
import numpy as np
import wx
from wx.py.shell import Shell
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.pyplot as plt
class ShellPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.shell = Shell(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.shell, 1, wx.GROW)
self.SetSizer(self.sizer)
self.Layout()
self.Fit()
class FigurePanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.parent = parent
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.figure, ax = plt.subplots()
self.canvas = FigureCanvas(self, -1, self.figure)
self.shellpanel = ShellPanel(self)
s1 = wx.BoxSizer(wx.VERTICAL)
s1.Add(self.canvas, 0, wx.GROW)
s1.Add(self.shellpanel, 1 , wx.EXPAND)
self.sizer.Add(s1, 5, wx.GROW)
self.SetSizer(self.sizer)
self.Layout()
self.Fit()
def LoadFigure(self):
self.figure, ax = plt.subplots(2, 2)
self.canvas.draw()
self.Refresh()
class FigureFrame(wx.Frame):
def __init__(self, parent, id, title, size):
wx.Frame.__init__(self, parent, id, title, size=size)
if __name__ == "__main__":
app = wx.App(False)
fr = FigureFrame(None, -1, title='Figure Loader', size=(wx.DisplaySize()[0]/2, wx.DisplaySize()[1]*3/4))
panel = FigurePanel(fr)
fr.Show()
app.MainLoop()
Is there any reason you are trying to create a new Figure and not simply new Axes on the already defined Figure?
I would write your code like so:
def LoadFigure(self):
self.figure.clf() # clear current figure
# add arbitrary number of new Axes
self.figure.add_subplot(221)
self.figure.add_subplot(222)
self.figure.add_subplot(223)
self.figure.add_subplot(224)
self.canvas.draw() # refresh canvas
EDIT: following your comment, I think the problem is that you're creating a new figure, but your Canvas is still referencing the old one. I don't know if you can change that directly in the FigureCanvas properties, maybe someone with more experience can provide a better answer. For the moment, I would Destroy the previous canvas, and create a new FigureCanvas object with your new figure.
def LoadFigure(self):
self.figure, ax = plt.subplots(2, 2)
self.canvas.Destroy()
self.canvas = FigureCanvas(self, -1, self.figure)
self.canvas.draw()
Try using the Figure itself to make your figure, not pyplot. It has usually worked for me because it gives you more room to improvise.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import wx
from wx.py.shell import Shell
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.pyplot as plt
class ShellPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.shell = Shell(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.shell, 1, wx.GROW)
self.SetSizer(self.sizer)
self.Layout()
self.Fit()
class FigurePanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.parent = parent
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.figure = Figure(figsize = (8,6.1), dpi =60)
self.ax = self.figure.add_subplot(1,1,1)
self.enlarged_figure = Figure(figsize = (8,6.1), dpi = 100)
self.ax2 = self.enlarged_figure.add_subplot(2,1,2)
self.canvas = FigureCanvas(self, -1, self.figure)
self.canvas.Show()
self.enlarged_canvas = FigureCanvas(self,-1,self.enlarged_figure)
self.enlarged_canvas.Show()
self.shellpanel = ShellPanel(self)
s1 = wx.BoxSizer(wx.VERTICAL)
s1.Add(self.canvas, 0, wx.GROW)
s1.Add(self.shellpanel, 1 , wx.EXPAND)
self.sizer.Add(s1, 5, wx.GROW)
self.SetSizer(self.sizer)
self.Layout()
self.Fit()
def LoadFigure(self):
self.figure, ax = plt.subplots(2, 2)
self.canvas.draw()
self.Refresh()
class FigureFrame(wx.Frame):
def __init__(self, parent, id, title, size):
wx.Frame.__init__(self, parent, id, title, size=size)
if __name__ == "__main__":
app = wx.App(False)
fr = FigureFrame(None, -1, title='Figure Loader', size=(wx.DisplaySize()[0]/2, wx.DisplaySize()[1]*3/4))
panel = FigurePanel(fr)
fr.Show()
app.MainLoop()
In the shell type in 'panel.LoadFigure()' and the new figure will show up very quickly and then disappear. Then type 'panel.canvas.draw()' and the new figure will be there. Then type 'panel.LoadFigure()' to load the old figure back. Repeat.
I don't know why the figure remains hidden after the MainLoop() continues but this is sort of a quick fix to your problem.

FigureCanvasWxAgg not resizing properly in panel (or notebook) in linux

I've written a program in wxpython that works just fine in windows but when tested in lunix I have some Display issues that that all occur in linux.
Here is a testapp that demonstrates the problem with the resizing of the FigureCanvasWxAgg in a panel, as seen the panel it self follows the resizingevent but the FigureCanvasWxAgg doesn't follow, this however is not a problem in Windows.
import wx
import matplotlib.figure as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import wx.lib.inspection
class Test(wx.Frame):
def __init__(self):
super(Test, self).__init__(parent=None, id=-1)
self.figure = plt.Figure()
self.panel = wx.Panel(self, 1)
self.figurepanel = FigureCanvas(self.panel, -1, self.figure)
self.axes1 = self.figure.add_subplot(111)
frame_box = wx.BoxSizer(wx.VERTICAL)
frame_box.AddStretchSpacer(prop=1)
frame_box.Add(self.panel, flag=wx.EXPAND, proportion=2)
frame_box.AddStretchSpacer(prop=1)
main_box = wx.BoxSizer(wx.HORIZONTAL)
main_box.AddStretchSpacer(prop=1)
main_box.Add(frame_box, flag=wx.EXPAND, proportion=1)
main_box.AddStretchSpacer(prop=1)
self.SetSizer(main_box)
self.Show()
self.Layout()
def main():
app = wx.App()
Test()
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
if __name__ == '__main__':
main()
What I would be very grateful to have answered is:
How do I resolve this issue of reszing FigureCanvasWxAgg in linux
Is there any difference in the general way of GUI programming with wxPython on Windows and in Linux
There are several issues with the code you posted:
You have horizontal and vertical spacer that expand as needed, which causes the central region to remain the same shape
self.figurepanel is not part of any sizer, which means it will not resize even if its container, self.panel does.
The code below produces a window filled by plot that resizes with the window:
import wx
import matplotlib.figure as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import wx.lib.inspection
class Test(wx.Frame):
def __init__(self):
super(Test, self).__init__(parent=None, id=-1)
self.panel = wx.Panel(self, 1)
self.panel.SetBackgroundColour('RED')
self.figure = plt.Figure()
self.axes1 = self.figure.add_subplot(111)
self.figurepanel = FigureCanvas(self.panel, -1, self.figure)
main_box = wx.BoxSizer(wx.HORIZONTAL)
main_box.Add(self.figurepanel, flag=wx.EXPAND, proportion=1)
self.panel.SetSizer(main_box)
frame_box = wx.BoxSizer(wx.VERTICAL)
frame_box.Add(self.panel, flag=wx.EXPAND, proportion=1)
self.SetSizer(frame_box)
self.Show()
self.Layout()
def main():
app = wx.App()
Test()
wx.lib.inspection.InspectionTool().Show()
app.MainLoop()
if __name__ == '__main__':
main()
There is no need to do any manual resizing yourself, and there is no problem, I think, with resize event propagation. If there is there would be a lot more breakage than what you are seeing.
I think I fixed the problem. The problem is caused by the wx.EVT_SIZE witch seems to be automatic in Windows but not in Linux. So to fix the problem in Linux all you have to do is to bind the the wx.Panel to the wx.EVT_SIZE and then define an apropriate eventhandler that takes care of the resizing.
What did the trick for me was:
#code beneath is a part if the __init__ metod
self.panel = wx.Panel(self, -1)
self.figurepanel = FigureCanvas(self.panel, -1, self.figure)
self.panel.Bind(wx.EVT_SIZE, self.on_size)
....
#the eventhandler for the panel. The method resizes the the figurepanel and the figure.
def on_size(self, event):
pix = self.panel.GetClientSize()
self.figure.set_size_inches(pix[0]/self.figure.get_dpi(),
pix[1]/self.figure.get_dpi())
x,y = self.panel.GetSize()
self.figurepanel.SetSize((x-1, y-1))
self.figurepanel.SetSize((x, y))
self.figurepanel.draw()
event.Skip()

wxpython + matplotlib: autoresizing a matplotlib figure

This python program plots a figure in a wxpython window.
How can I change the program so that:
the figure resizes when I resize the window
the main window cannot be resized smaller than a particular dimension? (say, half the default size of the window)
.
# adapted from:
# http://wiki.wxpython.org/Getting%20Started
# http://www.cs.colorado.edu/~kena/classes/5448/s11/presentations/pearse.pdf
import wx
import pylab as pl
import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
class GUIPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.parent = parent
# create some sizers
sizer = wx.BoxSizer(wx.VERTICAL)
# A button
self.button =wx.Button(self, label="Tada!")
self.Bind(wx.EVT_BUTTON, self.OnClick,self.button)
# put up a figure
self.figure = pl.figure()
self.axes = self.drawplot(self.figure)
self.canvas = FigureCanvas(self, -1, self.figure)
sizer.Add(self.canvas, 0, wx.ALIGN_CENTER|wx.ALL)
sizer.Add(self.button, 0, wx.ALIGN_CENTER|wx.ALL)
self.SetSizerAndFit(sizer)
def log(self, fmt, *args):
print (fmt % args)
def OnClick(self,event):
self.log("button clicked, id#%d\n", event.GetId())
def drawplot(self, fig):
ax = fig.add_subplot(1,1,1)
t = pl.arange(0,1,0.001)
ax.plot(t,t*t)
ax.grid()
return ax
app = wx.App(False)
frame = wx.Frame(None)
panel = GUIPanel(frame)
frame.Fit()
frame.Center()
frame.Show()
app.MainLoop()
1) Modify the way you setup your sizer to this:
sizer.Add(self.canvas, 1, wx.EXPAND | wx.ALL)
Here is method reference. It is also helpful to install and use wxPython Demo. Sizers are covered nicely there. BTW: wx.ALL is useless unless you specify border.
2) And add this to your frame setup after frame.Show():
size = frame.GetSize()
frame.SetMinSize((size[0] / 2, size[1] / 2))

Matplotlib: Label points on mouseover

I have a scatter plot with several thousand points. This post tells me how to label them:
Matplotlib: How to put individual tags for a scatter plot
But that will look like a disaster with so many points. What I would like instead is to have a "tool tip" type label that pops up when you mouseover a point. Is that possible using matplotlib?
Once you get the coords of the point you can show them or any object-linked info in a textctrl in the toolbar. For this you have to instantiate a toolbar (NavigationToolbar2Wx()) in your canvas and add the textcontrol there. This is not as nice as a popup but it does the job.
Here you have an example of customizing your toolbar (only showing the x coordinate in the txtctrl):
#!/usr/bin/env python
#-*- coding: utf-8 -*-
#
"""
jvisor_spectrum_panel (visor_07)
25 julio 2010
"""
#
import wx
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
#
#
class SpectrumPanel(wx.Panel):
def __init__(self, parent, xlabel='m/z', ylabel='Intensity'):
wx.Panel.__init__(self, parent)
#
self.parent = parent
self.xlabel = xlabel
self.ylabel = ylabel
self.SetBackgroundColour("white")
#
self.figure = Figure()
self.canvas = FigureCanvas(self, -1, self.figure)
#
self.add_toolbar()
#
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP| wx.GROW| wx.EXPAND)
sizer.Add(self.toolbar, 0, wx.LEFT)
self.canvas.mpl_connect('motion_notify_event', self.on_motion)
self.SetSizer(sizer)
self.Fit()
self.clean()
#
def add_toolbar(self):
""
self.toolbar = NavigationToolbar2Wx(self.canvas)
mass_txt = wx.StaticText(self.toolbar, label='m/z', pos=(230, 7),
size=(25, 17))
mass_txt.SetBackgroundColour("light gray")
self.mass = wx.TextCtrl(self.toolbar, pos=(260,4), size=(50, 22),
style=wx.TE_READONLY)
#
self.toolbar.SetToolBitmapSize(wx.Size(24, 25))
self.toolbar.SetMinSize((1500, 31))
self.toolbar.Realize()
self.toolbar.Update()
#
def clean(self):
""
self.figure.clear()
self.axes = self.figure.add_subplot(111)
#
def dibuja(self):
"dibuja el canvas"
self.axes.set_xlabel(self.xlabel)
self.axes.set_ylabel(self.ylabel)
self.canvas.draw()
#
def on_motion(self, evt):
if evt.inaxes:
xpos = evt.xdata
self.mass.SetValue(' %0.1f' % (xpos))
if __name__ == '__main__':
""
class TestFrame(wx.Frame):
def __init__(self, *args, **kargs):
wx.Frame.__init__(self, *args, **kargs)
self.panel = SpectrumPanel(self)
self.Fit()
#
app = wx.PySimpleApp()
fr = TestFrame(None)
fr.Show()
app.MainLoop()
And here you can see the new control in the toolbar:

Categories

Resources