source: trunk/wxmpl131.py @ 23

Last change on this file since 23 was 23, checked in by toby, 13 years ago

add a new sub directory based on OS and python ver for binary file(s)

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