Sunday, August 28, 2016

Embedding MatPlotLib figure in wxPython GUI

Introduction


wxPython comes with a simple plotting library called PyPlot which is an improvement over wxPlotCanvas, which is now deprecated. PyPlot features include zooming, legends and a grid. Possible graphs: scatter, line, and bar graphs.

So why do we worry to embed matplotlib if wxPython has a plotting by defualt? The simple answer is because of the rich features available in matplotlib library.

I will demostrate a minimal approach to embed MatPlotLib figure into a GUI based on wxPython classic (for Python 2.7.12). This article could also be used for Python 3 on wxPython Phoenix with little modifications. The final result is shown below:-



MatPlotLib figure is a GUI with little controls/widgets, so python programmers often embed the figure into a richer GUI libarary such as wxPython, TkInter, PyQt/PySide etc in order to obtain the most advantage of a GUI application. In this article, am going to use the wxPython GUI libarary.


The Problem


First of all, if you go to the MatPlotLib example webpage (under user_interfaces Examples), you will find many good examples on embedding MatPlotLib figure in wxPython GUI. But those examples are quite complex and difficult for a beginner to understand the work flow.

The Solution


First, lets make the GUI's Frame in wxPython. The code below comprises the simplest wxPython application ever concieved. It creates our "Frame", (also referred to as a window) that will contain all of the widgets and tools that we will define later in the program.
import wx

class MainFrame(wx.Frame):
    """docstring for MainFrame"""
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title=">>>> by www.UmarYusuf.com", size=(800, 580))

app = wx.App()
frame = MainFrame(None).Show()
app.MainLoop()



We are going to define two classes, the first class named "MainFrame" has been defined above and it is going to be the main GUI definition. The second class will be named "MatplotPanel" and it is the panel where the matplotlib figure will be displayed.

Now that we've created our frame as seen above, we need to customize it by adding some control/widgets! There are a plethora of control/widgets that can be added to the frame. For simplicity's sake I am going to emphasize one (SplitterWindow) that is commonly used in many wxPython GUI applications.

Lets add a SplitterWindow panels and divide/split it horizontally. Our matplotlib figure will be displayed on the top SplitterWindow while some controls/widgets will be displayed at the bottom SplitterWindow.


import wx

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 = wx.Panel(self.split_win, style=wx.SUNKEN_BORDER)
        self.bottom_split = wx.Panel(self.split_win, style=wx.SUNKEN_BORDER)
        self.split_win.SplitHorizontally(self.top_split, self.bottom_split, 480)


app = wx.App()
frame = MainFrame(None).Show()
app.MainLoop()





Lets add StaticText and Buttons to the bottom_split window. The "StaticText" will be added to show that such is possible, then the "Buttons" will be added to plot graph figure from matplotlib onto the top_split window.

I will add five (5) buttons and bind them to five (5) diffrent plot functions. So when you press button1 or button2 or button3 etc, it will call plot1 or plot2 or plot3 etc functions respectively and then display its graph on the top_split window.

import wx

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 = wx.Panel(self.split_win, style=wx.SUNKEN_BORDER)
        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

app = wx.App()
frame = MainFrame(None).Show()
app.MainLoop()

Now, lets create the second class "MatplotPanel" that will hold the matplotlib figure. In order for the two classes (MatplotPanel and MainFrame) to communicate with each other, we have to edit "self.top_split" in the MainFrame class to reference the MatplotPanel instead of a generic panel.

So, this line of code in the MainFrame class
self.top_split = wx.Panel(self.split_win, style=wx.SUNKEN_BORDER)

will change to
self.top_split = MatplotPanel(self.split_win)


To use matplotlib, we first import its packages. Them we can define the MatplotPanel class to display a very simple plot using the matplotlib "plot()" function.

import matplotlib
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas

class MatplotPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent,-1,size=(50,50))

        self.figure = Figure()
        self.axes = self.figure.add_subplot(111)

        t = [ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
        s = [0.0, 1.0, 0.0, 1.0, 0.0, 2.0, 1.0, 2.0, 1.0, 0.0]

        self.axes.plot(t, s)
        self.canvas = FigureCanvas(self, -1, self.figure)

The next task is to define the five (5) functions we defined above within the MainFrame class. They are going to be some common plot figures in matplotlib. To make things easy, we will declare two variables (a and b) to be used within the functions. These variables can be declared within MainFrame class (self.a and self.b) or outside the MainFrame class (a and b). Am going to use the later (declared within MainFrame class) in this article.

# plotting variables declared outside MainFrame class
# a = [ 0.25, 0.97, 0.59, 0.84, 0.93]
# b = [ 0.52, 0.83, 0.98, 0.28, 0.31, 0.013, 0.29, 0.49, 0.85, 0.80]

class MainFrame(wx.Frame):
    """docstring for MainFrame"""
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title=">>>> by www.UmarYusuf.com", size=(800, 580))

  .
  .
  .

# plotting variables declared within MainFrame class
        self.a = [ 0.25, 0.97, 0.59, 0.84, 0.93, 0.83, 0.98, 0.28, 0.31, 0.67]
        self.b = [ 0.52, 0.83, 0.98, 0.28, 0.31, 0.03, 0.29, 0.49, 0.85, 0.80]

  .
  .
  .


    def plot1(self, event):
     self.fig1 = Figure()

        self.ax1f1 = self.fig1.add_subplot(111)
        self.ax1f1.plot(self.a)

        self.ax1f1.set_title("Simple Plot by Button One")
        self.canvas = FigureCanvas(self, -1, self.fig1)



    def plot2(self, event):
     self.fig2 = Figure()

        self.ax1f2 = self.fig2.add_subplot(121)
        self.ax1f2.plot(self.a)
        self.ax1f2.set_title("First Plot by Button Two")

        self.ax2f2 = self.fig2.add_subplot(122)
        self.ax2f2.plot(self.b)
        self.ax2f2.set_title("Second Plot by Button Two")

        self.canvas = FigureCanvas(self, -1, self.fig2)



    def plot3(self, event):
     # Pcolormesh for a 10x10 matrix/array
        import numpy as np

        matrix = np.random.rand(10, 10)

        self.fig3 = Figure()
        self.ax1f3 = self.fig3.add_subplot(111)
        self.ax1f3.pcolormesh(matrix)
        self.ax1f3.set_title("Pcolormesh Plot by Button Three")

        self.canvas = FigureCanvas(self, -1, self.fig3)



    def plot4(self, event):
     self.fig4 = Figure()

        self.ax1f4 = self.fig4.add_subplot(111)
        self.ax1f4.bar(self.a, self.b, 0.05)

        self.ax1f4.set_title("Bar Plot by Button Four")
        self.canvas = FigureCanvas(self, -1, self.fig4)



    def plot5(self, event):
     self.fig5 = Figure()

        self.ax1f5 = self.fig5.add_subplot(111)
        self.ax1f5.scatter(self.a, self.b, 300)
        
        self.ax1f5.set_title("Scatter Plot by Button Five")
        self.canvas = FigureCanvas(self, -1, self.fig5)


That is it. The completed source code can be downloaded here from github.

If you have any question or comment, be free to fill the comment box below.

Thanks for reading.

No comments:

Post a Comment