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 matplotlib.numerix as Numerix
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__ = '1.3.0'
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 Alters the X and Y limits of C{Axes} objects while maintaining a history of
154 the changes.
155 """
157 self.autoscaleUnzoom = autoscaleUnzoom
158 self.history = weakref.WeakKeyDictionary()
159
161 """
162 Enable or disable autoscaling the axes as a result of zooming all the
163 way back out.
164 """
165 self.limits.setAutoscaleUnzoom(state)
166
167 - def _get_history(self, axes):
168 """
169 Returns the history list of X and Y limits associated with C{axes}.
170 """
171 return self.history.setdefault(axes, [])
172
174 """
175 Returns a boolean indicating whether C{axes} has had its limits
176 altered.
177 """
178 return not (not self._get_history(axes))
179
180 - def set(self, axes, xrange, yrange):
181 """
182 Changes the X and Y limits of C{axes} to C{xrange} and {yrange}
183 respectively. A boolean indicating whether or not the
184 axes should be redraw is returned, because polar axes cannot have
185 their limits changed sensibly.
186 """
187 if not axes.can_zoom():
188 return False
189
190
191
192 oldRange = tuple(axes.get_xlim()), tuple(axes.get_ylim())
193
194 history = self._get_history(axes)
195 history.append(oldRange)
196 axes.set_xlim(xrange)
197 axes.set_ylim(yrange)
198 return True
199
201 """
202 Changes the X and Y limits of C{axes} to their previous values. A
203 boolean indicating whether or not the axes should be redraw is
204 returned.
205 """
206 history = self._get_history(axes)
207 if not history:
208 return False
209
210 xrange, yrange = history.pop()
211 if self.autoscaleUnzoom and not len(history):
212 axes.autoscale_view()
213 else:
214 axes.set_xlim(xrange)
215 axes.set_ylim(yrange)
216 return True
217
218
219
220
221
222
224 """
225 Encapsulates all of the user-interaction logic required by the
226 C{PlotPanel}, following the Humble Dialog Box pattern proposed by Michael
227 Feathers:
228 U{http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf}
229 """
230
231
232
233
234 - def __init__(self, view, zoom=True, selection=True, rightClickUnzoom=True,
235 autoscaleUnzoom=True):
236 """
237 Create a new director for the C{PlotPanel} C{view}. The keyword
238 arguments C{zoom} and C{selection} have the same meanings as for
239 C{PlotPanel}.
240 """
241 self.view = view
242 self.zoomEnabled = zoom
243 self.selectionEnabled = selection
244 self.rightClickUnzoom = rightClickUnzoom
245 self.limits = AxesLimits(autoscaleUnzoom)
246 self.leftButtonPoint = None
247
249 """
250 Enable or disable left-click area selection.
251 """
252 self.selectionEnabled = state
253
255 """
256 Enable or disable zooming as a result of left-click area selection.
257 """
258 self.zoomEnabled = state
259
261 """
262 Enable or disable autoscaling the axes as a result of zooming all the
263 way back out.
264 """
265 self.limits.setAutoscaleUnzoom(state)
266
268 """
269 Enable or disable unzooming as a result of right-clicking.
270 """
271 self.rightClickUnzoom = state
272
274 """
275 Indicates if plot may be not redrawn due to the presence of a selection
276 box.
277 """
278 return self.leftButtonPoint is None
279
281 """
282 Returns a boolean indicating whether or not the plot has been zoomed in
283 as a result of a left-click area selection.
284 """
285 return self.limits.zoomed(axes)
286
288 """
289 Handles wxPython key-press events. These events are currently skipped.
290 """
291 evt.Skip()
292
294 """
295 Handles wxPython key-release events. These events are currently
296 skipped.
297 """
298 evt.Skip()
299
312
356
363
375
377 """
378 Handles wxPython mouse motion events, dispatching them based on whether
379 or not a selection is in process and what the cursor is over.
380 """
381 view = self.view
382 axes, xdata, ydata = find_axes(view, x, y)
383
384 if self.leftButtonPoint is not None:
385 self.selectionMouseMotion(evt, x, y, axes, xdata, ydata)
386 else:
387 if axes is None:
388 self.canvasMouseMotion(evt, x, y)
389 elif not axes.can_zoom():
390 self.unzoomableAxesMouseMotion(evt, x, y, axes, xdata, ydata)
391 else:
392 self.axesMouseMotion(evt, x, y, axes, xdata, ydata)
393
395 """
396 Handles wxPython mouse motion events that occur during a left-click
397 area selection.
398 """
399 view = self.view
400 x0, y0 = self.leftButtonPoint
401 view.rubberband.set(x0, y0, x, y)
402 if axes is None:
403 view.location.clear()
404 else:
405 view.location.set(format_coord(axes, xdata, ydata))
406
408 """
409 Handles wxPython mouse motion events that occur over the canvas.
410 """
411 view = self.view
412 view.cursor.setNormal()
413 view.crosshairs.clear()
414 view.location.clear()
415
417 """
418 Handles wxPython mouse motion events that occur over an axes.
419 """
420 view = self.view
421 view.cursor.setCross()
422 view.crosshairs.set(x, y)
423 view.location.set(format_coord(axes, xdata, ydata))
424
426 """
427 Handles wxPython mouse motion events that occur over an axes that does
428 not support zooming.
429 """
430 view = self.view
431 view.cursor.setNormal()
432 view.location.set(format_coord(axes, xdata, ydata))
433
434
435
436
437
438
440 """
441 Painters encapsulate the mechanics of drawing some value in a wxPython
442 window and erasing it. Subclasses override template methods to process
443 values and draw them.
444
445 @cvar PEN: C{wx.Pen} to use (defaults to C{wx.BLACK_PEN})
446 @cvar BRUSH: C{wx.Brush} to use (defaults to C{wx.TRANSPARENT_BRUSH})
447 @cvar FUNCTION: Logical function to use (defaults to C{wx.COPY})
448 @cvar FONT: C{wx.Font} to use (defaults to C{wx.NORMAL_FONT})
449 @cvar TEXT_FOREGROUND: C{wx.Colour} to use (defaults to C{wx.BLACK})
450 @cvar TEXT_BACKGROUND: C{wx.Colour} to use (defaults to C{wx.WHITE})
451 """
452
453 PEN = wx.BLACK_PEN
454 BRUSH = wx.TRANSPARENT_BRUSH
455 FUNCTION = wx.COPY
456 FONT = wx.NORMAL_FONT
457 TEXT_FOREGROUND = wx.BLACK
458 TEXT_BACKGROUND = wx.WHITE
459
460 - def __init__(self, view, enabled=True):
461 """
462 Create a new painter attached to the wxPython window C{view}. The
463 keyword argument C{enabled} has the same meaning as the argument to the
464 C{setEnabled()} method.
465 """
466 self.view = view
467 self.lastValue = None
468 self.enabled = enabled
469
471 """
472 Enable or disable this painter. Disabled painters do not draw their
473 values and calls to C{set()} have no effect on them.
474 """
475 oldState, self.enabled = self.enabled, state
476 if oldState and not self.enabled:
477 self.clear()
478
479 - def set(self, *value):
480 """
481 Update this painter's value and then draw it. Values may not be
482 C{None}, which is used internally to represent the absence of a current
483 value.
484 """
485 if self.enabled:
486 value = self.formatValue(value)
487 self._paint(value, None)
488
490 """
491 Redraw this painter's current value.
492 """
493 value = self.lastValue
494 self.lastValue = None
495 self._paint(value, dc)
496
497 - def clear(self, dc=None):
498 """
499 Clear the painter's current value from the screen and the painter
500 itself.
501 """
502 if self.lastValue is not None:
503 self._paint(None, dc)
504
506 """
507 Draws a previously processed C{value} on this painter's window.
508 """
509 if dc is None:
510 dc = wx.ClientDC(self.view)
511
512 dc.SetPen(self.PEN)
513 dc.SetBrush(self.BRUSH)
514 dc.SetFont(self.FONT)
515 dc.SetTextForeground(self.TEXT_FOREGROUND)
516 dc.SetTextBackground(self.TEXT_BACKGROUND)
517 dc.SetLogicalFunction(self.FUNCTION)
518 dc.BeginDrawing()
519
520 if self.lastValue is not None:
521 self.clearValue(dc, self.lastValue)
522 self.lastValue = None
523
524 if value is not None:
525 self.drawValue(dc, value)
526 self.lastValue = value
527
528 dc.EndDrawing()
529
536
538 """
539 Template method that draws a previously processed C{value} using the
540 wxPython device context C{dc}. This DC has already been configured, so
541 calls to C{BeginDrawing()} and C{EndDrawing()} may not be made.
542 """
543 pass
544
546 """
547 Template method that clears a previously processed C{value} that was
548 previously drawn, using the wxPython device context C{dc}. This DC has
549 already been configured, so calls to C{BeginDrawing()} and
550 C{EndDrawing()} may not be made.
551 """
552 pass
553
554
556 """
557 Draws a text message containing the current position of the mouse in the
558 lower left corner of the plot.
559 """
560
561 PADDING = 2
562 PEN = wx.WHITE_PEN
563 BRUSH = wx.WHITE_BRUSH
564
570
572 """
573 Returns the upper-left coordinates C{(X, Y)} for the string C{value}
574 its width and height C{(W, H)}.
575 """
576 height = dc.GetSize()[1]
577 w, h = dc.GetTextExtent(value)
578 x = self.PADDING
579 y = int(height - (h + self.PADDING))
580 return x, y, w, h
581
583 """
584 Draws the string C{value} in the lower left corner of the plot.
585 """
586 x, y, w, h = self.get_XYWH(dc, value)
587 dc.DrawText(value, x, y)
588
590 """
591 Clears the string C{value} from the lower left corner of the plot by
592 painting a white rectangle over it.
593 """
594 x, y, w, h = self.get_XYWH(dc, value)
595 dc.DrawRectangle(x, y, w, h)
596
597
599 """
600 Draws crosshairs through the current position of the mouse.
601 """
602
603 PEN = wx.WHITE_PEN
604 FUNCTION = wx.XOR
605
612
614 """
615 Draws crosshairs through the C{(X, Y)} coordinates.
616 """
617 dc.CrossHair(*value)
618
620 """
621 Clears the crosshairs drawn through the C{(X, Y)} coordinates.
622 """
623 dc.CrossHair(*value)
624
625
627 """
628 Draws a selection rubberband from one point to another.
629 """
630
631 PEN = wx.WHITE_PEN
632 FUNCTION = wx.XOR
633
646
648 """
649 Draws the selection rubberband around the rectangle
650 C{(x1, y1, x2, y2)}.
651 """
652 dc.DrawRectangle(*value)
653
655 """
656 Clears the selection rubberband around the rectangle
657 C{(x1, y1, x2, y2)}.
658 """
659 dc.DrawRectangle(*value)
660
661
663 """
664 Manages the current cursor of a wxPython window, allowing it to be switched
665 between a normal arrow and a square cross.
666 """
667 - def __init__(self, view, enabled=True):
668 """
669 Create a CursorChanger attached to the wxPython window C{view}. The
670 keyword argument C{enabled} has the same meaning as the argument to the
671 C{setEnabled()} method.
672 """
673 self.view = view
674 self.cursor = wx.CURSOR_DEFAULT
675 self.enabled = enabled
676
678 """
679 Enable or disable this cursor changer. When disabled, the cursor is
680 reset to the normal arrow and calls to the C{set()} methods have no
681 effect.
682 """
683 oldState, self.enabled = self.enabled, state
684 if oldState and not self.enabled and self.cursor != wx.CURSOR_DEFAULT:
685 self.cursor = wx.CURSOR_DEFAULT
686 self.view.SetCursor(wx.STANDARD_CURSOR)
687
689 """
690 Change the cursor of the associated window to a normal arrow.
691 """
692 if self.cursor != wx.CURSOR_DEFAULT and self.enabled:
693 self.cursor = wx.CURSOR_DEFAULT
694 self.view.SetCursor(wx.STANDARD_CURSOR)
695
697 """
698 Change the cursor of the associated window to a square cross.
699 """
700 if self.cursor != wx.CURSOR_CROSS and self.enabled:
701 self.cursor = wx.CURSOR_CROSS
702 self.view.SetCursor(wx.CROSS_CURSOR)
703
704
705
706
707
708
709
710 PS_DPI_HIGH_QUALITY = 600
711 PS_DPI_MEDIUM_QUALITY = 300
712 PS_DPI_LOW_QUALITY = 150
713 PS_DPI_DRAFT_QUALITY = 72
714
715
717 """
718 Sets the default wx.PostScriptDC resolution from a wx.PrintData's quality
719 setting.
720
721 This is a workaround for WX ignoring the quality setting and defaulting to
722 72 DPI. Unfortunately wx.Printout.GetDC() returns a wx.DC object instead
723 of the actual class, so it's impossible to set the resolution on the DC
724 itself.
725
726 Even more unforuntately, printing with libgnomeprint appears to always be
727 stuck at 72 DPI.
728 """
729 if not callable(getattr(wx, 'PostScriptDC_SetResolution', None)):
730 return
731
732 quality = printData.GetQuality()
733 if quality > 0:
734 dpi = quality
735 elif quality == wx.PRINT_QUALITY_HIGH:
736 dpi = PS_DPI_HIGH_QUALITY
737 elif quality == wx.PRINT_QUALITY_MEDIUM:
738 dpi = PS_DPI_MEDIUM_QUALITY
739 elif quality == wx.PRINT_QUALITY_LOW:
740 dpi = PS_DPI_LOW_QUALITY
741 elif quality == wx.PRINT_QUALITY_DRAFT:
742 dpi = PS_DPI_DRAFT_QUALITY
743 else:
744 dpi = PS_DPI_HIGH_QUALITY
745
746 wx.PostScriptDC_SetResolution(dpi)
747
748
824
825
988
989
990
991
992
993
994 EVT_POINT_ID = wx.NewId()
995
996
998 """
999 Register to receive wxPython C{PointEvent}s from a C{PlotPanel} or
1000 C{PlotFrame}.
1001 """
1002 win.Connect(id, -1, EVT_POINT_ID, func)
1003
1004
1006 """
1007 wxPython event emitted when a left-click-release occurs in a matplotlib
1008 axes of a window without an area selection.
1009
1010 @cvar axes: matplotlib C{Axes} which was left-clicked
1011 @cvar x: matplotlib X coordinate
1012 @cvar y: matplotlib Y coordinate
1013 @cvar xdata: axes X coordinate
1014 @cvar ydata: axes Y coordinate
1015 """
1017 """
1018 Create a new C{PointEvent} for the matplotlib coordinates C{(x, y)} of
1019 an C{axes}.
1020 """
1021 wx.PyCommandEvent.__init__(self, EVT_POINT_ID, id)
1022 self.axes = axes
1023 self.x = x
1024 self.y = y
1025 self.xdata, self.ydata = invert_point(x, y, axes.transData)
1026
1028 return PointEvent(self.GetId(), self.axes, self.x, self.y)
1029
1030
1031 EVT_SELECTION_ID = wx.NewId()
1032
1033
1035 """
1036 Register to receive wxPython C{SelectionEvent}s from a C{PlotPanel} or
1037 C{PlotFrame}.
1038 """
1039 win.Connect(id, -1, EVT_SELECTION_ID, func)
1040
1041
1043 """
1044 wxPython event emitted when an area selection occurs in a matplotlib axes
1045 of a window for which zooming has been disabled. The selection is
1046 described by a rectangle from C{(x1, y1)} to C{(x2, y2)}, of which only
1047 one point is required to be inside the axes.
1048
1049 @cvar axes: matplotlib C{Axes} which was left-clicked
1050 @cvar x1: matplotlib x1 coordinate
1051 @cvar y1: matplotlib y1 coordinate
1052 @cvar x2: matplotlib x2 coordinate
1053 @cvar y2: matplotlib y2 coordinate
1054 @cvar x1data: axes x1 coordinate
1055 @cvar y1data: axes y1 coordinate
1056 @cvar x2data: axes x2 coordinate
1057 @cvar y2data: axes y2 coordinate
1058 """
1059 - def __init__(self, id, axes, x1, y1, x2, y2):
1060 """
1061 Create a new C{SelectionEvent} for the area described by the rectangle
1062 from C{(x1, y1)} to C{(x2, y2)} in an C{axes}.
1063 """
1064 wx.PyCommandEvent.__init__(self, EVT_SELECTION_ID, id)
1065 self.axes = axes
1066 self.x1 = x1
1067 self.y1 = y1
1068 self.x2 = x2
1069 self.y2 = y2
1070 self.x1data, self.y1data = invert_point(x1, y1, axes.transData)
1071 self.x2data, self.y2data = invert_point(x2, y2, axes.transData)
1072
1074 return SelectionEvent(self.GetId(), self.axes, self.x1, self.y1,
1075 self.x2, self.y2)
1076
1077
1078
1079
1080
1081
1083 """
1084 A matplotlib canvas suitable for embedding in wxPython applications.
1085 """
1086 - def __init__(self, parent, id, size=(6.0, 3.70), dpi=96, cursor=True,
1087 location=True, crosshairs=True, selection=True, zoom=True,
1088 autoscaleUnzoom=True):
1089 """
1090 Creates a new PlotPanel window that is the child of the wxPython window
1091 C{parent} with the wxPython identifier C{id}.
1092
1093 The keyword arguments C{size} and {dpi} are used to create the
1094 matplotlib C{Figure} associated with this canvas. C{size} is the
1095 desired width and height of the figure, in inches, as the 2-tuple
1096 C{(width, height)}. C{dpi} is the dots-per-inch of the figure.
1097
1098 The keyword arguments C{cursor}, C{location}, C{crosshairs},
1099 C{selection}, C{zoom}, and C{autoscaleUnzoom} enable or disable various
1100 user interaction features that are descibed in their associated
1101 C{set()} methods.
1102 """
1103 FigureCanvasWxAgg.__init__(self, parent, id, Figure(size, dpi))
1104
1105 self.insideOnPaint = False
1106 self.cursor = CursorChanger(self, cursor)
1107 self.location = LocationPainter(self, location)
1108 self.crosshairs = CrosshairPainter(self, crosshairs)
1109 self.rubberband = RubberbandPainter(self, selection)
1110 rightClickUnzoom = True
1111 self.director = PlotPanelDirector(self, zoom, selection,
1112 rightClickUnzoom, autoscaleUnzoom)
1113
1114 self.figure.set_edgecolor('black')
1115 self.figure.set_facecolor('white')
1116 self.SetBackgroundColour(wx.WHITE)
1117
1118
1119
1120 topwin = self._get_toplevel_parent()
1121 topwin.Connect(-1, self.GetId(), wx.wxEVT_ACTIVATE, self.OnActivate)
1122
1123 wx.EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
1124 wx.EVT_WINDOW_DESTROY(self, self.OnDestroy)
1125
1127 """
1128 Returns the first toplevel parent of this window.
1129 """
1130 topwin = self.GetParent()
1131 while not isinstance(topwin, (wx.Frame, wx.Dialog)):
1132 topwin = topwin.GetParent()
1133 assert window is not None
1134 return topwin
1135
1137 """
1138 Handles the wxPython window activation event.
1139 """
1140 if not evt.GetActive():
1141 self.cursor.setNormal()
1142 self.location.clear()
1143 self.crosshairs.clear()
1144 self.rubberband.clear()
1145 evt.Skip()
1146
1148 """
1149 Overrides the wxPython backround repainting event to reduce flicker.
1150 """
1151 pass
1152
1154 """
1155 Handles the wxPython window destruction event.
1156 """
1157 if self.GetId() == evt.GetEventObject().GetId():
1158
1159 topwin = self._get_toplevel_parent()
1160 topwin.Disconnect(-1, self.GetId(), wx.wxEVT_ACTIVATE)
1161
1163 """
1164 Overrides the C{FigureCanvasWxAgg} paint event to redraw the
1165 crosshairs, etc.
1166 """
1167
1168 if not isinstance(self, FigureCanvasWxAgg):
1169 return
1170
1171 self.insideOnPaint = True
1172 FigureCanvasWxAgg._onPaint(self, evt)
1173 self.insideOnPaint = False
1174
1175 dc = wx.PaintDC(self)
1176 self.location.redraw(dc)
1177 self.crosshairs.redraw(dc)
1178 self.rubberband.redraw(dc)
1179
1185
1187 """
1188 Enable or disable the changing mouse cursor. When enabled, the cursor
1189 changes from the normal arrow to a square cross when the mouse enters a
1190 matplotlib axes on this canvas.
1191 """
1192 self.cursor.setEnabled(state)
1193
1195 """
1196 Enable or disable the display of the matplotlib axes coordinates of the
1197 mouse in the lower left corner of the canvas.
1198 """
1199 self.location.setEnabled(state)
1200
1202 """
1203 Enable or disable drawing crosshairs through the mouse cursor when it
1204 is inside a matplotlib axes.
1205 """
1206 self.crosshairs.setEnabled(state)
1207
1209 """
1210 Enable or disable area selections, where user selects a rectangular
1211 area of the canvas by left-clicking and dragging the mouse.
1212 """
1213 self.rubberband.setEnabled(state)
1214 self.director.setSelection(state)
1215
1217 """
1218 Enable or disable zooming in when the user makes an area selection and
1219 zooming out again when the user right-clicks.
1220 """
1221 self.director.setZoomEnabled(state)
1222
1224 """
1225 Enable or disable automatic view rescaling when the user zooms out to
1226 the initial figure.
1227 """
1228 self.director.setAutoscaleUnzoom(state)
1229
1231 """
1232 Returns a boolean indicating whether or not the C{axes} is zoomed in.
1233 """
1234 return self.director.zoomed(axes)
1235
1236 - def draw(self, **kwds):
1237 """
1238 Draw the associated C{Figure} onto the screen.
1239 """
1240
1241
1242 if (not self.director.canDraw()
1243 or not isinstance(self, FigureCanvasWxAgg)):
1244 return
1245
1246 if MATPLOTLIB_0_98_3:
1247 FigureCanvasWxAgg.draw(self, kwds.get('drawDC', None))
1248 else:
1249 FigureCanvasWxAgg.draw(self, kwds.get('repaint', True))
1250
1251
1252 if not self.insideOnPaint:
1253 self.location.redraw()
1254 self.crosshairs.redraw()
1255 self.rubberband.redraw()
1256
1258 """
1259 Called by the associated C{PlotPanelDirector} to emit a C{PointEvent}.
1260 """
1261 wx.PostEvent(self, PointEvent(self.GetId(), axes, x, y))
1262
1264 """
1265 Called by the associated C{PlotPanelDirector} to emit a
1266 C{SelectionEvent}.
1267 """
1268 wx.PostEvent(self, SelectionEvent(self.GetId(), axes, x1, y1, x2, y2))
1269
1271 """
1272 Returns the X and Y coordinates of a wxPython event object converted to
1273 matplotlib canavas coordinates.
1274 """
1275 return evt.GetX(), int(self.figure.bbox.height - evt.GetY())
1276
1278 """
1279 Overrides the C{FigureCanvasWxAgg} key-press event handler, dispatching
1280 the event to the associated C{PlotPanelDirector}.
1281 """
1282 self.director.keyDown(evt)
1283
1285 """
1286 Overrides the C{FigureCanvasWxAgg} key-release event handler,
1287 dispatching the event to the associated C{PlotPanelDirector}.
1288 """
1289 self.director.keyUp(evt)
1290
1298
1306
1314
1322
1324 """
1325 Overrides the C{FigureCanvasWxAgg} mouse motion event handler,
1326 dispatching the event to the associated C{PlotPanelDirector}.
1327 """
1328 x, y = self._get_canvas_xy(evt)
1329 self.director.mouseMotion(evt, x, y)
1330
1331
1332
1333
1334
1335
1337 """
1338 A matplotlib canvas embedded in a wxPython top-level window.
1339
1340 @cvar ABOUT_TITLE: Title of the "About" dialog.
1341 @cvar ABOUT_MESSAGE: Contents of the "About" dialog.
1342 """
1343
1344 ABOUT_TITLE = 'About wxmpl.PlotFrame'
1345 ABOUT_MESSAGE = ('wxmpl.PlotFrame %s\n' % __version__
1346 + 'Written by Ken McIvor <mcivor@iit.edu>\n'
1347 + 'Copyright 2005-2009 Illinois Institute of Technology')
1348
1349 - def __init__(self, parent, id, title, size=(6.0, 3.7), dpi=96, cursor=True,
1350 location=True, crosshairs=True, selection=True, zoom=True,
1351 autoscaleUnzoom=True, **kwds):
1352 """
1353 Creates a new PlotFrame top-level window that is the child of the
1354 wxPython window C{parent} with the wxPython identifier C{id} and the
1355 title of C{title}.
1356
1357 All of the named keyword arguments to this constructor have the same
1358 meaning as those arguments to the constructor of C{PlotPanel}.
1359
1360 Any additional keyword arguments are passed to the constructor of
1361 C{wx.Frame}.
1362 """
1363 wx.Frame.__init__(self, parent, id, title, **kwds)
1364 self.panel = PlotPanel(self, -1, size, dpi, cursor, location,
1365 crosshairs, selection, zoom)
1366
1367 pData = wx.PrintData()
1368 pData.SetPaperId(wx.PAPER_LETTER)
1369 if callable(getattr(pData, 'SetPrinterCommand', None)):
1370 pData.SetPrinterCommand(POSTSCRIPT_PRINTING_COMMAND)
1371 self.printer = FigurePrinter(self, pData)
1372
1373 self.create_menus()
1374 sizer = wx.BoxSizer(wx.VERTICAL)
1375 sizer.Add(self.panel, 1, wx.ALL|wx.EXPAND, 5)
1376 self.SetSizer(sizer)
1377 self.Fit()
1378
1380 mainMenu = wx.MenuBar()
1381 menu = wx.Menu()
1382
1383 id = wx.NewId()
1384 menu.Append(id, '&Save As...\tCtrl+S',
1385 'Save a copy of the current plot')
1386 wx.EVT_MENU(self, id, self.OnMenuFileSave)
1387
1388 menu.AppendSeparator()
1389
1390 if wx.Platform != '__WXMAC__':
1391 id = wx.NewId()
1392 menu.Append(id, 'Page Set&up...',
1393 'Set the size and margins of the printed figure')
1394 wx.EVT_MENU(self, id, self.OnMenuFilePageSetup)
1395
1396 id = wx.NewId()
1397 menu.Append(id, 'Print Pre&view...',
1398 'Preview the print version of the current plot')
1399 wx.EVT_MENU(self, id, self.OnMenuFilePrintPreview)
1400
1401 id = wx.NewId()
1402 menu.Append(id, '&Print...\tCtrl+P', 'Print the current plot')
1403 wx.EVT_MENU(self, id, self.OnMenuFilePrint)
1404
1405 menu.AppendSeparator()
1406
1407 id = wx.NewId()
1408 menu.Append(id, '&Close Window\tCtrl+W',
1409 'Close the current plot window')
1410 wx.EVT_MENU(self, id, self.OnMenuFileClose)
1411
1412 mainMenu.Append(menu, '&File')
1413 menu = wx.Menu()
1414
1415 id = wx.NewId()
1416 menu.Append(id, '&About...', 'Display version information')
1417 wx.EVT_MENU(self, id, self.OnMenuHelpAbout)
1418
1419 mainMenu.Append(menu, '&Help')
1420 self.SetMenuBar(mainMenu)
1421
1423 """
1424 Handles File->Save menu events.
1425 """
1426 fileName = wx.FileSelector('Save Plot', default_extension='png',
1427 wildcard=('Portable Network Graphics (*.png)|*.png|'
1428 + 'Encapsulated Postscript (*.eps)|*.eps|All files (*.*)|*.*'),
1429 parent=self, flags=wx.SAVE|wx.OVERWRITE_PROMPT)
1430
1431 if not fileName:
1432 return
1433
1434 path, ext = os.path.splitext(fileName)
1435 ext = ext[1:].lower()
1436
1437 if ext != 'png' and ext != 'eps':
1438 error_message = (
1439 'Only the PNG and EPS image formats are supported.\n'
1440 'A file extension of `png\' or `eps\' must be used.')
1441 wx.MessageBox(error_message, 'Error - plotit',
1442 parent=self, style=wx.OK|wx.ICON_ERROR)
1443 return
1444
1445 try:
1446 self.panel.print_figure(fileName)
1447 except IOError, e:
1448 if e.strerror:
1449 err = e.strerror
1450 else:
1451 err = e
1452
1453 wx.MessageBox('Could not save file: %s' % err, 'Error - plotit',
1454 parent=self, style=wx.OK|wx.ICON_ERROR)
1455
1457 """
1458 Handles File->Page Setup menu events
1459 """
1460 self.printer.pageSetup()
1461
1467
1473
1475 """
1476 Handles File->Close menu events.
1477 """
1478 self.Close()
1479
1481 """
1482 Handles Help->About menu events.
1483 """
1484 wx.MessageBox(self.ABOUT_MESSAGE, self.ABOUT_TITLE, parent=self,
1485 style=wx.OK)
1486
1492
1494 """
1495 Enable or disable the changing mouse cursor. When enabled, the cursor
1496 changes from the normal arrow to a square cross when the mouse enters a
1497 matplotlib axes on this canvas.
1498 """
1499 self.panel.set_cursor(state)
1500
1502 """
1503 Enable or disable the display of the matplotlib axes coordinates of the
1504 mouse in the lower left corner of the canvas.
1505 """
1506 self.panel.set_location(state)
1507
1509 """
1510 Enable or disable drawing crosshairs through the mouse cursor when it
1511 is inside a matplotlib axes.
1512 """
1513 self.panel.set_crosshairs(state)
1514
1516 """
1517 Enable or disable area selections, where user selects a rectangular
1518 area of the canvas by left-clicking and dragging the mouse.
1519 """
1520 self.panel.set_selection(state)
1521
1523 """
1524 Enable or disable zooming in when the user makes an area selection and
1525 zooming out again when the user right-clicks.
1526 """
1527 self.panel.set_zoom(state)
1528
1530 """
1531 Enable or disable automatic view rescaling when the user zooms out to
1532 the initial figure.
1533 """
1534 self.panel.set_autoscale_unzoom(state)
1535
1537 """
1538 Draw the associated C{Figure} onto the screen.
1539 """
1540 self.panel.draw()
1541
1542
1543
1544
1545
1546
1548 """
1549 A wxApp that provides a matplotlib canvas embedded in a wxPython top-level
1550 window, encapsulating wxPython's nuts and bolts.
1551
1552 @cvar ABOUT_TITLE: Title of the "About" dialog.
1553 @cvar ABOUT_MESSAGE: Contents of the "About" dialog.
1554 """
1555
1556 ABOUT_TITLE = None
1557 ABOUT_MESSAGE = None
1558
1559 - def __init__(self, title="WxMpl", size=(6.0, 3.7), dpi=96, cursor=True,
1560 location=True, crosshairs=True, selection=True, zoom=True, **kwds):
1561 """
1562 Creates a new PlotApp, which creates a PlotFrame top-level window.
1563
1564 The keyword argument C{title} specifies the title of this top-level
1565 window.
1566
1567 All of other the named keyword arguments to this constructor have the
1568 same meaning as those arguments to the constructor of C{PlotPanel}.
1569
1570 Any additional keyword arguments are passed to the constructor of
1571 C{wx.App}.
1572 """
1573 self.title = title
1574 self.size = size
1575 self.dpi = dpi
1576 self.cursor = cursor
1577 self.location = location
1578 self.crosshairs = crosshairs
1579 self.selection = selection
1580 self.zoom = zoom
1581 wx.App.__init__(self, **kwds)
1582
1596
1602
1604 """
1605 Enable or disable the changing mouse cursor. When enabled, the cursor
1606 changes from the normal arrow to a square cross when the mouse enters a
1607 matplotlib axes on this canvas.
1608 """
1609 self.frame.set_cursor(state)
1610
1612 """
1613 Enable or disable the display of the matplotlib axes coordinates of the
1614 mouse in the lower left corner of the canvas.
1615 """
1616 self.frame.set_location(state)
1617
1619 """
1620 Enable or disable drawing crosshairs through the mouse cursor when it
1621 is inside a matplotlib axes.
1622 """
1623 self.frame.set_crosshairs(state)
1624
1626 """
1627 Enable or disable area selections, where user selects a rectangular
1628 area of the canvas by left-clicking and dragging the mouse.
1629 """
1630 self.frame.set_selection(state)
1631
1633 """
1634 Enable or disable zooming in when the user makes an area selection and
1635 zooming out again when the user right-clicks.
1636 """
1637 self.frame.set_zoom(state)
1638
1640 """
1641 Draw the associated C{Figure} onto the screen.
1642 """
1643 self.frame.draw()
1644
1645
1646
1647
1648
1649
1651 """
1652 Manages a Numerical Python vector, automatically growing it as necessary to
1653 accomodate new entries.
1654 """
1656 self.data = Numerix.zeros((16,), Numerix.Float)
1657 self.nextRow = 0
1658
1660 """
1661 Zero and reset this buffer without releasing the underlying array.
1662 """
1663 self.data[:] = 0.0
1664 self.nextRow = 0
1665
1667 """
1668 Zero and reset this buffer, releasing the underlying array.
1669 """
1670 self.data = Numerix.zeros((16,), Numerix.Float)
1671 self.nextRow = 0
1672
1674 """
1675 Append a new entry to the end of this buffer's vector.
1676 """
1677 nextRow = self.nextRow
1678 data = self.data
1679
1680 resize = False
1681 if nextRow == data.shape[0]:
1682 nR = int(Numerix.ceil(self.data.shape[0]*1.5))
1683 resize = True
1684
1685 if resize:
1686 self.data = Numerix.zeros((nR,), Numerix.Float)
1687 self.data[0:data.shape[0]] = data
1688
1689 self.data[nextRow] = point
1690 self.nextRow += 1
1691
1693 """
1694 Returns the current vector or C{None} if the buffer contains no data.
1695 """
1696 if self.nextRow == 0:
1697 return None
1698 else:
1699 return self.data[0:self.nextRow]
1700
1701
1703 """
1704 Manages a Numerical Python matrix, automatically growing it as necessary to
1705 accomodate new rows of entries.
1706 """
1708 self.data = Numerix.zeros((16, 1), Numerix.Float)
1709 self.nextRow = 0
1710
1712 """
1713 Zero and reset this buffer without releasing the underlying array.
1714 """
1715 self.data[:, :] = 0.0
1716 self.nextRow = 0
1717
1719 """
1720 Zero and reset this buffer, releasing the underlying array.
1721 """
1722 self.data = Numerix.zeros((16, 1), Numerix.Float)
1723 self.nextRow = 0
1724
1726 """
1727 Append a new row of entries to the end of this buffer's matrix.
1728 """
1729 row = Numerix.asarray(row, Numerix.Float)
1730 nextRow = self.nextRow
1731 data = self.data
1732 nPts = row.shape[0]
1733
1734 if nPts == 0:
1735 return
1736
1737 resize = True
1738 if nextRow == data.shape[0]:
1739 nC = data.shape[1]
1740 nR = int(Numerix.ceil(self.data.shape[0]*1.5))
1741 if nC < nPts:
1742 nC = nPts
1743 elif data.shape[1] < nPts:
1744 nR = data.shape[0]
1745 nC = nPts
1746 else:
1747 resize = False
1748
1749 if resize:
1750 self.data = Numerix.zeros((nR, nC), Numerix.Float)
1751 rowEnd, colEnd = data.shape
1752 self.data[0:rowEnd, 0:colEnd] = data
1753
1754 self.data[nextRow, 0:nPts] = row
1755 self.nextRow += 1
1756
1758 """
1759 Returns the current matrix or C{None} if the buffer contains no data.
1760 """
1761 if self.nextRow == 0:
1762 return None
1763 else:
1764 return self.data[0:self.nextRow, :]
1765
1766
1767
1768
1769
1770
1772 """
1773 Returns a C{Bbox} describing the range of difference between two sets of X
1774 and Y coordinates.
1775 """
1776 return make_bbox(get_delta(X1, X2), get_delta(Y1, Y2))
1777
1778
1780 """
1781 Returns the vector of contiguous, different points between two vectors.
1782 """
1783 n1 = X1.shape[0]
1784 n2 = X2.shape[0]
1785
1786 if n1 < n2:
1787 return X2[n1:]
1788 elif n1 == n2:
1789
1790
1791 return X2
1792 else:
1793 return X2
1794
1795
1797 """
1798 Returns a C{Bbox} that contains the supplied sets of X and Y coordinates.
1799 """
1800 if X is None or X.shape[0] == 0:
1801 x1 = x2 = 0.0
1802 else:
1803 x1 = min(X)
1804 x2 = max(X)
1805
1806 if Y is None or Y.shape[0] == 0:
1807 y1 = y2 = 0.0
1808 else:
1809 y1 = min(Y)
1810 y2 = max(Y)
1811
1812 return Bbox.from_extents(x1, y1, x2, y2)
1813
1814
1815
1816
1817
1818
1820 """
1821 Plots and updates lines on a matplotlib C{Axes}.
1822 """
1824 """
1825 Create a new C{StripCharter} associated with a matplotlib C{axes}.
1826 """
1827 self.axes = axes
1828 self.channels = []
1829 self.lines = {}
1830
1832 """
1833 Specify the data-providers of the lines to be plotted and updated.
1834 """
1835 self.lines = None
1836 self.channels = channels[:]
1837
1838
1839 self.axes.legend_ = None
1840 self.axes.lines = []
1841
1843 """
1844 Redraw the associated axes with updated lines if any of the channels'
1845 data has changed.
1846 """
1847 axes = self.axes
1848 figureCanvas = axes.figure.canvas
1849
1850 zoomed = figureCanvas.zoomed(axes)
1851
1852 redraw = False
1853 if self.lines is None:
1854 self._create_plot()
1855 redraw = True
1856 else:
1857 for channel in self.channels:
1858 redraw = self._update_channel(channel, zoomed) or redraw
1859
1860 if redraw:
1861 if not zoomed:
1862 axes.autoscale_view()
1863 figureCanvas.draw()
1864
1866 """
1867 Initially plot the lines corresponding to the data-providers.
1868 """
1869 self.lines = {}
1870 axes = self.axes
1871 styleGen = _process_plot_var_args(axes)
1872
1873 for channel in self.channels:
1874 self._plot_channel(channel, styleGen)
1875
1876 if self.channels:
1877 lines = [self.lines[x] for x in self.channels]
1878 labels = [x.get_label() for x in lines]
1879 self.axes.legend(lines, labels, numpoints=2,
1880 prop=FontProperties(size='x-small'))
1881
1883 """
1884 Initially plot a line corresponding to one of the data-providers.
1885 """
1886 empty = False
1887 x = channel.getX()
1888 y = channel.getY()
1889 if x is None or y is None:
1890 x = y = []
1891 empty = True
1892
1893 line = styleGen(x, y).next()
1894 line._wxmpl_empty_line = empty
1895
1896 if channel.getColor() is not None:
1897 line.set_color(channel.getColor())
1898 if channel.getStyle() is not None:
1899 line.set_linestyle(channel.getStyle())
1900 if channel.getMarker() is not None:
1901 line.set_marker(channel.getMarker())
1902 line.set_markeredgecolor(line.get_color())
1903 line.set_markerfacecolor(line.get_color())
1904
1905 line.set_label(channel.getLabel())
1906 self.lines[channel] = line
1907 if not empty:
1908 self.axes.add_line(line)
1909
1911 """
1912 Replot a line corresponding to one of the data-providers if the data
1913 has changed.
1914 """
1915 if channel.hasChanged():
1916 channel.setChanged(False)
1917 else:
1918 return False
1919
1920 axes = self.axes
1921 line = self.lines[channel]
1922 newX = channel.getX()
1923 newY = channel.getY()
1924
1925 if newX is None or newY is None:
1926 return False
1927
1928 oldX = line._x
1929 oldY = line._y
1930
1931 x, y = newX, newY
1932 line.set_data(x, y)
1933
1934 if line._wxmpl_empty_line:
1935 axes.add_line(line)
1936 line._wxmpl_empty_line = False
1937 else:
1938 if line.get_transform() != axes.transData:
1939 xys = axes._get_verts_in_data_coords(
1940 line.get_transform(), zip(x, y))
1941 else:
1942 xys = Numerix.zeros((x.shape[0], 2), Numerix.Float)
1943 xys[:,0] = x
1944 xys[:,1] = y
1945 axes.update_datalim(xys)
1946
1947 if zoomed:
1948 return axes.viewLim.overlaps(
1949 make_delta_bbox(oldX, oldY, newX, newY))
1950 else:
1951 return True
1952
1953
1954
1955
1956
1957
1959 """
1960 Provides data for a C{StripCharter} to plot. Subclasses of C{Channel}
1961 override the template methods C{getX()} and C{getY()} to provide plot data
1962 and call C{setChanged(True)} when that data has changed.
1963 """
1964 - def __init__(self, name, color=None, style=None, marker=None):
1965 """
1966 Creates a new C{Channel} with the matplotlib label C{name}. The
1967 keyword arguments specify the strings for the line color, style, and
1968 marker to use when the line is plotted.
1969 """
1970 self.name = name
1971 self.color = color
1972 self.style = style
1973 self.marker = marker
1974 self.changed = False
1975
1977 """
1978 Returns the matplotlib label for this channel of data.
1979 """
1980 return self.name
1981
1983 """
1984 Returns the line color string to use when the line is plotted, or
1985 C{None} to use an automatically generated color.
1986 """
1987 return self.color
1988
1990 """
1991 Returns the line style string to use when the line is plotted, or
1992 C{None} to use the default line style.
1993 """
1994 return self.style
1995
1997 """
1998 Returns the line marker string to use when the line is plotted, or
1999 C{None} to use the default line marker.
2000 """
2001 return self.marker
2002
2004 """
2005 Returns a boolean indicating if the line data has changed.
2006 """
2007 return self.changed
2008
2010 """
2011 Sets the change indicator to the boolean value C{changed}.
2012
2013 @note: C{StripCharter} instances call this method after detecting a
2014 change, so a C{Channel} cannot be shared among multiple charts.
2015 """
2016 self.changed = changed
2017
2019 """
2020 Template method that returns the vector of X axis data or C{None} if
2021 there is no data available.
2022 """
2023 return None
2024
2026 """
2027 Template method that returns the vector of Y axis data or C{None} if
2028 there is no data available.
2029 """
2030 return None
2031