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