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