#!/usr/bin/env python
"""A wxPython widget to dislay a simple matplotlib graph.
This could be made *much* more complicated.
TODO:
Allow series colour change
Allow line, points, etc, series
Allow changing of number of points in series (how?)
Allow more than one series (how?)
Allow setting of axis ticks/subticks
etc, etc
The basic idea is to minimise the number of args passed to the constructor
and add methods to control the graph look&feel.
"""
import wx
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.path import Path
import matplotlib.patches as patches
import pylab
class MplWidget(wx.Panel):
"""Class to display a matplotlib graph on a wxPython panel.
Note that the dataset length must be set initially AND NOT CHANGED
THEREAFTER. Whether this is fixed in stone or merely due to my
imperfect understanding of wxPython ...
"""
def __init__(self, parent, toolbar=False, num_points=100,
x_range=(-1,1), y_range=(-1,1), **kwargs):
"""Create an MplWidget instance.
parent the owning object reference
toolbar if True attach the matplotlib toolbar to graph
num_points number of points in the dataset
x_range tuple (min, max) of graph X values
y_range tuple (min, max) of graph Y values
kwargs panel-specific keyword params
We set the data ranges once instead of letting each update automatically
set them - this can lead to strangeness in axis labelling. Consider
adding a self.setRanges() function if you need to change the X/Y ranges
for an update.
"""
wx.Panel.__init__(self, parent, **kwargs)
# create a static vertical boxsizer, a nice enclosing line
self.sbsizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, ''),
orient=wx.VERTICAL)
# usual Matplotlib functions
self.figure = Figure(figsize=(2,2))
self.axes = self.figure.add_subplot(111)
self.patch = None
# define the 2D dataset for number of points and containing None
# it appears we can't change a dataset *size* dynamically!?
self.x_data = [None] * num_points
self.y_data = [None] * num_points
self.y_error = [None] * num_points
self.plot_data = self.axes.plot(self.x_data, self.y_data,
color='red', lw=3)[0]
# graph title and labels - set to nondescript values
# user can change these
pylab.setp(self.axes, title='')
pylab.setp(self.axes, xlabel='')
pylab.setp(self.axes, ylabel='')
# set X and Y limits
# these could be changed, but we don't allow it in this version
(x_min, x_max) = x_range
self.axes.set_xbound(lower=x_min, upper=x_max)
(y_min, y_max) = y_range
self.axes.set_ybound(lower=y_min, upper=y_max)
# initialize the FigureCanvas, mapping the figure to the WxAgg backend
self.canvas = FigureCanvas(self, wx.ID_ANY, self.figure)
# add the figure canvas
self.sbsizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.EXPAND)
# if the user wants a toolbar
if toolbar:
# instantiate the Navigation Toolbar
self.toolbar = NavigationToolbar2Wx(self.canvas)
# needed to support Windows systems
self.toolbar.Realize()
# add it to the sub-box sizer
self.sbsizer.Add(self.toolbar, 0, wx.LEFT)
# explicitly show the toolbar
self.toolbar.Show()
self.SetSizer(self.sbsizer)
def setData(self, x_data, y_data, e_data):
"""Update the widget X, Y and errorbar data."""
# change graph data
self.x_data = x_data
self.y_data = y_data
self.plot_data.set_data(np.array(x_data), np.array(y_data))
# remove any previous errorbar patch
if self.patch:
self.axes.patches.remove(self.patch)
# calculate new errorbar info, patch it in
verts = []
codes = []
for (x, y, e) in zip(x_data, y_data, e_data):
verts.append((x, y+e))
codes.append(Path.MOVETO)
verts.append((x, y-e))
codes.append(Path.LINETO)
barpath = Path(verts, codes)
self.patch = patches.PathPatch(barpath, facecolor='grey', alpha=0.5)
self.axes.add_patch(self.patch)
def setTitle(self, title):
"""Set a new graph title."""
pylab.setp(self.axes, title=title)
def setAxisLabels(self, x_label, y_label=None):
"""Set new graph axis labels."""
pylab.setp(self.axes, xlabel=x_label, ylabel=y_label)
def setGrid(self, state):
"""Fiddle with the graph grid.
state if True show the grid
"""
self.axes.grid(state)
def setLinewidth(self, linewidth=1):
"""Set line series linewidth."""
pylab.setp(self.plot_data, linewidth=linewidth)
def Refresh(self):
"""Draw the series plot data specified.
We don't do a refresh automatically after each set*() change.
That could lead to many unnecessary redraws and flashing.
Let the user control do:
possibly many updates, then
one refresh
"""
self.canvas.draw()
if __name__ == '__main__':
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, title='Errorbar Test')
self.SetMinSize((550,450))
self.panel = wx.Panel(self, wx.ID_ANY)
topSizer = wx.BoxSizer(wx.VERTICAL)
self.mpl = MplWidget(self.panel, num_points=5,
x_range=(0,4), y_range=(0,1))
self.mpl.setTitle('Test graph')
self.mpl.setAxisLabels('X-axis', 'Y-axis')
self.mpl.setGrid(True)
topSizer.Add(self.mpl, 1, wx.ALL|wx.EXPAND)
self.btn = wx.Button(self.panel, -1, 'Another')
topSizer.Add(self.btn, 0, wx.ALIGN_RIGHT)
self.panel.SetSizer(topSizer)
topSizer.Fit(self)
self.linewidth = 0
self.btn.Bind(wx.EVT_BUTTON, self.onAnother)
def onAnother(self, event=None):
x = np.arange(0.0, 5, 1.0)
y = np.exp(-x) + np.random.random(x.size) / 5
e = np.random.random(x.size) / 5
self.mpl.setData(x, y, e)
self.linewidth += 1
if self.linewidth > 5:
self.linewidth = 1
self.mpl.setTitle('Test graph - linewidth %d' % self.linewidth)
self.mpl.setLinewidth(self.linewidth)
self.mpl.Refresh()
app = wx.PySimpleApp()
frame = MyForm().Show()
app.MainLoop()