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