Search code examples
pythonmatplotlibwxpythonwxwidgets

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.

enter image description here

After closing the figure wx app is visible.

enter image description here

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.


Solution

  • 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()
    

    enter image description here

    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.