Module wxmpl
[frames] | no frames]

Source Code for Module wxmpl

   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 numpy as NumPy 
  25  from matplotlib.axes import _process_plot_var_args 
  26  from matplotlib.backend_bases import FigureCanvasBase 
  27  from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg 
  28  from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg 
  29  from matplotlib.figure import Figure 
  30  from matplotlib.font_manager import FontProperties 
  31  from matplotlib.projections.polar import PolarAxes 
  32  from matplotlib.transforms import Bbox 
  33   
  34  __version__ = '2.0dev' 
  35   
  36  __all__ = ['PlotPanel', 'PlotFrame', 'PlotApp', 'StripCharter', 'Channel', 
  37      'FigurePrinter', 'PointEvent', 'EVT_POINT', 'SelectionEvent', 
  38      'EVT_SELECTION'] 
  39   
  40  # 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 = NumPy.zeros((16,), NumPy.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 = NumPy.zeros((16,), NumPy.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(NumPy.ceil(self.data.shape[0]*1.5)) 1679 resize = True 1680 1681 if resize: 1682 self.data = NumPy.zeros((nR,), NumPy.Float) 1683 self.data[0:data.shape[0]] = data 1684 1685 self.data[nextRow] = point 1686 self.nextRow += 1
1687
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 = NumPy.zeros((16, 1), NumPy.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 = NumPy.zeros((16, 1), NumPy.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 = NumPy.asarray(row, NumPy.Float) 1726 nextRow = self.nextRow 1727 data = self.data 1728 nPts = row.shape[0] 1729 1730 if nPts == 0: 1731 return 1732 1733 resize = True 1734 if nextRow == data.shape[0]: 1735 nC = data.shape[1] 1736 nR = int(NumPy.ceil(self.data.shape[0]*1.5)) 1737 if nC < nPts: 1738 nC = nPts 1739 elif data.shape[1] < nPts: 1740 nR = data.shape[0] 1741 nC = nPts 1742 else: 1743 resize = False 1744 1745 if resize: 1746 self.data = NumPy.zeros((nR, nC), NumPy.Float) 1747 rowEnd, colEnd = data.shape 1748 self.data[0:rowEnd, 0:colEnd] = data 1749 1750 self.data[nextRow, 0:nPts] = row 1751 self.nextRow += 1
1752
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 = NumPy.zeros((x.shape[0], 2), NumPy.Float) 1939 xys[:,0] = x 1940 xys[:,1] = y 1941 axes.update_datalim(xys) 1942 1943 if zoomed: 1944 return axes.viewLim.overlaps( 1945 make_delta_bbox(oldX, oldY, newX, newY)) 1946 else: 1947 return True
1948 1949 1950 # 1951 # 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