]> git.saurik.com Git - wxWidgets.git/commitdiff
Added PyPlot
authorRobin Dunn <robin@alldunn.com>
Wed, 17 Dec 2003 03:25:56 +0000 (03:25 +0000)
committerRobin Dunn <robin@alldunn.com>
Wed, 17 Dec 2003 03:25:56 +0000 (03:25 +0000)
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24890 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

wxPython/demo/PyPlot.py [new file with mode: 0644]
wxPython/docs/CHANGES.txt
wxPython/wx/lib/plot.py [new file with mode: 0644]

diff --git a/wxPython/demo/PyPlot.py b/wxPython/demo/PyPlot.py
new file mode 100644 (file)
index 0000000..3f00424
--- /dev/null
@@ -0,0 +1,114 @@
+# 11/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o New demo
+#
+
+import  wx
+import  wx.lib.plot
+
+################################################################\
+# Where's the code???                                           |
+#                                                               |
+# wx.lib.plot.py came with its own excellent demo built in,     |
+# for testing purposes, but it serves quite well to demonstrate |
+# the code and classes within, so we are simply borrowing that  |
+# code for the demo. Please load up wx.py.plot.py for a review  |
+# of the code itself. The demo/test is at the bottom of         |
+# the file, as expected.                                        |
+################################################################/
+
+#----------------------------------------------------------------------
+
+def runTest(frame, nb, log):
+    # Loading the demo class directly from the library's
+    # test code.
+    win = wx.lib.plot.TestFrame(nb, -1, "PlotCanvas Demo")
+    frame.otherWin = win
+    return None
+
+#----------------------------------------------------------------------
+
+overview = """\
+<html><body>
+<center><h2>PyPlot</h2></center>
+
+This demo illustrates the features of the new PyPlot modules, found
+in wx.lib.plot.py. All methods and functions are documented clearly
+therein; only the overview is included here.
+
+<p>
+PyPlot is an improvement over wxPlotCanvas, which is now deprecated.
+If you are using wxPlotCanvas now, please make plans to migrate in
+anticipation of the time that wxPlotCanvas disappears completely.
+
+<p>
+The demo illustrates four different plot styles (with appropriate
+variations on fonts, etc, to show how flexible it is) as well as
+provides you with a means to tinker with all the features that
+come with the class itself.
+
+<p><ul>
+<li>File Menu
+
+    <ul>
+    <li>Page Setup
+
+        This allows you to set up how the plot will be printed. This
+        is built into the library itself.
+       
+    <li>Print Preview
+
+        As you might expect, this allows you to preview how the plot
+        will look when printed, in light of the page setup you may
+        have selected above.
+        
+    <li>Print
+    
+        Suprise! It prints the current plot to your printer! :-)
+        
+    <li>Save Plot
+    
+        That's right, the library even provides you with the means
+        to export the plotted data out to a graphics file. Several
+        formats are allowed for, basically any image class that
+        supports saving.
+    </ul>
+    
+<li>Plot Menu
+
+    <ul>
+    <li>Plot 1 ... Plot 5
+     
+        Different data with different plot formats, including one empty
+        plot.
+         
+    <li>Enable Zoom
+    
+        If Zoom is enabled, you can rubber-band select an area of the
+        plot to examine it in detail using the left mouse button. Right
+        mouse button zooms back out. This is automatically supported
+        by the library, all you have to do is turn it on.
+        
+    <li>Enable Grid
+    
+        Plots can have different styles of grids, and and these grids can
+        be turned on or off as needed.
+        
+    <li>Enable Legend
+    
+        Plot can have legends or not, the contents which are definable 
+        by you.
+    </ul>
+</ul>
+        
+<HR><pre>
+%s</pre>
+</body></html>
+""" % wx.lib.plot.__doc__
+
+
+if __name__ == '__main__':
+    import sys,os
+    import run
+    run.main(['', os.path.basename(sys.argv[0])])
+
index 08d95413da39f248bd94d468aa2d685fe8b0c93d..0ff773b52a1de867cdc9b901da1b6d42b95cf54e 100644 (file)
@@ -16,13 +16,14 @@ Added wxMirrorDC.
 
 Added wxIconLocation
 
 
 Added wxIconLocation
 
-
 Added Python wrappers for the new wxVScrolledWindow, wxVListBox, and
 wxHtmlListBox classes.
 
 Added wrappers for wxBookCtrl and wxListbook.  wxNotebook now derives
 from wxBookCtrl.
 
 Added Python wrappers for the new wxVScrolledWindow, wxVListBox, and
 wxHtmlListBox classes.
 
 Added wrappers for wxBookCtrl and wxListbook.  wxNotebook now derives
 from wxBookCtrl.
 
+Added Gordon Williams' PyPlot module to the library, available as the
+wx.lib.plot module.
 
 
 
 
 
 
diff --git a/wxPython/wx/lib/plot.py b/wxPython/wx/lib/plot.py
new file mode 100644 (file)
index 0000000..416fc55
--- /dev/null
@@ -0,0 +1,1486 @@
+#-----------------------------------------------------------------------------
+# Name:        wx.lib.plot.py
+# Purpose:     
+#
+# Author:      Gordon Williams
+#
+# Created:     2003/11/03
+# RCS-ID:      $Id$
+# Copyright:   (c) 2002
+# Licence:     Use as you wish.
+#-----------------------------------------------------------------------------
+# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o 2.5 compatability update.
+# o Renamed to plot.py in the wx.lib directory.
+# o Reworked test frame to work with wx demo framework. This saves a bit
+#   of tedious cut and paste, and the test app is excellent.
+#
+
+"""
+This is a simple light weight plotting module that can be used with
+Boa or easily integrated into your own wxPython application.  The
+emphasis is on small size and fast plotting for large data sets.  It
+has a reasonable number of features to do line and scatter graphs
+easily.  It is not as sophisticated or as powerful as SciPy Plt or
+Chaco.  Both of these are great packages but consume huge amounts of
+computer resources for simple plots.  They can be found at
+http://scipy.com
+
+This file contains two parts; first the re-usable library stuff, then,
+after a "if __name__=='__main__'" test, a simple frame and a few default
+plots for examples and testing.
+
+Based on wxPlotCanvas
+Written by K.Hinsen, R. Srinivasan;
+Ported to wxPython Harm van der Heijden, feb 1999
+
+Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
+    -More style options
+    -Zooming using mouse "rubber band"
+    -Scroll left, right
+    -Grid(graticule)
+    -Printing, preview, and page set up (margins)
+    -Axis and title labels
+    -Cursor xy axis values
+    -Doc strings and lots of comments
+    -Optimizations for large number of points
+    -Legends
+    
+Did a lot of work here to speed markers up. Only a factor of 4
+improvement though. Lines are much faster than markers, especially
+filled markers.  Stay away from circles and triangles unless you
+only have a few thousand points.
+
+Times for 25,000 points
+Line - 0.078 sec
+Markers
+Square -                   0.22 sec
+dot -                      0.10
+circle -                   0.87
+cross,plus -               0.28
+triangle, triangle_down -  0.90
+
+Thanks to Chris Barker for getting this version working on Linux.
+
+Zooming controls with mouse (when enabled):
+    Left mouse drag - Zoom box.
+    Left mouse double click - reset zoom.
+    Right mouse click - zoom out centred on click location.
+"""
+
+import  string
+import  time
+import  wx
+
+# Needs Numeric
+try:
+    import Numeric
+except:
+    try:
+        import numarray as Numeric  #if numarray is used it is renamed Numeric
+    except:
+        msg= """
+        This module requires the Numeric or numarray module,
+        which could not be imported.  It probably is not installed
+        (it's not part of the standard Python distribution). See the
+        Python site (http://www.python.org) for information on
+        downloading source or binaries."""
+        raise ImportError, "Numeric or numarray not found. \n" + msg
+
+
+
+#
+# Plotting classes...
+#
+class PolyPoints:
+    """Base Class for lines and markers
+        - All methods are private.
+    """
+
+    def __init__(self, points, attr):
+        self.points = Numeric.array(points)
+        self.currentScale= (1,1)
+        self.currentShift= (0,0)
+        self.scaled = self.points
+        self.attributes = {}
+        self.attributes.update(self._attributes)
+        for name, value in attr.items():   
+            if name not in self._attributes.keys():
+                raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
+            self.attributes[name] = value
+        
+    def boundingBox(self):
+        if len(self.points) == 0:
+            # no curves to draw
+            # defaults to (-1,-1) and (1,1) but axis can be set in Draw
+            minXY= Numeric.array([-1,-1])
+            maxXY= Numeric.array([ 1, 1])
+        else:
+            minXY= Numeric.minimum.reduce(self.points)
+            maxXY= Numeric.maximum.reduce(self.points)
+        return minXY, maxXY
+
+    def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+        if len(self.points) == 0:
+            # no curves to draw
+            return
+        if (scale is not self.currentScale) or (shift is not self.currentShift):
+            # update point scaling
+            self.scaled = scale*self.points+shift
+            self.currentScale= scale
+            self.currentShift= shift
+        # else unchanged use the current scaling
+        
+    def getLegend(self):
+        return self.attributes['legend']
+
+
+class PolyLine(PolyPoints):
+    """Class to define line type and style
+        - All methods except __init__ are private.
+    """
+    
+    _attributes = {'colour': 'black',
+                   'width': 1,
+                   'style': wx.SOLID,
+                   'legend': ''}
+
+    def __init__(self, points, **attr):
+        """Creates PolyLine object
+            points - sequence (array, tuple or list) of (x,y) points making up line
+            **attr - key word attributes
+                Defaults:
+                    'colour'= 'black',          - wx.Pen Colour any wx.NamedColour
+                    'width'= 1,                 - Pen width
+                    'style'= wx.SOLID,          - wx.Pen style
+                    'legend'= ''                - Line Legend to display
+        """
+        PolyPoints.__init__(self, points, attr)
+
+    def draw(self, dc, printerScale, coord= None):
+        colour = self.attributes['colour']
+        width = self.attributes['width'] * printerScale
+        style= self.attributes['style']
+        dc.SetPen(wx.Pen(wx.NamedColour(colour), int(width), style))
+        if coord == None:
+            dc.DrawLines(self.scaled)
+        else:
+            dc.DrawLines(coord) # draw legend line
+
+    def getSymExtent(self, printerScale):
+        """Width and Height of Marker"""
+        h= self.attributes['width'] * printerScale
+        w= 5 * h
+        return (w,h)
+
+
+class PolyMarker(PolyPoints):
+    """Class to define marker type and style
+        - All methods except __init__ are private.
+    """
+  
+    _attributes = {'colour': 'black',
+                   'width': 1,
+                   'size': 2,
+                   'fillcolour': None,
+                   'fillstyle': wx.SOLID,
+                   'marker': 'circle',
+                   'legend': ''}
+
+    def __init__(self, points, **attr):
+        """Creates PolyMarker object
+        points - sequence (array, tuple or list) of (x,y) points
+        **attr - key word attributes
+            Defaults:
+                'colour'= 'black',          - wx.Pen Colour any wx.NamedColour
+                'width'= 1,                 - Pen width
+                'size'= 2,                  - Marker size
+                'fillcolour'= same as colour,      - wx.Brush Colour any wx.NamedColour
+                'fillstyle'= wx.SOLID,      - wx.Brush fill style (use wx.TRANSPARENT for no fill)
+                'marker'= 'circle'          - Marker shape
+                'legend'= ''                - Marker Legend to display
+              
+            Marker Shapes:
+                - 'circle'
+                - 'dot'
+                - 'square'
+                - 'triangle'
+                - 'triangle_down'
+                - 'cross'
+                - 'plus'
+        """
+      
+        PolyPoints.__init__(self, points, attr)
+
+    def draw(self, dc, printerScale, coord= None):
+        colour = self.attributes['colour']
+        width = self.attributes['width'] * printerScale
+        size = self.attributes['size'] * printerScale
+        fillcolour = self.attributes['fillcolour']
+        fillstyle = self.attributes['fillstyle']
+        marker = self.attributes['marker']
+
+        dc.SetPen(wx.Pen(wx.NamedColour(colour),int(width)))
+        if fillcolour:
+            dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
+        else:
+            dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
+        if coord == None:
+            self._drawmarkers(dc, self.scaled, marker, size)
+        else:
+            self._drawmarkers(dc, coord, marker, size) # draw legend marker
+
+    def getSymExtent(self, printerScale):
+        """Width and Height of Marker"""
+        s= 5*self.attributes['size'] * printerScale
+        return (s,s)
+
+    def _drawmarkers(self, dc, coords, marker,size=1):
+        f = eval('self._' +marker)
+        f(dc, coords, size)
+
+    def _circle(self, dc, coords, size=1):
+        fact= 2.5*size
+        wh= 5.0*size
+        rect= Numeric.zeros((len(coords),4),Numeric.Float)+[0.0,0.0,wh,wh]
+        rect[:,0:2]= coords-[fact,fact]
+        dc.DrawEllipseList(rect.astype(Numeric.Int32))
+
+    def _dot(self, dc, coords, size=1):
+        dc.DrawPointList(coords)
+
+    def _square(self, dc, coords, size=1):
+        fact= 2.5*size
+        wh= 5.0*size
+        rect= Numeric.zeros((len(coords),4),Numeric.Float)+[0.0,0.0,wh,wh]
+        rect[:,0:2]= coords-[fact,fact]
+        dc.DrawRectangleList(rect.astype(Numeric.Int32))
+
+    def _triangle(self, dc, coords, size=1):
+        shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
+        poly= Numeric.repeat(coords,3)
+        poly.shape= (len(coords),3,2)
+        poly += shape
+        dc.DrawPolygonList(poly.astype(Numeric.Int32))
+
+    def _triangle_down(self, dc, coords, size=1):
+        shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
+        poly= Numeric.repeat(coords,3)
+        poly.shape= (len(coords),3,2)
+        poly += shape
+        dc.DrawPolygonList(poly.astype(Numeric.Int32))
+      
+    def _cross(self, dc, coords, size=1):
+        fact= 2.5*size
+        for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
+            lines= Numeric.concatenate((coords,coords),axis=1)+f
+            dc.DrawLineList(lines.astype(Numeric.Int32))
+
+    def _plus(self, dc, coords, size=1):
+        fact= 2.5*size
+        for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
+            lines= Numeric.concatenate((coords,coords),axis=1)+f
+            dc.DrawLineList(lines.astype(Numeric.Int32))
+
+class PlotGraphics:
+    """Container to hold PolyXXX objects and graph labels
+        - All methods except __init__ are private.
+    """
+
+    def __init__(self, objects, title='', xLabel='', yLabel= ''):
+        """Creates PlotGraphics object
+        objects - list of PolyXXX objects to make graph
+        title - title shown at top of graph
+        xLabel - label shown on x-axis
+        yLabel - label shown on y-axis
+        """
+        if type(objects) not in [list,tuple]:
+            raise TypeError, "objects argument should be list or tuple"
+        self.objects = objects
+        self.title= title
+        self.xLabel= xLabel
+        self.yLabel= yLabel
+
+    def boundingBox(self):
+        p1, p2 = self.objects[0].boundingBox()
+        for o in self.objects[1:]:
+            p1o, p2o = o.boundingBox()
+            p1 = Numeric.minimum(p1, p1o)
+            p2 = Numeric.maximum(p2, p2o)
+        return p1, p2
+
+    def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+        for o in self.objects:
+            o.scaleAndShift(scale, shift)
+
+    def setPrinterScale(self, scale):
+        """Thickens up lines and markers only for printing"""
+        self.printerScale= scale
+
+    def setXLabel(self, xLabel= ''):
+        """Set the X axis label on the graph"""
+        self.xLabel= xLabel
+
+    def setYLabel(self, yLabel= ''):
+        """Set the Y axis label on the graph"""
+        self.yLabel= yLabel
+        
+    def setTitle(self, title= ''):
+        """Set the title at the top of graph"""
+        self.title= title
+
+    def getXLabel(self):
+        """Get x axis label string"""
+        return self.xLabel
+
+    def getYLabel(self):
+        """Get y axis label string"""
+        return self.yLabel
+
+    def getTitle(self, title= ''):
+        """Get the title at the top of graph"""
+        return self.title
+
+    def draw(self, dc):
+        for o in self.objects:
+            #t=time.clock()          # profile info
+            o.draw(dc, self.printerScale)
+            #dt= time.clock()-t
+            #print o, "time=", dt
+
+    def getSymExtent(self, printerScale):
+        """Get max width and height of lines and markers symbols for legend"""
+        symExt = self.objects[0].getSymExtent(printerScale)
+        for o in self.objects[1:]:
+            oSymExt = o.getSymExtent(printerScale)
+            symExt = Numeric.maximum(symExt, oSymExt)
+        return symExt
+    
+    def getLegendNames(self):
+        """Returns list of legend names"""
+        lst = [None]*len(self)
+        for i in range(len(self)):
+            lst[i]= self.objects[i].getLegend()
+        return lst
+            
+    def __len__(self):
+        return len(self.objects)
+
+    def __getitem__(self, item):
+        return self.objects[item]
+
+
+#-------------------------------------------------------------------------------
+# Main window that you will want to import into your application.
+
+class PlotCanvas(wx.Window):
+    """Subclass of a wx.Window to allow simple general plotting
+    of data with zoom, labels, and automatic axis scaling."""
+
+    def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
+            size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
+        """Constucts a window, which can be a child of a frame, dialog or
+        any other non-control window"""
+    
+        wx.Window.__init__(self, parent, id, pos, size, style, name)
+        self.border = (1,1)
+
+        self.SetBackgroundColour("white")
+        
+        self.Bind(wx.EVT_PAINT, self.OnPaint)
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+        
+        # Create some mouse events for zooming
+        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+        self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
+        self.Bind(wx.EVT_MOTION, self.OnMotion)
+        self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
+        self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
+
+        # set curser as cross-hairs
+        self.SetCursor(wx.CROSS_CURSOR)
+
+        # Things for printing
+        self.print_data = wx.PrintData()
+        self.print_data.SetPaperId(wx.PAPER_LETTER)
+        self.print_data.SetOrientation(wx.LANDSCAPE)
+        self.pageSetupData= wx.PageSetupDialogData()
+        self.pageSetupData.SetMarginBottomRight((25,25))
+        self.pageSetupData.SetMarginTopLeft((25,25))
+        self.pageSetupData.SetPrintData(self.print_data)
+        self.printerScale = 1
+        self.parent= parent
+
+        # Zooming variables
+        self._zoomInFactor =  0.5
+        self._zoomOutFactor = 2
+        self._zoomCorner1= Numeric.array([0.0, 0.0]) # left mouse down corner
+        self._zoomCorner2= Numeric.array([0.0, 0.0])   # left mouse up corner
+        self._zoomEnabled= False
+        self._hasDragged= False
+        
+        # Drawing Variables
+        self.last_draw = None
+        self._pointScale= 1
+        self._pointShift= 0
+        self._xSpec= 'auto'
+        self._ySpec= 'auto'
+        self._gridEnabled= False
+        self._legendEnabled= False
+        
+        # Fonts
+        self._fontCache = {}
+        self._fontSizeAxis= 10
+        self._fontSizeTitle= 15
+        self._fontSizeLegend= 7
+
+        # OnSize called to make sure the buffer is initialized.
+        # This might result in OnSize getting called twice on some
+        # platforms at initialization, but little harm done.
+        self.OnSize(None) # sets the initial size based on client size
+
+        
+    # SaveFile
+    def SaveFile(self, fileName= ''):
+        """Saves the file to the type specified in the extension. If no file
+        name is specified a dialog box is provided.  Returns True if sucessful,
+        otherwise False.
+        
+        .bmp  Save a Windows bitmap file.
+        .xbm  Save an X bitmap file.
+        .xpm  Save an XPM bitmap file.
+        .png  Save a Portable Network Graphics file.
+        .jpg  Save a Joint Photographic Experts Group file.
+        """
+        if string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+            dlg1 = wx.FileDialog(
+                    self, 
+                    "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
+                    "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
+                    wx.SAVE|wx.OVERWRITE_PROMPT
+                    )
+            try:
+                while 1:
+                    if dlg1.ShowModal() == wx.ID_OK:
+                        fileName = dlg1.GetPath()
+                        # Check for proper exension
+                        if string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+                            dlg2 = wx.MessageDialog(self, 'File name extension\n'
+                            'must be one of\n'
+                            'bmp, xbm, xpm, png, or jpg',
+                              'File Name Error', wx.OK | wx.ICON_ERROR)
+                            try:
+                                dlg2.ShowModal()
+                            finally:
+                                dlg2.Destroy()
+                        else:
+                            break # now save file
+                    else: # exit without saving
+                        return False
+            finally:
+                dlg1.Destroy()
+
+        # File name has required extension
+        fType = string.lower(fileName[-3:])
+        if fType == "bmp":
+            tp= wx.BITMAP_TYPE_BMP       # Save a Windows bitmap file.
+        elif fType == "xbm":
+            tp= wx.BITMAP_TYPE_XBM       # Save an X bitmap file.
+        elif fType == "xpm":
+            tp= wx.BITMAP_TYPE_XPM       # Save an XPM bitmap file.
+        elif fType == "jpg":
+            tp= wx.BITMAP_TYPE_JPEG      # Save a JPG file.
+        else:
+            tp= wx.BITMAP_TYPE_PNG       # Save a PNG file.
+        # Save Bitmap
+        res= self._Buffer.SaveFile(fileName, tp)
+        return res
+
+    def PageSetup(self):
+        """Brings up the page setup dialog"""
+        data = self.pageSetupData
+        data.SetPrintData(self.print_data)
+        dlg = wx.PageSetupDialog(self.parent, data)
+        try:
+            if dlg.ShowModal() == wx.ID_OK:
+                data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
+                # updates page parameters from dialog
+                self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
+                self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
+                self.pageSetupData.SetPrintData(data.GetPrintData())
+                self.print_data=data.GetPrintData() # updates print_data
+        finally:
+            dlg.Destroy()
+                
+    def Printout(self, paper=None):
+        """Print current plot."""
+        if paper != None:
+            self.print_data.SetPaperId(paper)
+        pdd = wx.PrintDialogData()
+        pdd.SetPrintData(self.print_data)
+        printer = wx.Printer(pdd)
+        out = PlotPrintout(self)
+        print_ok = printer.Print(self.parent, out)
+        if print_ok:
+            self.print_data = printer.GetPrintDialogData().GetPrintData()
+        out.Destroy()
+
+    def PrintPreview(self):
+        """Print-preview current plot."""
+        printout = PlotPrintout(self)
+        printout2 = PlotPrintout(self)
+        self.preview = wx.PrintPreview(printout, printout2, self.print_data)
+        if not self.preview.Ok():
+            wx.MessageDialog(self, "Print Preview failed.\n" \
+                               "Check that default printer is configured\n", \
+                               "Print error", wx.OK|wx.CENTRE).ShowModal()
+        self.preview.SetZoom(30)
+        # search up tree to find frame instance
+        frameInst= self
+        while not isinstance(frameInst, wx.Frame):
+            frameInst= frameInst.GetParent()
+        frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
+        frame.Initialize()
+        frame.SetPosition(self.GetPosition())
+        frame.SetSize((500,400))
+        frame.Centre(wx.BOTH)
+        frame.Show(True)
+
+    def SetFontSizeAxis(self, point= 10):
+        """Set the tick and axis label font size (default is 10 point)"""
+        self._fontSizeAxis= point
+        
+    def GetFontSizeAxis(self):
+        """Get current tick and axis label font size in points"""
+        return self._fontSizeAxis
+    
+    def SetFontSizeTitle(self, point= 15):
+        """Set Title font size (default is 15 point)"""
+        self._fontSizeTitle= point
+
+    def GetFontSizeTitle(self):
+        """Get current Title font size in points"""
+        return self._fontSizeTitle
+    
+    def SetFontSizeLegend(self, point= 7):
+        """Set Legend font size (default is 7 point)"""
+        self._fontSizeLegend= point
+        
+    def GetFontSizeLegend(self):
+        """Get current Legend font size in points"""
+        return self._fontSizeLegend
+
+    def SetEnableZoom(self, value):
+        """Set True to enable zooming."""
+        if value not in [True,False]:
+            raise TypeError, "Value should be True or False"
+        self._zoomEnabled= value
+
+    def GetEnableZoom(self):
+        """True if zooming enabled."""
+        return self._zoomEnabled
+
+    def SetEnableGrid(self, value):
+        """Set True to enable grid."""
+        if value not in [True,False]:
+            raise TypeError, "Value should be True or False"
+        self._gridEnabled= value
+        self.Redraw()
+
+    def GetEnableGrid(self):
+        """True if grid enabled."""
+        return self._gridEnabled
+
+    def SetEnableLegend(self, value):
+        """Set True to enable legend."""
+        if value not in [True,False]:
+            raise TypeError, "Value should be True or False"
+        self._legendEnabled= value 
+        self.Redraw()
+
+    def GetEnableLegend(self):
+        """True if Legend enabled."""
+        return self._legendEnabled
+
+    def Reset(self):
+        """Unzoom the plot."""
+        if self.last_draw is not None:
+            self.Draw(self.last_draw[0])
+        
+    def ScrollRight(self, units):          
+        """Move view right number of axis units."""
+        if self.last_draw is not None:
+            graphics, xAxis, yAxis= self.last_draw
+            xAxis= (xAxis[0]+units, xAxis[1]+units)
+            self.Draw(graphics,xAxis,yAxis)
+
+    def ScrollUp(self, units):
+        """Move view up number of axis units."""
+        if self.last_draw is not None:
+            graphics, xAxis, yAxis= self.last_draw
+            yAxis= (yAxis[0]+units, yAxis[1]+units)
+            self.Draw(graphics,xAxis,yAxis)
+        
+    def GetXY(self,event):
+        """Takes a mouse event and returns the XY user axis values."""
+        screenPos= Numeric.array( event.GetPosition())
+        x,y= (screenPos-self._pointShift)/self._pointScale
+        return x,y
+
+    def SetXSpec(self, type= 'auto'):
+        """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
+        where:
+            'none' - shows no axis or tick mark values
+            'min' - shows min bounding box values
+            'auto' - rounds axis range to sensible values
+        """
+        self._xSpec= type
+        
+    def SetYSpec(self, type= 'auto'):
+        """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
+        where:
+            'none' - shows no axis or tick mark values
+            'min' - shows min bounding box values
+            'auto' - rounds axis range to sensible values
+        """
+        self._ySpec= type
+
+    def GetXSpec(self):
+        """Returns current XSpec for axis"""
+        return self._xSpec
+    
+    def GetYSpec(self):
+        """Returns current YSpec for axis"""
+        return self._ySpec
+    
+    def GetXMaxRange(self):
+        """Returns (minX, maxX) x-axis range for displayed graph"""
+        graphics= self.last_draw[0]
+        p1, p2 = graphics.boundingBox()     # min, max points of graphics
+        xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+        return xAxis
+
+    def GetYMaxRange(self):
+        """Returns (minY, maxY) y-axis range for displayed graph"""
+        graphics= self.last_draw[0]
+        p1, p2 = graphics.boundingBox()     # min, max points of graphics
+        yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+        return yAxis
+
+    def GetXCurrentRange(self):
+        """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
+        return self.last_draw[1]
+    
+    def GetYCurrentRange(self):
+        """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
+        return self.last_draw[2]
+        
+    def Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
+        """Draw objects in graphics with specified x and y axis.
+        graphics- instance of PlotGraphics with list of PolyXXX objects
+        xAxis - tuple with (min, max) axis range to view
+        yAxis - same as xAxis
+        dc - drawing context - doesn't have to be specified.    
+        If it's not, the offscreen buffer is used
+        """
+        # check Axis is either tuple or none
+        if type(xAxis) not in [type(None),tuple]:
+            raise TypeError, "xAxis should be None or (minX,maxX)"
+        if type(yAxis) not in [type(None),tuple]:
+            raise TypeError, "yAxis should be None or (minY,maxY)"
+             
+        # check case for axis = (a,b) where a==b caused by improper zooms
+        if xAxis != None:
+            if xAxis[0] == xAxis[1]:
+                return
+        if yAxis != None:
+            if yAxis[0] == yAxis[1]:
+                return
+            
+        if dc == None:
+            # allows using floats for certain functions 
+            dc = FloatDCWrapper(wx.BufferedDC(wx.ClientDC(self), self._Buffer))
+            dc.Clear()
+            
+        dc.BeginDrawing()
+        # dc.Clear()
+        
+        # set font size for every thing but title and legend
+        dc.SetFont(self._getFont(self._fontSizeAxis))
+
+        # sizes axis to axis type, create lower left and upper right corners of plot
+        if xAxis == None or yAxis == None:
+            # One or both axis not specified in Draw
+            p1, p2 = graphics.boundingBox()     # min, max points of graphics
+            if xAxis == None:
+                xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+            if yAxis == None:
+                yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+            # Adjust bounding box for axis spec
+            p1[0],p1[1] = xAxis[0], yAxis[0]     # lower left corner user scale (xmin,ymin)
+            p2[0],p2[1] = xAxis[1], yAxis[1]     # upper right corner user scale (xmax,ymax)
+        else:
+            # Both axis specified in Draw
+            p1= Numeric.array([xAxis[0], yAxis[0]])    # lower left corner user scale (xmin,ymin)
+            p2= Numeric.array([xAxis[1], yAxis[1]])     # upper right corner user scale (xmax,ymax)
+
+        self.last_draw = (graphics, xAxis, yAxis)       # saves most recient values
+
+        # Get ticks and textExtents for axis if required
+        if self._xSpec is not 'none':        
+            xticks = self._ticks(xAxis[0], xAxis[1])
+            xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
+        else:
+            xticks = None
+            xTextExtent= (0,0) # No text for ticks
+        if self._ySpec is not 'none':
+            yticks = self._ticks(yAxis[0], yAxis[1])
+            yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
+            yTextExtentTop   = dc.GetTextExtent(yticks[-1][1])
+            yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
+                        max(yTextExtentBottom[1],yTextExtentTop[1]))
+        else:
+            yticks = None
+            yTextExtent= (0,0) # No text for ticks
+
+        # TextExtents for Title and Axis Labels
+        titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
+
+        # TextExtents for Legend
+        legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
+
+        # room around graph area
+        rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
+        lhsW= yTextExtent[0]+ yLabelWH[1]
+        bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
+        topH= yTextExtent[1]/2. + titleWH[1]
+        textSize_scale= Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
+        textSize_shift= Numeric.array([lhsW, bottomH])          # shift plot area by this amount
+
+        # drawing title and labels text
+        dc.SetFont(self._getFont(self._fontSizeTitle))
+        titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
+                 self.plotbox_origin[1]- self.plotbox_size[1])
+        dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
+        dc.SetFont(self._getFont(self._fontSizeAxis))
+        xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
+                 self.plotbox_origin[1]- xLabelWH[1])
+        dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
+        yLabelPos= (self.plotbox_origin[0],
+                 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
+        if graphics.getYLabel():  # bug fix for Linux
+            dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
+
+        # drawing legend makers and text
+        if self._legendEnabled:
+            self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
+
+        # allow for scaling and shifting plotted points
+        scale = (self.plotbox_size-textSize_scale) / (p2-p1)* Numeric.array((1,-1))
+        shift = -p1*scale + self.plotbox_origin + textSize_shift * Numeric.array((1,-1))
+        self._pointScale= scale  # make available for mouse events
+        self._pointShift= shift        
+        self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
+        
+        graphics.scaleAndShift(scale, shift)
+        graphics.setPrinterScale(self.printerScale)  # thicken up lines and markers if printing
+        
+        # set clipping area so drawing does not occur outside axis box
+        ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
+        dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
+        # Draw the lines and markers
+        #start = time.clock()
+        graphics.draw(dc)
+        # print "entire graphics drawing took: %f second"%(time.clock() - start)
+        # remove the clipping region
+        dc.DestroyClippingRegion()
+        dc.EndDrawing()
+        
+    def Redraw(self, dc= None):
+        """Redraw the existing plot."""
+        if self.last_draw is not None:
+            graphics, xAxis, yAxis= self.last_draw
+            self.Draw(graphics,xAxis,yAxis,dc)
+
+    def Clear(self):
+        """Erase the window."""
+        dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
+        dc.Clear()
+        self.last_draw = None
+
+    def Zoom(self, Center, Ratio):
+        """ Zoom on the plot
+            Centers on the X,Y coords given in Center
+            Zooms by the Ratio = (Xratio, Yratio) given
+        """
+        x,y = Center
+        if self.last_draw != None:
+            (graphics, xAxis, yAxis) = self.last_draw
+            w = (xAxis[1] - xAxis[0]) * Ratio[0]
+            h = (yAxis[1] - yAxis[0]) * Ratio[1]
+            xAxis = ( x - w/2, x + w/2 )
+            yAxis = ( y - h/2, y + h/2 )
+            self.Draw(graphics, xAxis, yAxis)
+        
+
+    # event handlers **********************************
+    def OnMotion(self, event):
+        if self._zoomEnabled and event.LeftIsDown():
+            if self._hasDragged:
+                self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+            else:
+                self._hasDragged= True
+            self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
+            self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
+
+    def OnMouseLeftDown(self,event):
+        self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
+
+    def OnMouseLeftUp(self, event):
+        if self._zoomEnabled:
+            if self._hasDragged == True:
+                self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+                self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
+                self._hasDragged = False  # reset flag
+                minX, minY= Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
+                maxX, maxY= Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
+                if self.last_draw != None:
+                    self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
+            #else: # A box has not been drawn, zoom in on a point
+            ## this interfered with the double click, so I've disables it.
+            #    X,Y = self.GetXY(event)
+            #    self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
+
+    def OnMouseDoubleClick(self,event):
+        if self._zoomEnabled:
+            self.Reset()
+        
+    def OnMouseRightDown(self,event):
+        if self._zoomEnabled:
+            X,Y = self.GetXY(event)
+            self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
+
+    def OnPaint(self, event):
+        # All that is needed here is to draw the buffer to screen
+        dc = wx.BufferedPaintDC(self, self._Buffer)        
+
+    def OnSize(self,event):
+        # The Buffer init is done here, to make sure the buffer is always
+        # the same size as the Window
+        Size  = self.GetClientSize()
+
+        # Make new offscreen bitmap: this bitmap will always have the
+        # current drawing in it, so it can be used to save the image to
+        # a file, or whatever.
+        self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
+        self._setSize()
+        if self.last_draw is None:
+            self.Clear()
+        else:
+            graphics, xSpec, ySpec = self.last_draw
+            self.Draw(graphics,xSpec,ySpec)
+
+        
+    # Private Methods **************************************************
+    def _setSize(self, width=None, height=None):
+        """DC width and height."""
+        if width == None:
+            (self.width,self.height) = self.GetClientSize()
+        else:
+            self.width, self.height= width,height    
+        self.plotbox_size = 0.97*Numeric.array([self.width, self.height])
+        xo = 0.5*(self.width-self.plotbox_size[0])
+        yo = self.height-0.5*(self.height-self.plotbox_size[1])
+        self.plotbox_origin = Numeric.array([xo, yo])
+    
+    def _setPrinterScale(self, scale):
+        """Used to thicken lines and increase marker size for print out."""
+        # line thickness on printer is very thin at 600 dot/in. Markers small
+        self.printerScale= scale
+     
+    def _printDraw(self, printDC):
+        """Used for printing."""
+        if self.last_draw != None:
+            graphics, xSpec, ySpec= self.last_draw
+            self.Draw(graphics,xSpec,ySpec,printDC)
+
+    def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
+        """Draws legend symbols and text"""
+        # top right hand corner of graph box is ref corner
+        trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
+        legendLHS= .091* legendBoxWH[0]  # border space between legend sym and graph box
+        lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
+        dc.SetFont(self._getFont(self._fontSizeLegend))
+        for i in range(len(graphics)):
+            o = graphics[i]
+            s= i*lineHeight
+            if isinstance(o,PolyMarker):
+                # draw marker with legend
+                pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
+                o.draw(dc, self.printerScale, coord= Numeric.array([pnt]))
+            elif isinstance(o,PolyLine):
+                # draw line with legend
+                pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
+                pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
+                o.draw(dc, self.printerScale, coord= Numeric.array([pnt1,pnt2]))
+            else:
+                raise TypeError, "object is neither PolyMarker or PolyLine instance"
+            # draw legend txt
+            pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
+            dc.DrawText(o.getLegend(),pnt[0],pnt[1])
+        dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
+
+    def _titleLablesWH(self, dc, graphics):
+        """Draws Title and labels and returns width and height for each"""
+        # TextExtents for Title and Axis Labels
+        dc.SetFont(self._getFont(self._fontSizeTitle))
+        title= graphics.getTitle()
+        titleWH= dc.GetTextExtent(title)
+        dc.SetFont(self._getFont(self._fontSizeAxis))
+        xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
+        xLabelWH= dc.GetTextExtent(xLabel)
+        yLabelWH= dc.GetTextExtent(yLabel)
+        return titleWH, xLabelWH, yLabelWH
+    
+    def _legendWH(self, dc, graphics):
+        """Returns the size in screen units for legend box"""
+        if self._legendEnabled != True:
+            legendBoxWH= symExt= txtExt= (0,0)
+        else:
+            # find max symbol size
+            symExt= graphics.getSymExtent(self.printerScale)
+            # find max legend text extent
+            dc.SetFont(self._getFont(self._fontSizeLegend))
+            txtList= graphics.getLegendNames()
+            txtExt= dc.GetTextExtent(txtList[0])
+            for txt in graphics.getLegendNames()[1:]:
+                txtExt= Numeric.maximum(txtExt,dc.GetTextExtent(txt))
+            maxW= symExt[0]+txtExt[0]    
+            maxH= max(symExt[1],txtExt[1])
+            # padding .1 for lhs of legend box and space between lines
+            maxW= maxW* 1.1
+            maxH= maxH* 1.1 * len(txtList)
+            dc.SetFont(self._getFont(self._fontSizeAxis))
+            legendBoxWH= (maxW,maxH)
+        return (legendBoxWH, symExt, txtExt)
+
+    def _drawRubberBand(self, corner1, corner2):
+        """Draws/erases rect box from corner1 to corner2"""
+        ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
+        # draw rectangle
+        dc = wx.ClientDC( self )
+        dc.BeginDrawing()                 
+        dc.SetPen(wx.Pen(wx.BLACK))
+        dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
+        dc.SetLogicalFunction(wx.INVERT)
+        dc.DrawRectangle( (ptx,pty), (rectWidth,rectHeight))
+        dc.SetLogicalFunction(wx.COPY)
+        dc.EndDrawing()
+
+    def _getFont(self,size):
+        """Take font size, adjusts if printing and returns wx.Font"""
+        s = size*self.printerScale
+        of = self.GetFont()
+        # Linux speed up to get font from cache rather than X font server
+        key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
+        font = self._fontCache.get (key, None)
+        if font:
+            return font                 # yeah! cache hit
+        else:
+            font =  wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
+            self._fontCache[key] = font
+            return font
+
+
+    def _point2ClientCoord(self, corner1, corner2):
+        """Converts user point coords to client screen int coords x,y,width,height"""
+        c1= Numeric.array(corner1)
+        c2= Numeric.array(corner2)
+        # convert to screen coords
+        pt1= c1*self._pointScale+self._pointShift
+        pt2= c2*self._pointScale+self._pointShift
+        # make height and width positive
+        pul= Numeric.minimum(pt1,pt2) # Upper left corner
+        plr= Numeric.maximum(pt1,pt2) # Lower right corner
+        rectWidth, rectHeight= plr-pul
+        ptx,pty= pul
+        return int(ptx),int(pty),int(rectWidth),int(rectHeight) # return ints
+    
+    def _axisInterval(self, spec, lower, upper):
+        """Returns sensible axis range for given spec"""
+        if spec == 'none' or spec == 'min':
+            if lower == upper:
+                return lower-0.5, upper+0.5
+            else:
+                return lower, upper
+        elif spec == 'auto':
+            range = upper-lower
+            if range == 0.:
+                return lower-0.5, upper+0.5
+            log = Numeric.log10(range)
+            power = Numeric.floor(log)
+            fraction = log-power
+            if fraction <= 0.05:
+                power = power-1
+            grid = 10.**power
+            lower = lower - lower % grid
+            mod = upper % grid
+            if mod != 0:
+                upper = upper - mod + grid
+            return lower, upper
+        elif type(spec) == type(()):
+            lower, upper = spec
+            if lower <= upper:
+                return lower, upper
+            else:
+                return upper, lower
+        else:
+            raise ValueError, str(spec) + ': illegal axis specification'
+
+    def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
+        
+        penWidth= self.printerScale        # increases thickness for printing only
+        dc.SetPen(wx.Pen(wx.NamedColour('BLACK'),int(penWidth)))
+        
+        # set length of tick marks--long ones make grid
+        if self._gridEnabled:
+            x,y,width,height= self._point2ClientCoord(p1,p2)
+            yTickLength= width/2.0 +1
+            xTickLength= height/2.0 +1
+        else:
+            yTickLength= 3 * self.printerScale  # lengthens lines for printing
+            xTickLength= 3 * self.printerScale
+        
+        if self._xSpec is not 'none':
+            lower, upper = p1[0],p2[0]
+            text = 1
+            for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]:   # miny, maxy and tick lengths
+                a1 = scale*Numeric.array([lower, y])+shift
+                a2 = scale*Numeric.array([upper, y])+shift
+                dc.DrawLine(a1[0],a1[1],a2[0],a2[1])  # draws upper and lower axis line
+                for x, label in xticks:
+                    pt = scale*Numeric.array([x, y])+shift
+                    dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
+                    if text:
+                        dc.DrawText(label,pt[0],pt[1])
+                text = 0  # axis values not drawn on top side
+
+        if self._ySpec is not 'none':
+            lower, upper = p1[1],p2[1]
+            text = 1
+            h = dc.GetCharHeight()
+            for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
+                a1 = scale*Numeric.array([x, lower])+shift
+                a2 = scale*Numeric.array([x, upper])+shift
+                dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
+                for y, label in yticks:
+                    pt = scale*Numeric.array([x, y])+shift
+                    dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
+                    if text:
+                        dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
+                                    pt[1]-0.5*h)
+                text = 0    # axis values not drawn on right side
+
+    def _ticks(self, lower, upper):
+        ideal = (upper-lower)/7.
+        log = Numeric.log10(ideal)
+        power = Numeric.floor(log)
+        fraction = log-power
+        factor = 1.
+        error = fraction
+        for f, lf in self._multiples:
+            e = Numeric.fabs(fraction-lf)
+            if e < error:
+                error = e
+                factor = f
+        grid = factor * 10.**power
+        if power > 4 or power < -4:
+            format = '%+7.1e'        
+        elif power >= 0:
+            digits = max(1, int(power))
+            format = '%' + `digits`+'.0f'
+        else:
+            digits = -int(power)
+            format = '%'+`digits+2`+'.'+`digits`+'f'
+        ticks = []
+        t = -grid*Numeric.floor(-lower/grid)
+        while t <= upper:
+            ticks.append( (t, format % (t,)) )
+            t = t + grid
+        return ticks
+
+    _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))]
+
+
+#-------------------------------------------------------------------------------
+# Used to layout the printer page
+
+class PlotPrintout(wx.Printout):
+    """Controls how the plot is made in printing and previewing"""
+    # Do not change method names in this class,
+    # we have to override wx.Printout methods here!
+    def __init__(self, graph):
+        """graph is instance of plotCanvas to be printed or previewed"""
+        wx.Printout.__init__(self)
+        self.graph = graph
+
+    def HasPage(self, page):
+        if page == 1:
+            return True
+        else:
+            return False
+
+    def GetPageInfo(self):
+        return (0, 1, 1, 1)  # disable page numbers
+
+    def OnPrintPage(self, page):
+        dc = FloatDCWrapper(self.GetDC())  # allows using floats for certain functions
+##        print "PPI Printer",self.GetPPIPrinter()
+##        print "PPI Screen", self.GetPPIScreen()
+##        print "DC GetSize", dc.GetSize()
+##        print "GetPageSizePixels", self.GetPageSizePixels()
+        # Note PPIScreen does not give the correct number
+        # Calulate everything for printer and then scale for preview
+        PPIPrinter= self.GetPPIPrinter()        # printer dots/inch (w,h)
+        #PPIScreen= self.GetPPIScreen()          # screen dots/inch (w,h)
+        dcSize= dc.GetSize()                    # DC size
+        pageSize= self.GetPageSizePixels() # page size in terms of pixcels
+        clientDcSize= self.graph.GetClientSize()
+        
+        # find what the margins are (mm)
+        margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
+        margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
+
+        # calculate offset and scale for dc
+        pixLeft= margLeftSize*PPIPrinter[0]/25.4  # mm*(dots/in)/(mm/in)
+        pixRight= margRightSize*PPIPrinter[0]/25.4    
+        pixTop= margTopSize*PPIPrinter[1]/25.4
+        pixBottom= margBottomSize*PPIPrinter[1]/25.4
+
+        plotAreaW= pageSize[0]-(pixLeft+pixRight)
+        plotAreaH= pageSize[1]-(pixTop+pixBottom)
+
+        # ratio offset and scale to screen size if preview
+        if self.IsPreview():
+            ratioW= float(dcSize[0])/pageSize[0]
+            ratioH= float(dcSize[1])/pageSize[1]
+            pixLeft *= ratioW
+            pixTop *= ratioH
+            plotAreaW *= ratioW
+            plotAreaH *= ratioH
+        
+        # rescale plot to page or preview plot area
+        self.graph._setSize(plotAreaW,plotAreaH)
+        
+        # Set offset and scale
+        dc.SetDeviceOrigin(pixLeft,pixTop)
+
+        # Thicken up pens and increase marker size for printing
+        ratioW= float(plotAreaW)/clientDcSize[0]
+        ratioH= float(plotAreaH)/clientDcSize[1]
+        aveScale= (ratioW+ratioH)/2
+        self.graph._setPrinterScale(aveScale)  # tickens up pens for printing
+
+        self.graph._printDraw(dc)
+        # rescale back to original
+        self.graph._setSize()
+        self.graph._setPrinterScale(1)
+
+        return True
+
+# Hack to allow plotting real numbers for the methods listed.
+# All others passed directly to DC.
+# For Drawing it is used as
+# dc = FloatDCWrapper(wx.BufferedDC(wx.ClientDC(self), self._Buffer))
+# For printing is is used as
+# dc = FloatDCWrapper(self.GetDC()) 
+class FloatDCWrapper:
+    def __init__(self, aDC):
+        self.theDC = aDC
+
+    def DrawLine(self, x1,y1,x2,y2):
+        self.theDC.DrawLine((int(x1),int(y1)),(int(x2),int(y2)))
+
+    def DrawText(self, txt, x, y):
+        self.theDC.DrawText(txt, (int(x), int(y)))
+
+    def DrawRotatedText(self, txt, x, y, angle):
+        self.theDC.DrawRotatedText(txt, (int(x), int(y)), angle)
+
+    def SetClippingRegion(self, x, y, width, height):
+        self.theDC.SetClippingRegion((int(x), int(y)), (int(width), int(height)))
+
+    def SetDeviceOrigin(self, x, y):
+        self.theDC.SetDeviceOrigin(int(x), int(y))
+
+    def __getattr__(self, name):
+        return getattr(self.theDC, name)
+
+
+
+
+#---------------------------------------------------------------------------
+# if running standalone...
+#
+#     ...a sample implementation using the above
+#
+
+def _draw1Objects():
+    # 100 points sin function, plotted as green circles
+    data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
+    data1.shape = (100, 2)
+    data1[:,1] = Numeric.sin(data1[:,0])
+    markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
+
+    # 50 points cos function, plotted as red line
+    data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
+    data1.shape = (50,2)
+    data1[:,1] = Numeric.cos(data1[:,0])
+    lines = PolyLine(data1, legend= 'Red Line', colour='red')
+
+    # A few more points...
+    pi = Numeric.pi
+    markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+                          (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
+                          marker='cross')
+    
+    return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
+
+def _draw2Objects():
+    # 100 points sin function, plotted as green dots
+    data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
+    data1.shape = (100, 2)
+    data1[:,1] = Numeric.sin(data1[:,0])
+    line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
+
+    # 50 points cos function, plotted as red dot-dash
+    data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
+    data1.shape = (50,2)
+    data1[:,1] = Numeric.cos(data1[:,0])
+    line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
+
+    # A few more points...
+    pi = Numeric.pi
+    markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+                          (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
+                          fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
+                          marker='square')
+
+    return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
+
+def _draw3Objects():
+    markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
+                'cross', 'plus', 'circle']
+    m=[]
+    for i in range(len(markerList)):
+        m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
+                          marker=markerList[i]))
+    return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
+
+def _draw4Objects():
+    # 25,000 point line
+    data1 = Numeric.arange(5e5,1e6,10)
+    data1.shape = (25000, 2)
+    line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
+
+    # A few more points...
+    markers2 = PolyMarker(data1, legend='Square', colour='blue',
+                          marker='square')
+    return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
+
+def _draw5Objects():
+    # Empty graph with axis defined but no points/lines
+    points=[]
+    line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
+    return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
+
+
+class TestFrame(wx.Frame):
+    def __init__(self, parent, id, title):
+        wx.Frame.__init__(self, parent, id, title,
+                          wx.DefaultPosition, (600, 400))
+
+        # Now Create the menu bar and items
+        self.mainmenu = wx.MenuBar()
+
+        menu = wx.Menu()
+        menu.Append(200, 'Page Setup...', 'Setup the printer page')
+        self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
+        
+        menu.Append(201, 'Print Preview...', 'Show the current plot on page')
+        self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
+        
+        menu.Append(202, 'Print...', 'Print the current plot')
+        self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
+        
+        menu.Append(203, 'Save Plot...', 'Save current plot')
+        self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
+        
+        menu.Append(205, 'E&xit', 'Enough of this already!')
+        self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
+        self.mainmenu.Append(menu, '&File')
+
+        menu = wx.Menu()
+        menu.Append(206, 'Draw1', 'Draw plots1')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
+        menu.Append(207, 'Draw2', 'Draw plots2')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
+        menu.Append(208, 'Draw3', 'Draw plots3')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
+        menu.Append(209, 'Draw4', 'Draw plots4')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
+        menu.Append(210, 'Draw5', 'Draw plots5')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
+
+        menu.Append(211, '&Redraw', 'Redraw plots')
+        self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
+        menu.Append(212, '&Clear', 'Clear canvas')
+        self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
+        menu.Append(213, '&Scale', 'Scale canvas')
+        self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213) 
+        menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214) 
+        menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
+        menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220) 
+        menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
+        self.Bind(wx.EVT_MENU,self.OnScrUp, id=225) 
+        menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
+        self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
+        menu.Append(235, '&Plot Reset', 'Reset to original plot')
+        self.Bind(wx.EVT_MENU,self.OnReset, id=235)
+
+        self.mainmenu.Append(menu, '&Plot')
+
+        menu = wx.Menu()
+        menu.Append(300, '&About', 'About this thing...')
+        self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
+        self.mainmenu.Append(menu, '&Help')
+
+        self.SetMenuBar(self.mainmenu)
+
+        # A status bar to tell people what's happening
+        self.CreateStatusBar(1)
+        
+        self.client = PlotCanvas(self)
+        # Create mouse event for showing cursor coords in status bar
+        self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+        self.Show(True)
+
+    def OnMouseLeftDown(self,event):
+        s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
+        self.SetStatusText(s)
+        event.Skip()
+
+    def OnFilePageSetup(self, event):
+        self.client.PageSetup()
+        
+    def OnFilePrintPreview(self, event):
+        self.client.PrintPreview()
+        
+    def OnFilePrint(self, event):
+        self.client.Printout()
+        
+    def OnSaveFile(self, event):
+        self.client.SaveFile()
+
+    def OnFileExit(self, event):
+        self.Close()
+
+    def OnPlotDraw1(self, event):
+        self.resetDefaults()
+        self.client.Draw(_draw1Objects())
+    
+    def OnPlotDraw2(self, event):
+        self.resetDefaults()
+        self.client.Draw(_draw2Objects())
+    
+    def OnPlotDraw3(self, event):
+        self.resetDefaults()
+        self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
+        self.client.SetFontSizeAxis(20)
+        self.client.SetFontSizeLegend(12)
+        self.client.SetXSpec('min')
+        self.client.SetYSpec('none')
+        self.client.Draw(_draw3Objects())
+
+    def OnPlotDraw4(self, event):
+        self.resetDefaults()
+        drawObj= _draw4Objects()
+        self.client.Draw(drawObj)
+##            # profile
+##            start = time.clock()            
+##            for x in range(10):
+##                self.client.Draw(drawObj)
+##            print "10 plots of Draw4 took: %f sec."%(time.clock() - start)
+##            # profile end
+
+    def OnPlotDraw5(self, event):
+        # Empty plot with just axes
+        self.resetDefaults()
+        drawObj= _draw5Objects()
+        # make the axis X= (0,5), Y=(0,10)
+        # (default with None is X= (-1,1), Y= (-1,1))
+        self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
+
+    def OnPlotRedraw(self,event):
+        self.client.Redraw()
+
+    def OnPlotClear(self,event):
+        self.client.Clear()
+        
+    def OnPlotScale(self, event):
+        if self.client.last_draw != None:
+            graphics, xAxis, yAxis= self.client.last_draw
+            self.client.Draw(graphics,(1,3.05),(0,1))
+
+    def OnEnableZoom(self, event):
+        self.client.SetEnableZoom(event.IsChecked())
+        
+    def OnEnableGrid(self, event):
+        self.client.SetEnableGrid(event.IsChecked())
+        
+    def OnEnableLegend(self, event):
+        self.client.SetEnableLegend(event.IsChecked())
+
+    def OnScrUp(self, event):
+        self.client.ScrollUp(1)
+        
+    def OnScrRt(self,event):
+        self.client.ScrollRight(2)
+
+    def OnReset(self,event):
+        self.client.Reset()
+
+    def OnHelpAbout(self, event):
+        from wx.lib.dialogs import wxScrolledMessageDialog
+        about = wxScrolledMessageDialog(self, __doc__, "About...")
+        about.ShowModal()
+
+    def resetDefaults(self):
+        """Just to reset the fonts back to the PlotCanvas defaults"""
+        self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
+        self.client.SetFontSizeAxis(10)
+        self.client.SetFontSizeLegend(7)
+        self.client.SetXSpec('auto')
+        self.client.SetYSpec('auto')
+        
+
+def __test():
+
+    class MyApp(wx.App):
+        def OnInit(self):
+            wx.InitAllImageHandlers()
+            frame = TestFrame(None, -1, "PlotCanvas")
+            #frame.Show(True)
+            self.SetTopWindow(frame)
+            return True
+
+
+    app = MyApp(0)
+    app.MainLoop()
+
+if __name__ == '__main__':
+    __test()