Integrating matplotlib with wxPython Dialogs

Matplotlib is a plotting library for Python and is a great tool to visualize static, animated, and interactive plots. This chapter provides a quick overview of how to use matplotlib within the graphical dialogs created by wxPython.

Note

Narda provides links to offers from third parties in this documentation. The providers of these pages are exclusively responsible for the contents and in particular any damages that may arise from the use or non-use of the information provided in this way. Narda only checks the pages at the time the link is created. All subsequent changes are the responsibility of the provider.

Step 1) Import Matplotlib and Configure Backend for rendering

Matlplotlib provides various plotting features. A detailed user guide for this library to draw figures with axes, lines, legends, labels, plot types, etc, are given in the tutorial below:

To embed the matplotlib features on the wxPython interface, the WXAgg backend needs to be configured. The WXAgg backend refers to:

  • The wxPython interface for graphical user interface creation

  • The Agg renderer to provide high quality raster image rendering

Therefore, the WXAgg backend provides Agg rendering to the wxWidgets canvas in a wxPython interface.

Several backend and renderer combinations are supported by matplotlib, therefore, wxAgg needs to be specified while scripting. To configure the backend, the matplotlib package is imported and a .use() function is used to specify the backend as follows:

1
2
import matplotlib as mpl
mpl.use('WXAgg')  # switch backend (case insensitive)

Once the backend is configured, the following classes are imported from the matplotlib library:

  1. class Figure: It is the top level container for all the plotting elements. A list of plotting functions offered by this class (plots, subplots, axes, legends, etc.) can be found in the link below:

  1. class FigureCanvasWxAgg: This class is imported from the wxAgg backend. It contains the Figure class and does the event handling for the plotting data to visualize static or dynamic plots in real-time.

    1. The plots/figures can either be completely drawn/redrawn with a .draw() method.

    2. To allow drawing/redrawing only a limited area of a plot (e.g. excluding the axes, legends, labels during real-time measurements for enhanced performance), a .blit() method can be used for buffering.

    More details on the WXAgg backend classes are given in the link below:

    https://matplotlib.org/2.1.2/api/backend_wxagg_api.html

To import the above classes, the following lines of code are added:

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

Step 2) Event handling for wxWidget (wxPanel)

To draw a plot inside the wxDialog, event handling needs to be done for the relevant wx container i.e. wxPanel. For this purpose, a class is derived from the wxPanel and a canvas is added to it by instantiating the matplotlib backend class FigureCanvasWxAgg (imported as FigureCanvas) inside this derived class.

The following piece of code from Example 4 shows the TracePlotCanvasPnl class derived from wxPanel for event handling which makes use of the matplotlib features combined with the wxAgg backend.

  1. The class Figure is instantiated here to allow the use of plotting functions inside the wxPanel i.e. the lines, the axes, the legends etc.

  2. The class FigureCanvas is instantiated to place the figure inside the wxpanel.

  3. The axes are instantiated to use matplotlib axes features.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class TracePlotCanvasPnl(wx.Panel):
    """wxWidget plot panel based on matplotlib plot"""

    def __init__(self, parent, tag: str = ""):
        """ wxWidget plot panel based on matplotlib plot

        :param parent: The parent widget.
        :type parent: wx.Widget
        :param tag: Optional string tag.
        :type tag: str
        """
        super(TracePlotCanvasPnl, self).__init__(parent, size=(450, 360))
        self.tag = tag
        self.figure = Figure()
        self.axes = self.figure.add_subplot(111)
        self.canvas = FigureCanvas(self, -1, self.figure)
        self.canvas.SetMinSize(wx.Size(10, 10))
        self.x_values = []
        self.lines = {}
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.canvas, proportion=1, flag=wx.ALL | wx.GROW)
        self.SetSizer(self.sizer)
        self.Fit()
        self.trace_colors = {
            "": "#000000",
            "UNKNOWN": "#000000",
            "RMS": "#00C000",
            "PPk": "#FF0000",
            "MPk": "#0000FF",
            "Avg": "#0000FF",
            "Smp": "#0000FF",
            "MnR": "#003955",
            "AvR": "#FF00FF",
            "MxR": "#553900",
            "MxP": "#AA0000",
            "MnP": "#0000AA",
            "MxA": "#0000AA",
            "MxS": "#0000AA"
        }

Several functions can be defined in this event handling class as per the user requirements.

The function update_settings() makes use of matplotlib features and defines the axes specifications for the figure i.e. the labels, bounds, titles, grids etc.

To plot the data points as lines or as markers, the .plot() method is used. A list of plotting parameters with styling options can be found in the link below:

It is important to note here that the .axes.legend() is called after the .plot() method to ensure that the legends are not overridden by the plots.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def update_settings(self, spectrum_settings: SpectrumSettings, title:str = ""):
    """

    :param spectrum_settings: Measurement settings of the Spectrum View.
    :type spectrum_settings: SpectrumSettings
    :param title: Title of the plot panel.
    :type title: str
    """
    self.clear_memory()

    self.axes.set_ylabel("Power/{}".format(spectrum_settings.unit))
    self.axes.set_xlabel("Frequency/MHz")
    self.axes.set_title(title)
    self.axes.set_autoscale_on(False)
    self.axes.set_autoscalex_on(False)
    self.axes.set_autoscaley_on(False)
    self.axes.grid(True)
    self.axes.set_xbound(spectrum_settings.fstart / 1e6, spectrum_settings.fstop / 1e6)
    self.axes.set_ybound(spectrum_settings.lmin, spectrum_settings.lmax)
    self.figure.subplots_adjust(left=0.2, right=0.9, bottom=0.2, top=0.9)

    self.x_values = spectrum_settings.get_flist(1e6)
    # self.ty = [float(self.user_selected_value)] * spectrum_settings.bin_count
    # self.axes.plot(self.x_values, self.ty)

    for trace in spectrum_settings.trace_list:
        trace_name = str(trace)
        y_values = [0.0]*spectrum_settings.bin_count
        self.lines[str(trace)] = self.axes.plot(self.x_values, y_values,    color=self.trace_colors.get(trace_name, "#000000"),
                                                label=trace_name)
    self.axes.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left',
                     ncol=4, mode="expand", borderaxespad=0.)

Once the plot settings have been defined, the y-axis values are updated for each trace of the data and a canvas is redrawn for the new values using .draw() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def update_y_values(self, trace: str, y_values: list):
    """ Updates a trace with new y data.

    :param trace: Trace name
    :type trace: str
    :param y_values: List of level values.
    :type y_values: list of float
    """
    line = self.lines.get(trace)
    if not line:
        return
    line[0].set_ydata(y_values)

def update_plot(self):
    self.canvas.draw()

The sample integration of matplotlib with wxPython Dialogs can be found in the event handling script template plotdialogs.py of the Example 4: Writing Your Own Plot-Based Dialog Script of the Scripting Tutorial.