1
2
3
4
5
6
7
8
9 """
10 Embedding matplotlib in wxPython applications is straightforward, but the
11 default plotting widget lacks the capabilities necessary for interactive use.
12 WxMpl (wxPython+matplotlib) is a library of components that provide these
13 missing features in the form of a better matplolib FigureCanvas.
14 """
15
16
17 import wx
18 import sys
19 import os.path
20 import weakref
21
22 import matplotlib
23 matplotlib.use('WXAgg')
24 import numpy as NumPy
25 from matplotlib.axes import _process_plot_var_args
26 from matplotlib.backend_bases import FigureCanvasBase
27 from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
28 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
29 from matplotlib.figure import Figure
30 from matplotlib.font_manager import FontProperties
31 from matplotlib.projections.polar import PolarAxes
32 from matplotlib.transforms import Bbox
33
34 __version__ = '2.0dev'
35
36 __all__ = ['PlotPanel', 'PlotFrame', 'PlotApp', 'StripCharter', 'Channel',
37 'FigurePrinter', 'PointEvent', 'EVT_POINT', 'SelectionEvent',
38 'EVT_SELECTION']
39
40
41
42 POSTSCRIPT_PRINTING_COMMAND = 'lpr'
43
44
45
46
47
48
49
50
51 MATPLOTLIB_0_98_3 = '0.98.3' <= matplotlib.__version__
52
53
54
55
56
57
59 """
60 Returns a coordinate inverted by the specificed C{Transform}.
61 """
62 return transform.inverted().transform_point((x, y))
63
64
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
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
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
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
149
150
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
162 """
163 Alters the X and Y limits of C{Axes} objects while maintaining a history of
164 the changes.
165 """
167 self.autoscaleUnzoom = autoscaleUnzoom
168 self.history = weakref.WeakKeyDictionary()
169
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
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
201
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
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
231
232
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
242
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
259 """
260 Enable or disable left-click area selection.
261 """
262 self.selectionEnabled = state
263
265 """
266 Enable or disable zooming as a result of left-click area selection.
267 """
268 self.zoomEnabled = state
269
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
278 """
279 Enable or disable unzooming as a result of right-clicking.
280 """
281 self.rightClickUnzoom = state
282
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
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
298 """
299 Handles wxPython key-press events. These events are currently skipped.
300 """
301 evt.Skip()
302
304 """
305 Handles wxPython key-release events. These events are currently
306 skipped.
307 """
308 evt.Skip()
309
322
366
373
385
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
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
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
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
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
447
448
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
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
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
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
546
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
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
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
580
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
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
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
609 """
610 Draws crosshairs through the current position of the mouse.
611 """
612
613 PEN = wx.WHITE_PEN
614 FUNCTION = wx.XOR
615
622
624 """
625 Draws crosshairs through the C{(X, Y)} coordinates.
626 """
627 dc.CrossHair(*value)
628
630 """
631 Clears the crosshairs drawn through the C{(X, Y)} coordinates.
632 """
633 dc.CrossHair(*value)
634
635
637 """
638 Draws a selection rubberband from one point to another.
639 """
640
641 PEN = wx.WHITE_PEN
642 FUNCTION = wx.XOR
643
656
658 """
659 Draws the selection rubberband around the rectangle
660 C{(x1, y1, x2, y2)}.
661 """
662 dc.DrawRectangle(*value)
663
665 """
666 Clears the selection rubberband around the rectangle
667 C{(x1, y1, x2, y2)}.
668 """
669 dc.DrawRectangle(*value)
670
671
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
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
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
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
717
718
719
720 PS_DPI_HIGH_QUALITY = 600
721 PS_DPI_MEDIUM_QUALITY = 300
722 PS_DPI_LOW_QUALITY = 150
723 PS_DPI_DRAFT_QUALITY = 72
724
725
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
830
831
994
995
996
997
998
999
1000 EVT_POINT_ID = wx.NewId()
1001
1002
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
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 """
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
1034 return PointEvent(self.GetId(), self.axes, self.x, self.y)
1035
1036
1037 EVT_SELECTION_ID = wx.NewId()
1038
1039
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
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
1080 return SelectionEvent(self.GetId(), self.axes, self.x1, self.y1,
1081 self.x2, self.y2)
1082
1083
1084
1085
1086
1087
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
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
1125
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
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
1144 """
1145 Overrides the wxPython backround repainting event to reduce flicker.
1146 """
1147 pass
1148
1150 """
1151 Handles the wxPython window destruction event.
1152 """
1153 if self.GetId() == evt.GetEventObject().GetId():
1154
1155 topwin = toplevel_parent_of_window(self)
1156 topwin.Disconnect(-1, self.GetId(), wx.wxEVT_ACTIVATE)
1157
1159 """
1160 Overrides the C{FigureCanvasWxAgg} paint event to redraw the
1161 crosshairs, etc.
1162 """
1163
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
1181
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
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
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
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
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
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
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
1237
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
1248 if not self.insideOnPaint:
1249 self.location.redraw()
1250 self.crosshairs.redraw()
1251 self.rubberband.redraw()
1252
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
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
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
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
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
1294
1302
1310
1318
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
1330
1331
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
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
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
1453 """
1454 Handles File->Page Setup menu events
1455 """
1456 self.printer.pageSetup()
1457
1463
1469
1471 """
1472 Handles File->Close menu events.
1473 """
1474 self.Close()
1475
1477 """
1478 Handles Help->About menu events.
1479 """
1480 wx.MessageBox(self.ABOUT_MESSAGE, self.ABOUT_TITLE, parent=self,
1481 style=wx.OK)
1482
1488
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
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
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
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
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
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
1533 """
1534 Draw the associated C{Figure} onto the screen.
1535 """
1536 self.panel.draw()
1537
1538
1539
1540
1541
1542
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
1592
1598
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
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
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
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
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
1636 """
1637 Draw the associated C{Figure} onto the screen.
1638 """
1639 self.frame.draw()
1640
1641
1642
1643
1644
1645
1647 """
1648 Manages a Numerical Python vector, automatically growing it as necessary to
1649 accomodate new entries.
1650 """
1652 self.data = NumPy.zeros((16,), NumPy.Float)
1653 self.nextRow = 0
1654
1656 """
1657 Zero and reset this buffer without releasing the underlying array.
1658 """
1659 self.data[:] = 0.0
1660 self.nextRow = 0
1661
1663 """
1664 Zero and reset this buffer, releasing the underlying array.
1665 """
1666 self.data = NumPy.zeros((16,), NumPy.Float)
1667 self.nextRow = 0
1668
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(NumPy.ceil(self.data.shape[0]*1.5))
1679 resize = True
1680
1681 if resize:
1682 self.data = NumPy.zeros((nR,), NumPy.Float)
1683 self.data[0:data.shape[0]] = data
1684
1685 self.data[nextRow] = point
1686 self.nextRow += 1
1687
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
1699 """
1700 Manages a Numerical Python matrix, automatically growing it as necessary to
1701 accomodate new rows of entries.
1702 """
1704 self.data = NumPy.zeros((16, 1), NumPy.Float)
1705 self.nextRow = 0
1706
1708 """
1709 Zero and reset this buffer without releasing the underlying array.
1710 """
1711 self.data[:, :] = 0.0
1712 self.nextRow = 0
1713
1715 """
1716 Zero and reset this buffer, releasing the underlying array.
1717 """
1718 self.data = NumPy.zeros((16, 1), NumPy.Float)
1719 self.nextRow = 0
1720
1722 """
1723 Append a new row of entries to the end of this buffer's matrix.
1724 """
1725 row = NumPy.asarray(row, NumPy.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(NumPy.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 = NumPy.zeros((nR, nC), NumPy.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
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
1765
1766
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
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
1786
1787 return X2
1788 else:
1789 return X2
1790
1791
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
1813
1814
1816 """
1817 Plots and updates lines on a matplotlib C{Axes}.
1818 """
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
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
1835 self.axes.legend_ = None
1836 self.axes.lines = []
1837
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
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
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
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 = NumPy.zeros((x.shape[0], 2), NumPy.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
1952
1953
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
1973 """
1974 Returns the matplotlib label for this channel of data.
1975 """
1976 return self.name
1977
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
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
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
2000 """
2001 Returns a boolean indicating if the line data has changed.
2002 """
2003 return self.changed
2004
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
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
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