Embedding a Matplotlib Figure in a Traits App
Traits, part of theEnthought Tools Suit, provides a great framework for creating GUI Apps without a lot of the normal boilerplate required to connect the UI the rest of the application logic. A brief introduction to Traits can be found here. Although ETS comes with it's own traits-aware plotting framework (Chaco), if you already know matplotlib it is just as easy to embed this instead. The advantages of Chaco (IMHO) are its interactive "tools", an (in development) OpenGL rendering backend and an easy-to-understand codebase. However, matplotlib has more and better documentation and better defaults; it just works. The key to getting TraitsUI and matplotlib to play nice is to use the mpl object-oriented API, rather than pylab / pyplot. This recipe requires the following packages:
- numpy
- wxPython
- matplotlib
Traits > 3.0
TraitsGUI > 3.0
TraitsBackendWX > 3.0
For this example, we will display a function (y, a sine wave) of one variable (x, a numpy ndarray) and one parameter (scale, a float value with bounds). We want to be able to vary the parameter from the UI and see the resulting changes to y in a plot window. Here's what the final result looks like: The TraitsUI "CustomEditor" can be used to display any wxPython window as the editor for the object. You simply pass the CustomEditor a callable which, when called, returns the wxPython window you want to display. In this case, our MakePlot() function returns a wxPanel containing the mpl FigureCanvas and Navigation toolbar. This example exploits a few of Traits' features. We use "dynamic initialisation" to create the Axes and Line2D objects on demand (using the _xxx_default methods). We use Traits "notification", to call update_line(...) whenever the x- or y-data is changed. Further, the y-data is declared as a Property trait which depends on both the 'scale' parameter and the x-data. 'y' is then recalculated on demand, whenever either 'scale' or 'x' change. The 'cached_property' decorator prevents recalculation of y if it's dependancies are not modified.
Finally, there's a bit of wx-magic in the redraw() method to limit the redraw rate by delaying the actual drawing by 50ms. This uses the wx.CallLater class. This prevents excessive redrawing as the slider is dragged, keeping the UI from lagging.Here's the full listing:
1 """
2 A simple demonstration of embedding a matplotlib plot window in
3 a traits-application. The CustomEditor allow any wxPython window
4 to be used as an editor. The demo also illustrates Property traits,
5 which provide nice dependency-handling and dynamic initialisation, using
6 the _xxx_default(...) method.
7 """
8 from enthought.traits.api import HasTraits, Instance, Range,\
9 Array, on_trait_change, Property,\
10 cached_property, Bool
11 from enthought.traits.ui.api import View, Item
12 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
13 from matplotlib.backends.backend_wx import NavigationToolbar2Wx
14 from matplotlib.figure import Figure
15 from matplotlib.axes import Axes
16 from matplotlib.lines import Line2D
17 from enthought.traits.ui.api import CustomEditor
18 import wx
19 import numpy
20 def MakePlot(parent, editor):
21 """
22 Builds the Canvas window for displaying the mpl-figure
23 """
24 fig = editor.object.figure
25 panel = wx.Panel(parent, -1)
26 canvas = FigureCanvasWxAgg(panel, -1, fig)
27 toolbar = NavigationToolbar2Wx(canvas)
28 toolbar.Realize()
29 sizer = wx.BoxSizer(wx.VERTICAL)
30 sizer.Add(canvas,1,wx.EXPAND|wx.ALL,1)
31 sizer.Add(toolbar,0,wx.EXPAND|wx.ALL,1)
32 panel.SetSizer(sizer)
33 return panel
34 class PlotModel(HasTraits):
35 """A Model for displaying a matplotlib figure"""
36 #we need instances of a Figure, a Axes and a Line2D
37 figure = Instance(Figure, ())
38 axes = Instance(Axes)
39 line = Instance(Line2D)
40 _draw_pending = Bool(False) #a flag to throttle the redraw rate
41 #a variable paremeter
42 scale = Range(0.1,10.0)
43 #an independent variable
44 x = Array(value=numpy.linspace(-5,5,512))
45 #a dependent variable
46 y = Property(Array, depends_on=['scale','x'])
47 traits_view = View(
48 Item('figure',
49 editor=CustomEditor(MakePlot),
50 resizable=True),
51 Item('scale'),
52 resizable=True
53 )
54 def _axes_default(self):
55 return self.figure.add_subplot(111)
56 def _line_default(self):
57 return self.axes.plot(self.x, self.y)[0]
58 @cached_property
59 def _get_y(self):
60 return numpy.sin(self.scale * self.x)
61 @on_trait_change("x, y")
62 def update_line(self, obj, name, val):
63 attr = {'x': "set_xdata", 'y': "set_ydata"}[name]
64 getattr(self.line, attr)(val)
65 self.redraw()
66 def redraw(self):
67 if self._draw_pending:
68 return
69 canvas = self.figure.canvas
70 if canvas is None:
71 return
72 def _draw():
73 canvas.draw()
74 self._draw_pending = False
75 wx.CallLater(50, _draw).Start()
76 self._draw_pending = True
77 if __name__=="__main__":
78 model = PlotModel(scale=2.0)
79 model.configure_traits()