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
    1451
    1457
    1463
    1469
    1475
    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