source: trunk/wxmpl.py @ 17

Last change on this file since 17 was 17, checked in by vondreel, 13 years ago
File size: 60.2 KB
Line 
1# Name: wxmpl
2# Purpose: painless matplotlib embedding for wxPython
3# Author: Ken McIvor <mcivor@iit.edu>
4#
5# Copyright 2005-2006 Illinois Institute of Technology
6#
7# See the file "LICENSE" for information on usage and redistribution
8# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
9
10"""
11Embedding matplotlib in wxPython applications is straightforward, but the
12default plotting widget lacks the capabilities necessary for interactive use.
13WxMpl (wxPython+matplotlib) is a library of components that provide these
14missing features in the form of a better matplolib FigureCanvas.
15"""
16
17
18import wx
19import sys
20import os.path
21import weakref
22
23import matplotlib
24matplotlib.use('WXAgg')
25import matplotlib.numerix as Numerix
26from matplotlib.axes import PolarAxes, _process_plot_var_args
27from matplotlib.backend_bases import FigureCanvasBase
28from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
29from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
30from matplotlib.figure import Figure
31from matplotlib.font_manager import FontProperties
32from matplotlib.transforms import Bbox, Point, Value
33from matplotlib.transforms import bound_vertices, inverse_transform_bbox
34
35__version__ = '1.2.8'
36
37__all__ = ['PlotPanel', 'PlotFrame', 'PlotApp', 'StripCharter', 'Channel',
38    'FigurePrinter', 'EVT_POINT', 'EVT_SELECTION']
39
40# If you want to use something other than `lpr' to print under linux you may
41# specify that command here.
42LINUX_PRINTING_COMMAND = 'lpr'
43
44# Work around some problems with the pre-0.84 WXAgg backend
45BROKEN_WXAGG_BACKEND = matplotlib.__version__ < '0.84'
46
47
48#
49# Utility functions and classes
50#
51
52def is_polar(axes):
53    """
54    Returns a boolean indicating if C{axes} is a polar axes.
55    """
56    return isinstance(axes, PolarAxes)
57
58
59def find_axes(canvas, x, y):
60    """
61    Finds the C{Axes} within a matplotlib C{FigureCanvas} contains the canavs
62    coordinates C{(x, y)} and returns that axes and the corresponding data
63    coordinates C{xdata, ydata} as a 3-tuple.
64
65    If no axes contains the specified point a 3-tuple of C{None} is returned.
66    """
67
68    axes = None
69    for a in canvas.get_figure().get_axes():
70        if a.in_axes(x, y):
71            if axes is None:
72                axes = a
73            else:
74                return None, None, None
75
76    if axes is None:
77        return None, None, None
78
79    xdata, ydata = axes.transData.inverse_xy_tup((x, y))
80    return axes, xdata, ydata
81
82
83def get_bbox_lims(bbox):
84    """
85    Returns the boundaries of the X and Y intervals of a C{Bbox}.
86    """
87    return bbox.intervalx().get_bounds(), bbox.intervaly().get_bounds()
88
89
90def find_selected_axes(canvas, x1, y1, x2, y2):
91    """
92    Finds the C{Axes} within a matplotlib C{FigureCanvas} that overlaps with a
93    canvas area from C{(x1, y1)} to C{(x1, y1)}.  That axes and the
94    corresponding X and Y axes ranges are returned as a 3-tuple.
95
96    If no axes overlaps with the specified area, or more than one axes
97    overlaps, a 3-tuple of C{None}s is returned.
98    """
99    axes = None
100    bbox = bound_vertices([(x1, y1), (x2, y2)])
101
102    for a in canvas.get_figure().get_axes():
103        if bbox.overlaps(a.bbox):
104            if axes is None:
105                axes = a
106            else:
107                return None, None, None
108
109    if axes is None:
110        return None, None, None
111
112    xymin, xymax = limit_selection(bbox, axes)
113    xrange, yrange = get_bbox_lims(
114        inverse_transform_bbox(axes.transData, bound_vertices([xymin, xymax])))
115    return axes, xrange, yrange
116
117
118def limit_selection(bbox, axes):
119    """
120    Finds the region of a selection C{bbox} which overlaps with the supplied
121    C{axes} and returns it as the 2-tuple C{((xmin, ymin), (xmax, ymax))}.
122    """
123    bxr, byr = get_bbox_lims(bbox)
124    axr, ayr = get_bbox_lims(axes.bbox)
125
126    xmin = max(bxr[0], axr[0])
127    xmax = min(bxr[1], axr[1])
128    ymin = max(byr[0], ayr[0])
129    ymax = min(byr[1], ayr[1])
130    return (xmin, ymin), (xmax, ymax)
131
132
133def format_coord(axes, xdata, ydata):
134    """
135    A C{None}-safe version of {Axes.format_coord()}.
136    """
137    if xdata is None or ydata is None:
138        return ''
139    return axes.format_coord(xdata, ydata)
140
141
142class AxesLimits:
143    """
144    Alters the X and Y limits of C{Axes} objects while maintaining a history of
145    the changes.
146    """
147    def __init__(self):
148        self.history = weakref.WeakKeyDictionary()
149
150    def _get_history(self, axes):
151        """
152        Returns the history list of X and Y limits associated with C{axes}.
153        """
154        return self.history.setdefault(axes, [])
155
156    def zoomed(self, axes):
157        """
158        Returns a boolean indicating whether C{axes} has had its limits
159        altered.
160        """
161        return not (not self._get_history(axes))
162
163    def set(self, axes, xrange, yrange):
164        """
165        Changes the X and Y limits of C{axes} to C{xrange} and {yrange}
166        respectively.  A boolean indicating whether or not the
167        axes should be redraw is returned, because polar axes cannot have
168        their limits changed sensibly.
169        """
170        if is_polar(axes):
171            return False
172
173        history = self._get_history(axes)
174        if history:
175            oldRange = axes.get_xlim(), axes.get_ylim()
176        else:
177            oldRange = None, None
178
179        history.append(oldRange)
180        axes.set_xlim(xrange)
181        axes.set_ylim(yrange)
182        return True
183
184    def restore(self, axes):
185        """
186        Changes the X and Y limits of C{axes} to their previous values.  A
187        boolean indicating whether or not the axes should be redraw is
188        returned.
189        """
190        hist = self._get_history(axes)
191        if not hist:
192            return False
193        else:
194            xrange, yrange = hist.pop()
195            if xrange is None and yrange is None:
196                axes.autoscale_view()
197            else:
198                axes.set_xlim(xrange)
199                axes.set_ylim(yrange)
200            return True
201
202
203class DestructableViewMixin:
204    """
205    Utility class to break the circular reference between an object and its
206    associated "view".
207    """
208    def destroy(self):
209        """
210        Sets this object's C{view} attribute to C{None}.
211        """
212        self.view = None
213
214
215#
216# Director of the matplotlib canvas
217#
218
219class PlotPanelDirector(DestructableViewMixin):
220    """
221    Encapsulates all of the user-interaction logic required by the
222    C{PlotPanel}, following the Humble Dialog Box pattern proposed by Michael
223    Feathers:
224    U{http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf}
225    """
226
227    # TODO: merge all of the self.view.XYZ.something() methods into
228    #       accessor methods of the PlotPanel (Law of Demeter fixes).
229    # TODO: make `rightClickUnzoom' an option on PlotPanel, PlotFrame, etc
230    # TODO: add a programmatic interface to zooming
231
232    def __init__(self, view, zoom=True, selection=True, rightClickUnzoom=True):
233        """
234        Create a new director for the C{PlotPanel} C{view}.  The keyword
235        arguments C{zoom} and C{selection} have the same meanings as for
236        C{PlotPanel}.
237        """
238        self.view = view
239        self.zoomEnabled = zoom
240        self.selectionEnabled = selection
241        self.rightClickUnzoom = rightClickUnzoom
242        self.limits = AxesLimits()
243        self.leftButtonPoint = None
244
245    def setSelection(self, state):
246        """
247        Enable or disable left-click area selection.
248        """
249        self.selectionEnabled = state
250
251    def setZoomEnabled(self, state):
252        """
253        Enable or disable zooming as a result of left-click area selection.
254        """
255        self.zoomEnabled = state
256
257    def setRightClickUnzoom(self, state):
258        """
259        Enable or disable unzooming as a result of right-clicking.
260        """
261        self.rightClickUnzoom = state
262
263    def canDraw(self):
264        """
265        Returns a boolean indicating whether or not the plot may be redrawn.
266        """
267        return self.leftButtonPoint is None
268
269    def zoomed(self, axes):
270        """
271        Returns a boolean indicating whether or not the plot has been zoomed in
272        as a result of a left-click area selection.
273        """
274        return self.limits.zoomed(axes)
275
276    def keyDown(self, evt):
277        """
278        Handles wxPython key-press events.  These events are currently skipped.
279        """
280        evt.Skip()
281
282    def keyUp(self, evt):
283        """
284        Handles wxPython key-release events.  These events are currently
285        skipped.
286        """
287        evt.Skip()
288
289    def leftButtonDown(self, evt, x, y):
290        """
291        Handles wxPython left-click events.
292        """
293        self.leftButtonPoint = (x, y)
294
295        view = self.view
296        axes, xdata, ydata = find_axes(view, x, y)
297
298        if self.selectionEnabled and not is_polar(axes):
299            view.cursor.setCross()
300            view.crosshairs.clear()
301
302    def leftButtonUp(self, evt, x, y):
303        """
304        Handles wxPython left-click-release events.
305        """
306        if self.leftButtonPoint is None:
307            return
308
309        view = self.view
310        axes, xdata, ydata = find_axes(view, x, y)
311
312        x0, y0 = self.leftButtonPoint
313        self.leftButtonPoint = None
314        view.rubberband.clear()
315
316        if x0 == x:
317            if y0 == y and axes is not None:
318                view.notify_point(axes, x, y)
319                view.crosshairs.set(x, y)
320            return
321        elif y0 == y:
322            return
323
324        xdata = ydata = None
325        axes, xrange, yrange = find_selected_axes(view, x0, y0, x, y)
326
327        if axes is not None:
328            xdata, ydata = axes.transData.inverse_xy_tup((x, y))
329            if self.zoomEnabled:
330                if self.limits.set(axes, xrange, yrange):
331                    self.view.draw()
332            else:
333                bbox = bound_vertices([(x0, y0), (x, y)])
334                (x1, y1), (x2, y2) = limit_selection(bbox, axes)
335                self.view.notify_selection(axes, x1, y1, x2, y2)
336
337        if axes is None:
338            view.cursor.setNormal()
339        elif is_polar(axes):
340            view.cursor.setNormal()
341            view.location.set(format_coord(axes, xdata, ydata))
342        else:
343            view.crosshairs.set(x, y)
344            view.location.set(format_coord(axes, xdata, ydata))
345
346    def rightButtonDown(self, evt, x, y):
347        """
348        Handles wxPython right-click events.  These events are currently
349        skipped.
350        """
351        evt.Skip()
352
353    def rightButtonUp(self, evt, x, y):
354        """
355        Handles wxPython right-click-release events.
356        """
357        view = self.view
358        axes, xdata, ydata = find_axes(view, x, y)
359        if (axes is not None and self.zoomEnabled and self.rightClickUnzoom
360        and self.limits.restore(axes)):
361            view.crosshairs.clear()
362            view.draw()
363            view.crosshairs.set(x, y)
364
365    def mouseMotion(self, evt, x, y):
366        """
367        Handles wxPython mouse motion events, dispatching them based on whether
368        or not a selection is in process and what the cursor is over.
369        """
370        view = self.view
371        axes, xdata, ydata = find_axes(view, x, y)
372
373        if self.leftButtonPoint is not None:
374            self.selectionMouseMotion(evt, x, y, axes, xdata, ydata)
375        else:
376            if axes is None:
377                self.canvasMouseMotion(evt, x, y)
378            elif is_polar(axes):
379                self.polarAxesMouseMotion(evt, x, y, axes, xdata, ydata)
380            else:
381                self.axesMouseMotion(evt, x, y, axes, xdata, ydata)
382
383    def selectionMouseMotion(self, evt, x, y, axes, xdata, ydata):
384        """
385        Handles wxPython mouse motion events that occur during a left-click
386        area selection.
387        """
388        view = self.view
389        x0, y0 = self.leftButtonPoint
390        view.rubberband.set(x0, y0, x, y)
391        if axes is None:
392            view.location.clear()
393        else:
394            view.location.set(format_coord(axes, xdata, ydata))
395
396    def canvasMouseMotion(self, evt, x, y):
397        """
398        Handles wxPython mouse motion events that occur over the canvas.
399        """
400        view = self.view
401        view.cursor.setNormal()
402        view.crosshairs.clear()
403        view.location.clear()
404
405    def axesMouseMotion(self, evt, x, y, axes, xdata, ydata):
406        """
407        Handles wxPython mouse motion events that occur over an axes.
408        """
409        view = self.view
410        view.cursor.setCross()
411        view.crosshairs.set(x, y)
412        view.location.set(format_coord(axes, xdata, ydata))
413
414    def polarAxesMouseMotion(self, evt, x, y, axes, xdata, ydata):
415        """
416        Handles wxPython mouse motion events that occur over a polar axes.
417        """
418        view = self.view
419        view.cursor.setNormal()
420        view.location.set(format_coord(axes, xdata, ydata))
421
422
423#
424# Components used by the PlotPanel
425#
426
427class Painter(DestructableViewMixin):
428    """
429    Painters encapsulate the mechanics of drawing some value in a wxPython
430    window and erasing it.  Subclasses override template methods to process
431    values and draw them.
432
433    @cvar PEN: C{wx.Pen} to use (defaults to C{wx.BLACK_PEN})
434    @cvar BRUSH: C{wx.Brush} to use (defaults to C{wx.TRANSPARENT_BRUSH})
435    @cvar FUNCTION: Logical function to use (defaults to C{wx.COPY})
436    @cvar FONT: C{wx.Font} to use (defaults to C{wx.NORMAL_FONT})
437    @cvar TEXT_FOREGROUND: C{wx.Colour} to use (defaults to C{wx.BLACK})
438    @cvar TEXT_BACKGROUND: C{wx.Colour} to use (defaults to C{wx.WHITE})
439    """
440
441    PEN = wx.BLACK_PEN
442    BRUSH = wx.TRANSPARENT_BRUSH
443    FUNCTION = wx.COPY
444    FONT = wx.NORMAL_FONT
445    TEXT_FOREGROUND = wx.BLACK
446    TEXT_BACKGROUND = wx.WHITE
447
448    def __init__(self, view, enabled=True):
449        """
450        Create a new painter attached to the wxPython window C{view}.  The
451        keyword argument C{enabled} has the same meaning as the argument to the
452        C{setEnabled()} method.
453        """
454        self.view = view
455        self.lastValue = None
456        self.enabled = enabled
457
458    def setEnabled(self, state):
459        """
460        Enable or disable this painter.  Disabled painters do not draw their
461        values and calls to C{set()} have no effect on them.
462        """
463        oldState, self.enabled = self.enabled, state
464        if oldState and not self.enabled:
465            self.clear()
466
467    def set(self, *value):
468        """
469        Update this painter's value and then draw it.  Values may not be
470        C{None}, which is used internally to represent the absence of a current
471        value.
472        """
473        if self.enabled:
474            value = self.formatValue(value)
475            self._paint(value, None)
476
477    def redraw(self, dc=None):
478        """
479        Redraw this painter's current value.
480        """
481        value = self.lastValue
482        self.lastValue = None
483        self._paint(value, dc)
484
485    def clear(self, dc=None):
486        """
487        Clear the painter's current value from the screen and the painter
488        itself.
489        """
490        if self.lastValue is not None:
491            self._paint(None, dc)
492
493    def _paint(self, value, dc):
494        """
495        Draws a previously processed C{value} on this painter's window.
496        """
497        if dc is None:
498            dc = wx.ClientDC(self.view)
499
500        dc.SetPen(self.PEN)
501        dc.SetBrush(self.BRUSH)
502        dc.SetFont(self.FONT)
503        dc.SetTextForeground(self.TEXT_FOREGROUND)
504        dc.SetTextBackground(self.TEXT_BACKGROUND)
505        dc.SetLogicalFunction(self.FUNCTION)
506        dc.BeginDrawing()
507
508        if self.lastValue is not None:
509            self.clearValue(dc, self.lastValue)
510            self.lastValue = None
511
512        if value is not None:
513            self.drawValue(dc, value)
514            self.lastValue = value
515
516        dc.EndDrawing()
517
518    def formatValue(self, value):
519        """
520        Template method that processes the C{value} tuple passed to the
521        C{set()} method, returning the processed version.
522        """
523        return value
524
525    def drawValue(self, dc, value):
526        """
527        Template method that draws a previously processed C{value} using the
528        wxPython device context C{dc}.  This DC has already been configured, so
529        calls to C{BeginDrawing()} and C{EndDrawing()} may not be made.
530        """
531        pass
532
533    def clearValue(self, dc, value):
534        """
535        Template method that clears a previously processed C{value} that was
536        previously drawn, using the wxPython device context C{dc}.  This DC has
537        already been configured, so calls to C{BeginDrawing()} and
538        C{EndDrawing()} may not be made.
539        """
540        pass
541
542
543class LocationPainter(Painter):
544    """
545    Draws a text message containing the current position of the mouse in the
546    lower left corner of the plot.
547    """
548
549    PADDING = 2
550    PEN = wx.WHITE_PEN
551    BRUSH = wx.WHITE_BRUSH
552
553    def formatValue(self, value):
554        """
555        Extracts a string from the 1-tuple C{value}.
556        """
557        return value[0]
558
559    def get_XYWH(self, dc, value):
560        """
561        Returns the upper-left coordinates C{(X, Y)} for the string C{value}
562        its width and height C{(W, H)}.
563        """
564        height = dc.GetSize()[1]
565        w, h = dc.GetTextExtent(value)
566        x = self.PADDING
567        y = int(height - (h + self.PADDING))
568        return x, y, w, h
569
570    def drawValue(self, dc, value):
571        """
572        Draws the string C{value} in the lower left corner of the plot.
573        """
574        x, y, w, h = self.get_XYWH(dc, value)
575        dc.DrawText(value, x, y)
576
577    def clearValue(self, dc, value):
578        """
579        Clears the string C{value} from the lower left corner of the plot by
580        painting a white rectangle over it.
581        """
582        x, y, w, h = self.get_XYWH(dc, value)
583        dc.DrawRectangle(x, y, w, h)
584
585
586class CrosshairPainter(Painter):
587    """
588    Draws crosshairs through the current position of the mouse.
589    """
590
591    PEN = wx.WHITE_PEN
592    FUNCTION = wx.XOR
593
594    def formatValue(self, value):
595        """
596        Converts the C{(X, Y)} mouse coordinates from matplotlib to wxPython.
597        """
598        x, y = value
599        return int(x), int(self.view.get_figure().bbox.height() - y)
600
601    def drawValue(self, dc, value):
602        """
603        Draws crosshairs through the C{(X, Y)} coordinates.
604        """
605        dc.CrossHair(*value)
606
607    def clearValue(self, dc, value):
608        """
609        Clears the crosshairs drawn through the C{(X, Y)} coordinates.
610        """
611        dc.CrossHair(*value)
612
613
614class RubberbandPainter(Painter):
615    """
616    Draws a selection rubberband from one point to another.
617    """
618
619    PEN = wx.WHITE_PEN
620    FUNCTION = wx.XOR
621
622    def formatValue(self, value):
623        """
624        Converts the C{(x1, y1, x2, y2)} mouse coordinates from matplotlib to
625        wxPython.
626        """
627        x1, y1, x2, y2 = value
628        height = self.view.get_figure().bbox.height()
629        y1 = height - y1
630        y2 = height - y2
631        if x2 < x1: x1, x2 = x2, x1
632        if y2 < y1: y1, y2 = y2, y1
633        return [int(z) for z in (x1, y1, x2-x1, y2-y1)]
634
635    def drawValue(self, dc, value):
636        """
637        Draws the selection rubberband around the rectangle
638        C{(x1, y1, x2, y2)}.
639        """
640        dc.DrawRectangle(*value)
641
642    def clearValue(self, dc, value):
643        """
644        Clears the selection rubberband around the rectangle
645        C{(x1, y1, x2, y2)}.
646        """
647        dc.DrawRectangle(*value)
648
649
650class CursorChanger(DestructableViewMixin):
651    """
652    Manages the current cursor of a wxPython window, allowing it to be switched
653    between a normal arrow and a square cross.
654    """
655    def __init__(self, view, enabled=True):
656        """
657        Create a CursorChanger attached to the wxPython window C{view}.  The
658        keyword argument C{enabled} has the same meaning as the argument to the
659        C{setEnabled()} method.
660        """
661        self.view = view
662        self.cursor = wx.CURSOR_DEFAULT
663        self.enabled = enabled
664
665    def setEnabled(self, state):
666        """
667        Enable or disable this cursor changer.  When disabled, the cursor is
668        reset to the normal arrow and calls to the C{set()} methods have no
669        effect.
670        """
671        oldState, self.enabled = self.enabled, state
672        if oldState and not self.enabled and self.cursor != wx.CURSOR_DEFAULT:
673            self.cursor = wx.CURSOR_DEFAULT
674            self.view.SetCursor(wx.STANDARD_CURSOR)
675
676    def setNormal(self):
677        """
678        Change the cursor of the associated window to a normal arrow.
679        """
680        if self.cursor != wx.CURSOR_DEFAULT and self.enabled:
681            self.cursor = wx.CURSOR_DEFAULT
682            self.view.SetCursor(wx.STANDARD_CURSOR)
683
684    def setCross(self):
685        """
686        Change the cursor of the associated window to a square cross.
687        """
688        if self.cursor != wx.CURSOR_CROSS and self.enabled:
689            self.cursor = wx.CURSOR_CROSS
690            self.view.SetCursor(wx.CROSS_CURSOR)
691
692
693#
694# Printing Framework
695#
696
697# TODO: Map print quality settings onto PostScript resolutions automatically.
698#       For now, it's set to something reasonable to work around the fact that
699#       it defaults to `72' rather than `720' under wxPython 2.4.2.4
700wx.PostScriptDC_SetResolution(300)
701
702
703class FigurePrinter(DestructableViewMixin):
704    """
705    Provides a simplified interface to the wxPython printing framework that's
706    designed for printing matplotlib figures.
707    """
708
709    def __init__(self, view, printData=None):
710        """
711        Create a new C{FigurePrinter} associated with the wxPython widget
712        C{view}.  The keyword argument C{printData} supplies a C{wx.PrintData}
713        object containing the default printer settings.
714        """
715        self.view = view
716
717        if printData is None:
718            self.pData = wx.PrintData()
719        else:
720            self.pData = printData
721
722    def getPrintData(self):
723        """
724        Return the current printer settings in their C{wx.PrintData} object.
725        """
726        return self.pData
727
728    def setPrintData(self, printData):
729        """
730        Use the printer settings in C{printData}.
731        """
732        self.pData = printData
733
734    def pageSetup(self):
735        dlg = wx.PrintDialog(self.view)
736        pdData = dlg.GetPrintDialogData()
737        pdData.SetPrintData(self.pData)
738        pdData.SetSetupDialog(True)
739
740        if dlg.ShowModal() == wx.ID_OK:
741            self.pData = pdData.GetPrintData()
742        dlg.Destroy()
743
744    def previewFigure(self, figure, title=None):
745        """
746        Open a "Print Preview" window for the matplotlib chart C{figure}.  The
747        keyword argument C{title} provides the printing framework with a title
748        for the print job.
749        """
750        window = self.view
751        while not isinstance(window, wx.Frame):
752            window = window.GetParent()
753            assert window is not None
754
755        fpo = FigurePrintout(figure, title)
756        fpo4p = FigurePrintout(figure, title)
757        preview = wx.PrintPreview(fpo, fpo4p, self.pData)
758        frame = wx.PreviewFrame(preview, window, 'Print Preview')
759        if self.pData.GetOrientation() == wx.PORTRAIT:
760            frame.SetSize(wx.Size(450, 625))
761        else:
762            frame.SetSize(wx.Size(600, 500))
763        frame.Initialize()
764        frame.Show(True)
765
766    def printFigure(self, figure, title=None):
767        """
768        Open a "Print" dialog to print the matplotlib chart C{figure}.  The
769        keyword argument C{title} provides the printing framework with a title
770        for the print job.
771        """
772        pdData = wx.PrintDialogData()
773        pdData.SetPrintData(self.pData)
774        printer = wx.Printer(pdData)
775        fpo = FigurePrintout(figure, title)
776        if printer.Print(self.view, fpo, True):
777            self.pData = pdData.GetPrintData()
778
779
780class FigurePrintout(wx.Printout):
781    """
782    Render a matplotlib C{Figure} to a page or file using wxPython's printing
783    framework.
784    """
785
786    ASPECT_RECTANGULAR = 1
787    ASPECT_SQUARE = 2
788
789    def __init__(self, figure, title=None, size=None, aspectRatio=None):
790        """
791        Create a printout for the matplotlib chart C{figure}.  The
792        keyword argument C{title} provides the printing framework with a title
793        for the print job.  The keyword argument C{size} specifies how to scale
794        the figure, from 1 to 100 percent.  The keyword argument C{aspectRatio}
795        determines whether the printed figure will be rectangular or square.
796        """
797        self.figure = figure
798
799        figTitle = figure.gca().title.get_text()
800        if not figTitle:
801            figTitle = title or 'Matplotlib Figure'
802
803        if size is None:
804            size = 100
805        elif size < 0 or size > 100:
806            raise ValueError('invalid figure size')
807        self.size = size
808
809        if aspectRatio is None:
810            aspectRatio = self.ASPECT_RECTANGULAR
811        elif (aspectRatio != self.ASPECT_RECTANGULAR
812        and aspectRatio != self.ASPECT_SQUARE):
813            raise ValueError('invalid aspect ratio')
814        self.aspectRatio = aspectRatio
815
816        wx.Printout.__init__(self, figTitle)
817
818    def GetPageInfo(self):
819        """
820        Overrides wx.Printout.GetPageInfo() to provide the printing framework
821        with the number of pages in this print job.
822        """
823        return (0, 1, 1, 1)
824
825    def OnPrintPage(self, pageNumber):
826        """
827        Overrides wx.Printout.OnPrintPage to render the matplotlib figure to
828        a printing device context.
829        """
830        # % of printable area to use
831        imgPercent = max(1, min(100, self.size)) / 100.0
832
833        # ratio of the figure's width to its height
834        if self.aspectRatio == self.ASPECT_RECTANGULAR:
835            aspectRatio = 1.61803399
836        elif self.aspectRatio == self.ASPECT_SQUARE:
837            aspectRatio = 1.0
838        else:
839            raise ValueError('invalid aspect ratio')
840
841        # Device context to draw the page
842        dc = self.GetDC()
843
844        # PPI_P: Pixels Per Inch of the Printer
845        wPPI_P, hPPI_P = [float(x) for x in self.GetPPIPrinter()]
846        PPI_P = (wPPI_P + hPPI_P)/2.0
847
848        # PPI: Pixels Per Inch of the DC
849        if self.IsPreview():
850            wPPI, hPPI = [float(x) for x in self.GetPPIScreen()]
851        else:
852            wPPI, hPPI = wPPI_P, hPPI_P
853        PPI = (wPPI + hPPI)/2.0
854
855        # Pg_Px: Size of the page (pixels)
856        wPg_Px,  hPg_Px  = [float(x) for x in self.GetPageSizePixels()]
857
858        # Dev_Px: Size of the DC (pixels)
859        wDev_Px, hDev_Px = [float(x) for x in self.GetDC().GetSize()]
860
861        # Pg: Size of the page (inches)
862        wPg = wPg_Px / PPI_P
863        hPg = hPg_Px / PPI_P
864
865        # minimum margins (inches)
866        # TODO: make these arguments to __init__()
867        wM = 0.75
868        hM = 0.75
869
870        # Area: printable area within the margins (inches)
871        wArea = wPg - 2*wM
872        hArea = hPg - 2*hM
873
874        # Fig: printing size of the figure
875        # hFig is at a maximum when wFig == wArea
876        max_hFig = wArea / aspectRatio
877        hFig = min(imgPercent * hArea, max_hFig)
878        wFig = aspectRatio * hFig
879
880        # scale factor = device size / page size (equals 1.0 for real printing)
881        S = ((wDev_Px/PPI)/wPg + (hDev_Px/PPI)/hPg)/2.0
882
883        # Fig_S: scaled printing size of the figure (inches)
884        # M_S: scaled minimum margins (inches)
885        wFig_S = S * wFig
886        hFig_S = S * hFig
887        wM_S = S * wM
888        hM_S = S * hM
889
890        # Fig_Dx: scaled printing size of the figure (device pixels)
891        # M_Dx: scaled minimum margins (device pixels)
892        wFig_Dx = int(S * PPI * wFig)
893        hFig_Dx = int(S * PPI * hFig)
894        wM_Dx = int(S * PPI * wM)
895        hM_Dx = int(S * PPI * hM)
896
897        image = self.render_figure_as_image(wFig, hFig, PPI)
898
899        if self.IsPreview():
900            image = image.Scale(wFig_Dx, hFig_Dx)
901        self.GetDC().DrawBitmap(image.ConvertToBitmap(), wM_Dx, hM_Dx, False)
902
903        return True
904
905    def render_figure_as_image(self, wFig, hFig, dpi):
906        """
907        Renders a matplotlib figure using the Agg backend and stores the result
908        in a C{wx.Image}.  The arguments C{wFig} and {hFig} are the width and
909        height of the figure, and C{dpi} is the dots-per-inch to render at.
910        """
911        figure = self.figure
912
913        old_dpi = figure.dpi.get()
914        figure.dpi.set(dpi)
915        old_width = figure.figwidth.get()
916        figure.figwidth.set(wFig)
917        old_height = figure.figheight.get()
918        figure.figheight.set(hFig)
919        old_frameon = figure.frameon
920        figure.frameon = False
921
922        wFig_Px = int(figure.bbox.width())
923        hFig_Px = int(figure.bbox.height())
924
925        agg = RendererAgg(wFig_Px, hFig_Px, Value(dpi))
926        figure.draw(agg)
927
928        figure.dpi.set(old_dpi)
929        figure.figwidth.set(old_width)
930        figure.figheight.set(old_height)
931        figure.frameon = old_frameon
932
933        image = wx.EmptyImage(wFig_Px, hFig_Px)
934        image.SetData(agg.tostring_rgb())
935        return image
936
937
938#
939# wxPython event interface for the PlotPanel and PlotFrame
940#
941
942EVT_POINT_ID = wx.NewId()
943
944
945def EVT_POINT(win, id, func):
946    """
947    Register to receive wxPython C{PointEvent}s from a C{PlotPanel} or
948    C{PlotFrame}.
949    """
950    win.Connect(id, -1, EVT_POINT_ID, func)
951
952
953class PointEvent(wx.PyCommandEvent):
954    """
955    wxPython event emitted when a left-click-release occurs in a matplotlib
956    axes of a window without an area selection.
957
958    @cvar axes: matplotlib C{Axes} which was left-clicked
959    @cvar x: matplotlib X coordinate
960    @cvar y: matplotlib Y coordinate
961    @cvar xdata: axes X coordinate
962    @cvar ydata: axes Y coordinate
963    """
964    def __init__(self, id, axes, x, y):
965        """
966        Create a new C{PointEvent} for the matplotlib coordinates C{(x, y)} of
967        an C{axes}.
968        """
969        wx.PyCommandEvent.__init__(self, EVT_POINT_ID, id)
970        self.axes = axes
971        self.x = x
972        self.y = y
973        self.xdata, self.ydata = axes.transData.inverse_xy_tup((x, y))
974
975    def Clone(self):
976        return PointEvent(self.GetId(), self.axes, self.x, self.y)
977
978
979EVT_SELECTION_ID = wx.NewId()
980
981
982def EVT_SELECTION(win, id, func):
983    """
984    Register to receive wxPython C{SelectionEvent}s from a C{PlotPanel} or
985    C{PlotFrame}.
986    """
987    win.Connect(id, -1, EVT_SELECTION_ID, func)
988
989
990class SelectionEvent(wx.PyCommandEvent):
991    """
992    wxPython event emitted when an area selection occurs in a matplotlib axes
993    of a window for which zooming has been disabled.  The selection is
994    described by a rectangle from C{(x1, y1)} to C{(x2, y2)}, of which only
995    one point is required to be inside the axes.
996
997    @cvar axes: matplotlib C{Axes} which was left-clicked
998    @cvar x1: matplotlib x1 coordinate
999    @cvar y1: matplotlib y1 coordinate
1000    @cvar x2: matplotlib x2 coordinate
1001    @cvar y2: matplotlib y2 coordinate
1002    @cvar x1data: axes x1 coordinate
1003    @cvar y1data: axes y1 coordinate
1004    @cvar x2data: axes x2 coordinate
1005    @cvar y2data: axes y2 coordinate
1006    """
1007    def __init__(self, id, axes, x1, y1, x2, y2):
1008        """
1009        Create a new C{SelectionEvent} for the area described by the rectangle
1010        from C{(x1, y1)} to C{(x2, y2)} in an C{axes}.
1011        """
1012        wx.PyCommandEvent.__init__(self, EVT_SELECTION_ID, id)
1013        self.axes = axes
1014        self.x1 = x1
1015        self.y1 = y1
1016        self.x2 = x2
1017        self.y2 = y2
1018        self.x1data, self.y1data = axes.transData.inverse_xy_tup((x1, y1))
1019        self.x2data, self.y2data = axes.transData.inverse_xy_tup((x2, y2))
1020
1021    def Clone(self):
1022        return SelectionEvent(self.GetId(), self.axes, self.x1, self.y1,
1023            self.x2, self.y2)
1024
1025
1026#
1027# Matplotlib canvas in a wxPython window
1028#
1029
1030class PlotPanel(FigureCanvasWxAgg):
1031    """
1032    A matplotlib canvas suitable for embedding in wxPython applications.
1033    """
1034    def __init__(self, parent, id, size=(6.0, 3.70), dpi=96, cursor=True,
1035     location=True, crosshairs=True, selection=True, zoom=True):
1036        """
1037        Creates a new PlotPanel window that is the child of the wxPython window
1038        C{parent} with the wxPython identifier C{id}.
1039
1040        The keyword arguments C{size} and {dpi} are used to create the
1041        matplotlib C{Figure} associated with this canvas.  C{size} is the
1042        desired width and height of the figure, in inches, as the 2-tuple
1043        C{(width, height)}.  C{dpi} is the dots-per-inch of the figure.
1044
1045        The keyword arguments C{cursor}, C{location}, C{crosshairs},
1046        C{selection}, and C{zoom} enable or disable various user interaction
1047        features that are descibed in their associated C{set()} methods.
1048        """
1049        FigureCanvasWxAgg.__init__(self, parent, id, Figure(size, dpi))
1050
1051        self.insideOnPaint = False
1052        self.cursor = CursorChanger(self, cursor)
1053        self.location = LocationPainter(self, location)
1054        self.crosshairs = CrosshairPainter(self, crosshairs)
1055        self.rubberband = RubberbandPainter(self, selection)
1056        self.director = PlotPanelDirector(self, zoom, selection)
1057
1058        self.figure.set_edgecolor('black')
1059        self.figure.set_facecolor('white')
1060        self.SetBackgroundColour(wx.WHITE)
1061
1062        # find the toplevel parent window and register an activation event
1063        # handler that is keyed to the id of this PlotPanel
1064        topwin = self._get_toplevel_parent()
1065        topwin.Connect(-1, self.GetId(), wx.wxEVT_ACTIVATE, self.OnActivate)
1066
1067        wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
1068        wx.EVT_WINDOW_DESTROY(self, self.OnDestroy)
1069
1070    def _get_toplevel_parent(self):
1071        """
1072        Returns the first toplevel parent of this window.
1073        """
1074        topwin = self.GetParent()
1075        while not isinstance(topwin, (wx.Frame, wx.Dialog)):
1076            topwin = topwin.GetParent()
1077        return topwin       
1078
1079    def OnActivate(self, evt):
1080        """
1081        Handles the wxPython window activation event.
1082        """
1083        if not evt.GetActive():
1084            self.cursor.setNormal()
1085            self.location.clear()
1086            self.crosshairs.clear()
1087            self.rubberband.clear()
1088        evt.Skip()
1089
1090    def OnEraseBackground(self, evt):
1091        """
1092        Overrides the wxPython backround repainting event to reduce flicker.
1093        """
1094        pass
1095
1096    def OnDestroy(self, evt):
1097        """
1098        Handles the wxPython window destruction event.
1099        """
1100        if self.GetId() == evt.GetEventObject().GetId():
1101            objects = [self.cursor, self.location, self.rubberband,
1102                self.crosshairs, self.director]
1103            for obj in objects:
1104                obj.destroy()
1105
1106            # unregister the activation event handler for this PlotPanel
1107            topwin = self._get_toplevel_parent()
1108            topwin.Disconnect(-1, self.GetId(), wx.wxEVT_ACTIVATE)
1109
1110    def _onPaint(self, evt):
1111        """
1112        Overrides the C{FigureCanvasWxAgg} paint event to redraw the
1113        crosshairs, etc.
1114        """
1115        if not isinstance(self, FigureCanvasWxAgg):
1116            return
1117
1118        self.insideOnPaint = True
1119        FigureCanvasWxAgg._onPaint(self, evt)
1120        self.insideOnPaint = False
1121
1122        dc = wx.PaintDC(self)
1123        self.location.redraw(dc)
1124        self.crosshairs.redraw(dc)
1125        self.rubberband.redraw(dc)
1126
1127    def get_figure(self):
1128        """
1129        Returns the figure associated with this canvas.
1130        """
1131        return self.figure
1132
1133    def set_cursor(self, state):
1134        """
1135        Enable or disable the changing mouse cursor.  When enabled, the cursor
1136        changes from the normal arrow to a square cross when the mouse enters a
1137        matplotlib axes on this canvas.
1138        """
1139        self.cursor.setEnabled(state)
1140
1141    def set_location(self, state):
1142        """
1143        Enable or disable the display of the matplotlib axes coordinates of the
1144        mouse in the lower left corner of the canvas.
1145        """
1146        self.location.setEnabled(state)
1147
1148    def set_crosshairs(self, state):
1149        """
1150        Enable or disable drawing crosshairs through the mouse cursor when it
1151        is inside a matplotlib axes.
1152        """
1153        self.crosshairs.setEnabled(state)
1154
1155    def set_selection(self, state):
1156        """
1157        Enable or disable area selections, where user selects a rectangular
1158        area of the canvas by left-clicking and dragging the mouse.
1159        """
1160        self.rubberband.setEnabled(state)
1161        self.director.setSelection(state)
1162
1163    def set_zoom(self, state):
1164        """
1165        Enable or disable zooming in when the user makes an area selection and
1166        zooming out again when the user right-clicks.
1167        """
1168        self.director.setZoomEnabled(state)
1169
1170    def zoomed(self, axes):
1171        """
1172        Returns a boolean indicating whether or not the C{axes} is zoomed in.
1173        """
1174        return self.director.zoomed(axes)
1175
1176    def draw(self, repaint=True):
1177        """
1178        Draw the associated C{Figure} onto the screen.
1179        """
1180        if (not self.director.canDraw()
1181        or  not isinstance(self, FigureCanvasWxAgg)):
1182            return
1183
1184        # Before matplotlib 0.84, FigureCanvasWxAgg.draw() always called
1185        # gui_repaint(), which redrew the plot using a ClientDC.  This is
1186        # a workaround that lets us repaint the plot decorations in a sane
1187        # manner.
1188
1189        doRepaint = repaint and not self.insideOnPaint
1190        if BROKEN_WXAGG_BACKEND:
1191            FigureCanvasAgg.draw(self)
1192            s = self.tostring_rgb()
1193            w = int(self.renderer.width)
1194            h = int(self.renderer.height)
1195            image = wx.EmptyImage(w, h)
1196            image.SetData(s)
1197            self.bitmap = image.ConvertToBitmap()
1198
1199            # Don't repaint when called by _onPaint()
1200            if doRepaint:
1201                self.gui_repaint()
1202        else:
1203            FigureCanvasWxAgg.draw(self, repaint)
1204
1205        # Don't redraw the decorations when called by _onPaint()
1206        if doRepaint:
1207            self.location.redraw()
1208            self.crosshairs.redraw()
1209            self.rubberband.redraw()
1210
1211    def notify_point(self, axes, x, y):
1212        """
1213        Called by the associated C{PlotPanelDirector} to emit a C{PointEvent}.
1214        """
1215        wx.PostEvent(self, PointEvent(self.GetId(), axes, x, y))
1216
1217    def notify_selection(self, axes, x1, y1, x2, y2):
1218        """
1219        Called by the associated C{PlotPanelDirector} to emit a
1220        C{SelectionEvent}.
1221        """
1222        wx.PostEvent(self, SelectionEvent(self.GetId(), axes, x1, y1, x2, y2))
1223
1224    def _get_canvas_xy(self, evt):
1225        """
1226        Returns the X and Y coordinates of a wxPython event object converted to
1227        matplotlib canavas coordinates.
1228        """
1229        return evt.GetX(), int(self.figure.bbox.height() - evt.GetY())
1230
1231    def _onKeyDown(self, evt):
1232        """
1233        Overrides the C{FigureCanvasWxAgg} key-press event handler, dispatching
1234        the event to the associated C{PlotPanelDirector}.
1235        """
1236        self.director.keyDown(evt)
1237
1238    def _onKeyUp(self, evt):
1239        """
1240        Overrides the C{FigureCanvasWxAgg} key-release event handler,
1241        dispatching the event to the associated C{PlotPanelDirector}.
1242        """
1243        self.director.keyUp(evt)
1244 
1245    def _onLeftButtonDown(self, evt):
1246        """
1247        Overrides the C{FigureCanvasWxAgg} left-click event handler,
1248        dispatching the event to the associated C{PlotPanelDirector}.
1249        """
1250        x, y = self._get_canvas_xy(evt)
1251        self.director.leftButtonDown(evt, x, y)
1252
1253    def _onLeftButtonUp(self, evt):
1254        """
1255        Overrides the C{FigureCanvasWxAgg} left-click-release event handler,
1256        dispatching the event to the associated C{PlotPanelDirector}.
1257        """
1258        x, y = self._get_canvas_xy(evt)
1259        self.director.leftButtonUp(evt, x, y)
1260
1261    def _onRightButtonDown(self, evt):
1262        """
1263        Overrides the C{FigureCanvasWxAgg} right-click event handler,
1264        dispatching the event to the associated C{PlotPanelDirector}.
1265        """
1266        x, y = self._get_canvas_xy(evt)
1267        self.director.rightButtonDown(evt, x, y)
1268
1269    def _onRightButtonUp(self, evt):
1270        """
1271        Overrides the C{FigureCanvasWxAgg} right-click-release event handler,
1272        dispatching the event to the associated C{PlotPanelDirector}.
1273        """
1274        x, y = self._get_canvas_xy(evt)
1275        self.director.rightButtonUp(evt, x, y)
1276
1277    def _onMotion(self, evt):
1278        """
1279        Overrides the C{FigureCanvasWxAgg} mouse motion event handler,
1280        dispatching the event to the associated C{PlotPanelDirector}.
1281        """
1282        x, y = self._get_canvas_xy(evt)
1283        self.director.mouseMotion(evt, x, y)
1284
1285
1286#
1287# Matplotlib canvas in a top-level wxPython window
1288#
1289
1290class PlotFrame(wx.Frame):
1291    """
1292    A matplotlib canvas embedded in a wxPython top-level window.
1293
1294    @cvar ABOUT_TITLE: Title of the "About" dialog.
1295    @cvar ABOUT_MESSAGE: Contents of the "About" dialog.
1296    """
1297
1298    ABOUT_TITLE = 'About wxmpl.PlotFrame'
1299    ABOUT_MESSAGE = ('wxmpl.PlotFrame %s\n' %  __version__
1300        + 'Written by Ken McIvor <mcivor@iit.edu>\n'
1301        + 'Copyright 2005 Illinois Institute of Technology')
1302
1303    def __init__(self, parent, id, title, size=(6.0, 3.7), dpi=96, cursor=True,
1304     location=True, crosshairs=True, selection=True, zoom=True, **kwds):
1305        """
1306        Creates a new PlotFrame top-level window that is the child of the
1307        wxPython window C{parent} with the wxPython identifier C{id} and the
1308        title of C{title}.
1309
1310        All of the named keyword arguments to this constructor have the same
1311        meaning as those arguments to the constructor of C{PlotPanel}.
1312
1313        Any additional keyword arguments are passed to the constructor of
1314        C{wx.Frame}.
1315        """
1316        wx.Frame.__init__(self, parent, id, title, **kwds)
1317        self.panel = PlotPanel(self, -1, size, dpi, cursor, location,
1318            crosshairs, selection, zoom)
1319
1320        pData = wx.PrintData()
1321        pData.SetPaperId(wx.PAPER_LETTER)
1322        pData.SetPrinterCommand(LINUX_PRINTING_COMMAND)
1323        self.printer = FigurePrinter(self, pData)
1324
1325        self.create_menus()
1326        sizer = wx.BoxSizer(wx.VERTICAL)
1327        sizer.Add(self.panel, 1, wx.ALL|wx.EXPAND, 5)
1328        self.SetSizer(sizer)
1329        self.Fit()
1330
1331        wx.EVT_WINDOW_DESTROY(self, self.OnDestroy)
1332
1333    def create_menus(self):
1334        mainMenu = wx.MenuBar()
1335        menu = wx.Menu()
1336
1337        id = wx.NewId()
1338        menu.Append(id, '&Save As...\tCtrl+S',
1339            'Save a copy of the current plot')
1340        wx.EVT_MENU(self, id, self.OnMenuFileSave)
1341
1342        # Printing under OSX doesn't work well because the DPI of the
1343        # printer is always reported as 72.  It will be disabled until print
1344        # qualities are mapped onto wx.PostScriptDC resolutions.
1345
1346        if not sys.platform.startswith('darwin'):
1347            menu.AppendSeparator()
1348
1349            id = wx.NewId()
1350            menu.Append(id, 'Page Set&up...',
1351                'Set the size and margins of the printed figure')
1352            wx.EVT_MENU(self, id, self.OnMenuFilePageSetup)
1353
1354            id = wx.NewId()
1355            menu.Append(id, 'Print Pre&view...',
1356                'Preview the print version of the current plot')
1357            wx.EVT_MENU(self, id, self.OnMenuFilePrintPreview)
1358
1359            id = wx.NewId()
1360            menu.Append(id, '&Print...\tCtrl+P', 'Print the current plot')
1361            wx.EVT_MENU(self, id, self.OnMenuFilePrint)
1362
1363        menu.AppendSeparator()
1364
1365        id = wx.NewId()
1366        menu.Append(id, '&Close Window\tCtrl+W',
1367            'Close the current plot window')
1368        wx.EVT_MENU(self, id, self.OnMenuFileClose)
1369
1370        mainMenu.Append(menu, '&File')
1371        menu = wx.Menu()
1372
1373        id = wx.NewId()
1374        menu.Append(id, '&About...', 'Display version information')
1375        wx.EVT_MENU(self, id, self.OnMenuHelpAbout)
1376
1377        mainMenu.Append(menu, '&Help')
1378        self.SetMenuBar(mainMenu)
1379
1380    def OnDestroy(self, evt):
1381        if self.GetId() == evt.GetEventObject().GetId():
1382            self.printer.destroy()
1383
1384    def OnMenuFileSave(self, evt):
1385        """
1386        Handles File->Save menu events.
1387        """
1388        fileName = wx.FileSelector('Save Plot', default_extension='png',
1389            wildcard=('Portable Network Graphics (*.png)|*.png|'
1390                + 'Encapsulated Postscript (*.eps)|*.eps|All files (*.*)|*.*'),
1391            parent=self, flags=wx.SAVE|wx.OVERWRITE_PROMPT)
1392
1393        if not fileName:
1394            return
1395
1396        path, ext = os.path.splitext(fileName)
1397        ext = ext[1:].lower()
1398
1399        if ext != 'png' and ext != 'eps':
1400            error_message = (
1401                'Only the PNG and EPS image formats are supported.\n'
1402                'A file extension of `png\' or `eps\' must be used.')
1403            wx.MessageBox(error_message, 'Error - plotit',
1404                parent=self, style=wx.OK|wx.ICON_ERROR)
1405            return
1406
1407        try:
1408            self.panel.print_figure(fileName)
1409        except IOError, e:
1410            if e.strerror:
1411                err = e.strerror
1412            else:
1413                err = e
1414
1415            wx.MessageBox('Could not save file: %s' % err, 'Error - plotit',
1416                parent=self, style=wx.OK|wx.ICON_ERROR)
1417
1418    def OnMenuFilePageSetup(self, evt):
1419        """
1420        Handles File->Page Setup menu events
1421        """
1422        self.printer.pageSetup()
1423
1424    def OnMenuFilePrintPreview(self, evt):
1425        """
1426        Handles File->Print Preview menu events
1427        """
1428        self.printer.previewFigure(self.get_figure())
1429
1430    def OnMenuFilePrint(self, evt):
1431        """
1432        Handles File->Print menu events
1433        """
1434        self.printer.printFigure(self.get_figure())
1435
1436    def OnMenuFileClose(self, evt):
1437        """
1438        Handles File->Close menu events.
1439        """
1440        self.Close()
1441
1442    def OnMenuHelpAbout(self, evt):
1443        """
1444        Handles Help->About menu events.
1445        """
1446        wx.MessageBox(self.ABOUT_MESSAGE, self.ABOUT_TITLE, parent=self,
1447            style=wx.OK)
1448
1449    def get_figure(self):
1450        """
1451        Returns the figure associated with this canvas.
1452        """
1453        return self.panel.figure
1454
1455    def set_cursor(self, state):
1456        """
1457        Enable or disable the changing mouse cursor.  When enabled, the cursor
1458        changes from the normal arrow to a square cross when the mouse enters a
1459        matplotlib axes on this canvas.
1460        """
1461        self.panel.set_cursor(state)
1462
1463    def set_location(self, state):
1464        """
1465        Enable or disable the display of the matplotlib axes coordinates of the
1466        mouse in the lower left corner of the canvas.
1467        """
1468        self.panel.set_location(state)
1469
1470    def set_crosshairs(self, state):
1471        """
1472        Enable or disable drawing crosshairs through the mouse cursor when it
1473        is inside a matplotlib axes.
1474        """
1475        self.panel.set_crosshairs(state)
1476
1477    def set_selection(self, state):
1478        """
1479        Enable or disable area selections, where user selects a rectangular
1480        area of the canvas by left-clicking and dragging the mouse.
1481        """
1482        self.panel.set_selection(state)
1483
1484    def set_zoom(self, state):
1485        """
1486        Enable or disable zooming in when the user makes an area selection and
1487        zooming out again when the user right-clicks.
1488        """
1489        self.panel.set_zoom(state)
1490
1491    def draw(self):
1492        """
1493        Draw the associated C{Figure} onto the screen.
1494        """
1495        self.panel.draw()
1496
1497
1498#
1499# wxApp providing a matplotlib canvas in a top-level wxPython window
1500#
1501
1502class PlotApp(wx.App):
1503    """
1504    A wxApp that provides a matplotlib canvas embedded in a wxPython top-level
1505    window, encapsulating wxPython's nuts and bolts.
1506
1507    @cvar ABOUT_TITLE: Title of the "About" dialog.
1508    @cvar ABOUT_MESSAGE: Contents of the "About" dialog.
1509    """
1510
1511    ABOUT_TITLE = None
1512    ABOUT_MESSAGE = None
1513
1514    def __init__(self, title="WxMpl", size=(6.0, 3.7), dpi=96, cursor=True,
1515     location=True, crosshairs=True, selection=True, zoom=True, **kwds):
1516        """
1517        Creates a new PlotApp, which creates a PlotFrame top-level window.
1518
1519        The keyword argument C{title} specifies the title of this top-level
1520        window.
1521
1522        All of other the named keyword arguments to this constructor have the
1523        same meaning as those arguments to the constructor of C{PlotPanel}.
1524
1525        Any additional keyword arguments are passed to the constructor of
1526        C{wx.App}.
1527        """
1528        self.title = title
1529        self.size = size
1530        self.dpi = dpi
1531        self.cursor = cursor
1532        self.location = location
1533        self.crosshairs = crosshairs
1534        self.selection = selection
1535        self.zoom = zoom
1536        wx.App.__init__(self, **kwds)
1537
1538    def OnInit(self):
1539        self.frame = panel = PlotFrame(None, -1, self.title, self.size,
1540            self.dpi, self.cursor, self.location, self.crosshairs,
1541            self.selection, self.zoom)
1542
1543        if self.ABOUT_TITLE is not None:
1544            panel.ABOUT_TITLE = self.ABOUT_TITLE
1545
1546        if self.ABOUT_MESSAGE is not None:
1547            panel.ABOUT_MESSAGE = self.ABOUT_MESSAGE
1548
1549        panel.Show(True)
1550        return True
1551
1552    def get_figure(self):
1553        """
1554        Returns the figure associated with this canvas.
1555        """
1556        return self.frame.get_figure()
1557
1558    def set_cursor(self, state):
1559        """
1560        Enable or disable the changing mouse cursor.  When enabled, the cursor
1561        changes from the normal arrow to a square cross when the mouse enters a
1562        matplotlib axes on this canvas.
1563        """
1564        self.frame.set_cursor(state)
1565
1566    def set_location(self, state):
1567        """
1568        Enable or disable the display of the matplotlib axes coordinates of the
1569        mouse in the lower left corner of the canvas.
1570        """
1571        self.frame.set_location(state)
1572
1573    def set_crosshairs(self, state):
1574        """
1575        Enable or disable drawing crosshairs through the mouse cursor when it
1576        is inside a matplotlib axes.
1577        """
1578        self.frame.set_crosshairs(state)
1579
1580    def set_selection(self, state):
1581        """
1582        Enable or disable area selections, where user selects a rectangular
1583        area of the canvas by left-clicking and dragging the mouse.
1584        """
1585        self.frame.set_selection(state)
1586
1587    def set_zoom(self, state):
1588        """
1589        Enable or disable zooming in when the user makes an area selection and
1590        zooming out again when the user right-clicks.
1591        """
1592        self.frame.set_zoom(state)
1593
1594    def draw(self):
1595        """
1596        Draw the associated C{Figure} onto the screen.
1597        """
1598        self.frame.draw()
1599
1600
1601#
1602# Automatically resizing vectors and matrices
1603#
1604
1605class VectorBuffer:
1606    """
1607    Manages a Numerical Python vector, automatically growing it as necessary to
1608    accomodate new entries.
1609    """
1610    def __init__(self):
1611        self.data = Numerix.zeros((16,), Numerix.Float)
1612        self.nextRow = 0
1613
1614    def clear(self):
1615        """
1616        Zero and reset this buffer without releasing the underlying array.
1617        """
1618        self.data[:] = 0.0
1619        self.nextRow = 0
1620
1621    def reset(self):
1622        """
1623        Zero and reset this buffer, releasing the underlying array.
1624        """
1625        self.data = Numerix.zeros((16,), Numerix.Float)
1626        self.nextRow = 0
1627
1628    def append(self, point):
1629        """
1630        Append a new entry to the end of this buffer's vector.
1631        """
1632        nextRow = self.nextRow
1633        data = self.data
1634
1635        resize = False
1636        if nextRow == data.shape[0]:
1637            nR = int(Numerix.ceil(self.data.shape[0]*1.5))
1638            resize = True
1639
1640        if resize:
1641            self.data = Numerix.zeros((nR,), Numerix.Float)
1642            self.data[0:data.shape[0]] = data
1643
1644        self.data[nextRow] = point
1645        self.nextRow += 1
1646
1647    def getData(self):
1648        """
1649        Returns the current vector or C{None} if the buffer contains no data.
1650        """
1651        if self.nextRow == 0:
1652            return None
1653        else:
1654            return self.data[0:self.nextRow]
1655
1656
1657class MatrixBuffer:
1658    """
1659    Manages a Numerical Python matrix, automatically growing it as necessary to
1660    accomodate new rows of entries.
1661    """
1662    def __init__(self):
1663        self.data = Numerix.zeros((16, 1), Numerix.Float)
1664        self.nextRow = 0
1665
1666    def clear(self):
1667        """
1668        Zero and reset this buffer without releasing the underlying array.
1669        """
1670        self.data[:, :] = 0.0
1671        self.nextRow = 0
1672
1673    def reset(self):
1674        """
1675        Zero and reset this buffer, releasing the underlying array.
1676        """
1677        self.data = Numerix.zeros((16, 1), Numerix.Float)
1678        self.nextRow = 0
1679
1680    def append(self, row):
1681        """
1682        Append a new row of entries to the end of this buffer's matrix.
1683        """
1684        row = Numerix.asarray(row, Numerix.Float)
1685        nextRow = self.nextRow
1686        data = self.data
1687        nPts = row.shape[0]
1688
1689        if nPts == 0:
1690            return
1691
1692        resize = True
1693        if nextRow == data.shape[0]:
1694            nC = data.shape[1]
1695            nR = int(Numerix.ceil(self.data.shape[0]*1.5))
1696            if nC < nPts:
1697                nC = nPts
1698        elif data.shape[1] < nPts:
1699            nR = data.shape[0]
1700            nC = nPts
1701        else:
1702            resize = False
1703
1704        if resize:
1705            self.data = Numerix.zeros((nR, nC), Numerix.Float)
1706            rowEnd, colEnd = data.shape
1707            self.data[0:rowEnd, 0:colEnd] = data
1708
1709        self.data[nextRow, 0:nPts] = row
1710        self.nextRow += 1
1711
1712    def getData(self):
1713        """
1714        Returns the current matrix or C{None} if the buffer contains no data.
1715        """
1716        if self.nextRow == 0:
1717            return None
1718        else:
1719            return self.data[0:self.nextRow, :]
1720
1721
1722#
1723# Utility functions used by the StripCharter
1724#
1725
1726def make_delta_bbox(X1, Y1, X2, Y2):
1727    """
1728    Returns a C{Bbox} describing the range of difference between two sets of X
1729    and Y coordinates.
1730    """
1731    return make_bbox(get_delta(X1, X2), get_delta(Y1, Y2))
1732
1733
1734def get_delta(X1, X2):
1735    """
1736    Returns the vector of contiguous, different points between two vectors.
1737    """
1738    n1 = X1.shape[0]
1739    n2 = X2.shape[0]
1740
1741    if n1 < n2:
1742        return X2[n1:]
1743    elif n1 == n2:
1744        # shape is no longer a reliable indicator of change, so assume things
1745        # are different
1746        return X2
1747    else:
1748        return X2
1749
1750
1751def make_bbox(X, Y):
1752    """
1753    Returns a C{Bbox} that contains the supplied sets of X and Y coordinates.
1754    """
1755    if X is None or X.shape[0] == 0:
1756        x1 = x2 = 0.0
1757    else:
1758        x1 = min(X)
1759        x2 = max(X)
1760
1761    if Y is None or Y.shape[0] == 0:
1762        y1 = y2 = 0.0
1763    else:
1764        y1 = min(Y)
1765        y2 = max(Y)
1766
1767    return Bbox(Point(Value(x1), Value(y1)), Point(Value(x2), Value(y2)))
1768
1769
1770#
1771# Strip-charts lines using a matplotlib axes
1772#
1773
1774class StripCharter:
1775    """
1776    Plots and updates lines on a matplotlib C{Axes}.
1777    """
1778    def __init__(self, axes):
1779        """
1780        Create a new C{StripCharter} associated with a matplotlib C{axes}.
1781        """
1782        self.axes = axes
1783        self.channels = []
1784        self.lines = {}
1785
1786    def setChannels(self, channels):
1787        """
1788        Specify the data-providers of the lines to be plotted and updated.
1789        """
1790        self.lines = None
1791        self.channels = channels[:]
1792
1793        # minimal Axes.cla()
1794        self.axes.legend_ = None
1795        self.axes.lines = []
1796
1797    def update(self):
1798        """
1799        Redraw the associated axes with updated lines if any of the channels'
1800        data has changed.
1801        """
1802        axes = self.axes
1803        figureCanvas = axes.figure.canvas
1804        zoomed = figureCanvas.zoomed(axes)
1805
1806        redraw = False
1807        if self.lines is None:
1808            self._create_plot()
1809            redraw = True
1810        else:
1811            for channel in self.channels:
1812                redraw = self._update_channel(channel, zoomed) or redraw
1813
1814        if redraw:
1815            if not zoomed:
1816                axes.autoscale_view()
1817            figureCanvas.draw()
1818
1819    def _create_plot(self):
1820        """
1821        Initially plot the lines corresponding to the data-providers.
1822        """
1823        self.lines = {}
1824        axes = self.axes
1825
1826        styleGen = _process_plot_var_args()
1827        for channel in self.channels:
1828            self._plot_channel(channel, styleGen)
1829
1830        if self.channels:
1831            lines  = [self.lines[x] for x in self.channels]
1832            labels = [x.get_label() for x in lines]
1833            self.axes.legend(lines, labels, pad=0.1, axespad=0.0, numpoints=2,
1834                handlelen=0.02, handletextsep=0.01,
1835                prop=FontProperties(size='xx-small'))
1836
1837#        # Draw the legend on the figure instead...
1838#        handles = [self.lines[x] for x in self.channels]
1839#        labels = [x._label for x in handles]
1840#        self.axes.figure.legend(handles, labels, 'upper right',
1841#            pad=0.1, handlelen=0.02, handletextsep=0.01, numpoints=2,
1842#            prop=FontProperties(size='xx-small'))
1843
1844    def _plot_channel(self, channel, styleGen):
1845        """
1846        Initially plot a line corresponding to one of the data-providers.
1847        """
1848        empty = False
1849        x = channel.getX()
1850        y = channel.getY()
1851        if x is None or y is None:
1852            x = y = []
1853            empty = True
1854
1855        line = styleGen(x, y).next()
1856        line._wxmpl_empty_line = empty
1857
1858        if channel.getColor() is not None:
1859            line.set_color(channel.getColor())
1860        if channel.getStyle() is not None:
1861            line.set_linestyle(channel.getStyle())
1862        if channel.getMarker() is not None:
1863            line.set_marker(channel.getMarker())
1864            line.set_markeredgecolor(line.get_color())
1865            line.set_markerfacecolor(line.get_color())
1866
1867        line.set_label(channel.getLabel())
1868        self.lines[channel] = line
1869        if not empty:
1870            self.axes.add_line(line)
1871
1872    def _update_channel(self, channel, zoomed):
1873        """
1874        Replot a line corresponding to one of the data-providers if the data
1875        has changed.
1876        """
1877        if channel.hasChanged():
1878            channel.setChanged(False)
1879        else:
1880            return False
1881
1882        axes = self.axes
1883        line = self.lines[channel]
1884        newX = channel.getX()
1885        newY = channel.getY()
1886
1887        if newX is None or newY is None:
1888            return False
1889
1890        oldX = line._x
1891        oldY = line._y
1892
1893        x, y = newX, newY
1894        line.set_data(x, y)
1895
1896        if line._wxmpl_empty_line:
1897            axes.add_line(line)
1898            line._wxmpl_empty_line = False
1899        else:
1900            if line.get_transform() != axes.transData:
1901                xys = axes._get_verts_in_data_coords(
1902                    line.get_transform(), zip(x, y))
1903                x = Numerix.array([a for (a, b) in xys])
1904                y = Numerix.array([b for (a, b) in xys])
1905            axes.update_datalim_numerix(x, y)
1906
1907        if zoomed:
1908            return axes.viewLim.overlaps(
1909                make_delta_bbox(oldX, oldY, newX, newY))
1910        else:
1911            return True
1912
1913
1914#
1915# Data-providing interface to the StripCharter
1916#
1917
1918class Channel:
1919    """
1920    Provides data for a C{StripCharter} to plot.  Subclasses of C{Channel}
1921    override the template methods C{getX()} and C{getY()} to provide plot data
1922    and call C{setChanged(True)} when that data has changed.
1923    """
1924    def __init__(self, name, color=None, style=None, marker=None):
1925        """
1926        Creates a new C{Channel} with the matplotlib label C{name}.  The
1927        keyword arguments specify the strings for the line color, style, and
1928        marker to use when the line is plotted.
1929        """
1930        self.name = name
1931        self.color = color
1932        self.style = style
1933        self.marker = marker
1934        self.changed = False
1935
1936    def getLabel(self):
1937        """
1938        Returns the matplotlib label for this channel of data.
1939        """
1940        return self.name
1941
1942    def getColor(self):
1943        """
1944        Returns the line color string to use when the line is plotted, or
1945        C{None} to use an automatically generated color.
1946        """
1947        return self.color
1948
1949    def getStyle(self):
1950        """
1951        Returns the line style string to use when the line is plotted, or
1952        C{None} to use the default line style.
1953        """
1954        return self.style
1955
1956    def getMarker(self):
1957        """
1958        Returns the line marker string to use when the line is plotted, or
1959        C{None} to use the default line marker.
1960        """
1961        return self.marker
1962
1963    def hasChanged(self):
1964        """
1965        Returns a boolean indicating if the line data has changed.
1966        """
1967        return self.changed
1968
1969    def setChanged(self, changed):
1970        """
1971        Sets the change indicator to the boolean value C{changed}.
1972
1973        @note: C{StripCharter} instances call this method after detecting a
1974        change, so a C{Channel} cannot be shared among multiple charts.
1975        """
1976        self.changed = changed
1977
1978    def getX(self):
1979        """
1980        Template method that returns the vector of X axis data or C{None} if
1981        there is no data available.
1982        """
1983        return None
1984
1985    def getY(self):
1986        """
1987        Template method that returns the vector of Y axis data or C{None} if
1988        there is no data available.
1989        """
1990        return None
1991
Note: See TracBrowser for help on using the repository browser.