]> git.saurik.com Git - wxWidgets.git/blobdiff - wxPython/wx/lib/plot.py
fixup authorship
[wxWidgets.git] / wxPython / wx / lib / plot.py
index cf5df17ca106d82c44a0489e0515e4fc03b3ad4e..1462407433e45b55da9821fdadd303c0148d1f1f 100644 (file)
 #   - Modified line end shape from round to square.
 #   - Removed FloatDCWrapper for conversion to ints and ints in arguments
 #
-
+# Oct 15, 2004  Gordon Williams (g_will@cyberus.ca)
+#   - Imported modules given leading underscore to name.
+#   - Added Cursor Line Tracking and User Point Labels. 
+#   - Demo for Cursor Line Tracking and Point Labels.
+#   - Size of plot preview frame adjusted to show page better.
+#   - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
+#   - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
+#       can be in either user coords or screen coords.
+#   
+# Nov 2004  Oliver Schoenborn (oliver.schoenborn@utoronto.ca) with 
+# valuable input from and testing by Gary)
+# - Factored out "draw command" so extensions easier to implement: clean 
+#   separation b/w user's "draw" (Draw) and internal "draw" (_draw)
+# - Added ability to define your own ticks at PlotCanvas.Draw
+# - Added better bar charts: PolyBar class and getBarTicksGen()
+# - Put legend writing for a Poly* into Poly* class where it belongs
+# - If no legend specified or exists, no gap created
+#
 
 """
 This is a simple light weight plotting module that can be used with
@@ -79,16 +96,16 @@ Zooming controls with mouse (when enabled):
     Right mouse click - zoom out centred on click location.
 """
 
-import  string
-import  time
+import  string as _string
+import  time as _time
 import  wx
 
 # Needs Numeric or numarray
 try:
-    import Numeric
+    import Numeric as _Numeric
 except:
     try:
-        import numarray as Numeric  #if numarray is used it is renamed Numeric
+        import numarray as _Numeric  #if numarray is used it is renamed Numeric
     except:
         msg= """
         This module requires the Numeric or numarray module,
@@ -109,7 +126,7 @@ class PolyPoints:
     """
 
     def __init__(self, points, attr):
-        self.points = Numeric.array(points)
+        self.points = _Numeric.array(points)
         self.currentScale= (1,1)
         self.currentShift= (0,0)
         self.scaled = self.points
@@ -124,11 +141,11 @@ class PolyPoints:
         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])
+            minXY= _Numeric.array([-1,-1])
+            maxXY= _Numeric.array([ 1, 1])
         else:
-            minXY= Numeric.minimum.reduce(self.points)
-            maxXY= Numeric.maximum.reduce(self.points)
+            minXY= _Numeric.minimum.reduce(self.points)
+            maxXY= _Numeric.maximum.reduce(self.points)
         return minXY, maxXY
 
     def scaleAndShift(self, scale=(1,1), shift=(0,0)):
@@ -142,10 +159,44 @@ class PolyPoints:
             self.currentShift= shift
         # else unchanged use the current scaling
         
+    def drawLegend(self, dc, printerScale, symWidth, hgap, textHeight, x0, y0):
+        legend = self.getLegend()
+        if legend != '':
+            self._drawLegendSym(dc, printerScale, symWidth, x0, y0)
+            dc.DrawText(legend, x0+symWidth+hgap, y0-textHeight/2)
+            return True
+        
+        return False
+        
+    def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0):
+        # default:
+        pnt= (x0+symWidth/2., y0)
+        self.draw(dc, printerScale, coord= _Numeric.array([pnt]))
+
     def getLegend(self):
         return self.attributes['legend']
 
-
+    def getClosestPoint(self, pntXY, pointScaled= True):
+        """Returns the index of closest point on the curve, pointXY, scaledXY, distance
+            x, y in user coords
+            if pointScaled == True based on screen coords
+            if pointScaled == False based on user coords
+        """
+        if pointScaled == True:
+            #Using screen coords
+            p = self.scaled
+            pxy = self.currentScale * _Numeric.array(pntXY)+ self.currentShift
+        else:
+            #Using user coords
+            p = self.points
+            pxy = _Numeric.array(pntXY)
+        #determine distance for each point
+        d= _Numeric.sqrt(_Numeric.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
+        pntIndex = _Numeric.argmin(d)
+        dist = d[pntIndex]
+        return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
+        
+        
 class PolyLine(PolyPoints):
     """Class to define line type and style
         - All methods except __init__ are private.
@@ -172,7 +223,7 @@ class PolyLine(PolyPoints):
         colour = self.attributes['colour']
         width = self.attributes['width'] * printerScale
         style= self.attributes['style']
-        pen = wx.Pen(wx.NamedColour(colour), width, style)
+        pen = wx.Pen(colour, width, style)
         pen.SetCap(wx.CAP_BUTT)
         dc.SetPen(pen)
         if coord == None:
@@ -180,6 +231,11 @@ class PolyLine(PolyPoints):
         else:
             dc.DrawLines(coord) # draw legend line
 
+    def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0):
+        pnt1= (x0, y0)
+        pnt2= (x0+symWidth/1.5, y0)
+        self.draw(dc, printerScale, coord= _Numeric.array([pnt1,pnt2]))
+
     def getSymExtent(self, printerScale):
         """Width and Height of Marker"""
         h= self.attributes['width'] * printerScale
@@ -208,7 +264,7 @@ class PolyMarker(PolyPoints):
                 '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
+                'fillcolour'= same as colour, - wx.Brush Colour
                 'fillstyle'= wx.SOLID,      - wx.Brush fill style (use wx.TRANSPARENT for no fill)
                 'marker'= 'circle'          - Marker shape
                 'legend'= ''                - Marker Legend to display
@@ -233,11 +289,11 @@ class PolyMarker(PolyPoints):
         fillstyle = self.attributes['fillstyle']
         marker = self.attributes['marker']
 
-        dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
+        dc.SetPen(wx.Pen(colour, width))
         if fillcolour:
-            dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
+            dc.SetBrush(wx.Brush(fillcolour,fillstyle))
         else:
-            dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
+            dc.SetBrush(wx.Brush(colour, fillstyle))
         if coord == None:
             self._drawmarkers(dc, self.scaled, marker, size)
         else:
@@ -255,9 +311,9 @@ class PolyMarker(PolyPoints):
     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= _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))
+        dc.DrawEllipseList(rect.astype(_Numeric.Int32))
 
     def _dot(self, dc, coords, size=1):
         dc.DrawPointList(coords)
@@ -265,35 +321,177 @@ class PolyMarker(PolyPoints):
     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= _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))
+        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= _Numeric.repeat(coords,3)
         poly.shape= (len(coords),3,2)
         poly += shape
-        dc.DrawPolygonList(poly.astype(Numeric.Int32))
+        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= _Numeric.repeat(coords,3)
         poly.shape= (len(coords),3,2)
         poly += shape
-        dc.DrawPolygonList(poly.astype(Numeric.Int32))
+        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))
+            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))
+            lines= _Numeric.concatenate((coords,coords),axis=1)+f
+            dc.DrawLineList(lines.astype(_Numeric.Int32))
+
+class PolyBar(PolyPoints):
+    """Class to define one or more bars for bar chart.  All bars in a 
+       PolyBar share same style etc. 
+        - All methods except __init__ are private.
+    """
+  
+    _attributes = {'colour': 'black',
+                   'width': 1,
+                   'size': 2,
+                   'legend': '',
+                   'fillcolour': None,
+                   'fillstyle': wx.SOLID}
+
+    def __init__(self, points, **attr):
+        """Creates several bars. 
+        points - sequence (array, tuple or list) of (x,y) points
+                 indicating *top left* corner of bar. Can also be
+                 just one (x,y) tuple if labels attribute is just
+                 a string (not a list)
+        **attr - key word attributes
+            Defaults:
+                'colour'= wx.BLACK,           - wx.Pen Colour
+                'width'= 1,                   - Pen width
+                'size'= 2,                    - Bar size
+                'fillcolour'= same as colour, - wx.Brush Colour
+                'fillstyle'= wx.SOLID,        - wx.Brush fill style (use wx.TRANSPARENT for no fill)
+                'legend'= ''                  - string used for PolyBar legend
+                'labels'= ''                  - string if only one point, or list of labels, one per point
+            Note that if no legend is given the label is used. 
+        """
+        barPoints = [(0,0)] # needed so height of bar can be determined
+        self.labels = []
+        # add labels and points to above data members:
+        try: 
+            labels = attr['labels']
+            del attr['labels']
+        except:
+            labels = None
+        if labels is None: # figure out if we have 1 point or many
+            try: 
+                points[0][0] # ok: we have a seq of points
+                barPoints.extend(points)
+            except: # failed, so have just one point:
+                barPoints.append(points)
+        elif isinstance(labels, list) or isinstance(labels, tuple):
+            # labels is a sequence so points must be too, with same length:
+            self.labels.extend(labels)
+            barPoints.extend(points)
+            if len(labels) != len(points):
+                msg = "%d bar labels missing" % (len(points)-len(labels))
+                raise ValueError, msg
+        else: # label given, but only one, so must be only one point:
+            barPoints.append(points)
+            self.labels.append(labels)
+
+        PolyPoints.__init__(self, barPoints, 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']
+
+        dc.SetPen(wx.Pen(colour,int(width)))
+        if fillcolour:
+            dc.SetBrush(wx.Brush(fillcolour,fillstyle))
+        else:
+            dc.SetBrush(wx.Brush(colour, fillstyle))
+        if coord == None:
+            self._drawbar(dc, self.scaled, size)
+        else:
+            self._drawLegendMarker(dc, coord, size) #draw legend marker
+
+    def _drawLegendMarker(self, dc, coord, size):
+        fact= 10 
+        wh= 2*fact
+        rect= _Numeric.zeros((len(coord),4),_Numeric.Float)+[0.0,0.0,wh,wh]
+        rect[:,0:2]= coord-[fact,fact]
+        dc.DrawRectangleList(rect.astype(_Numeric.Int32))
+        
+    def offset(self, heights, howMany = 1, shift=0.25):
+        """Return a list of points, where x's are those of this bar,
+        shifted by shift*howMany (in caller's units, not pixel units), and
+        heights are given in heights."""
+        points = [(point[0][0]+shift*howMany,point[1]) for point 
+                    in zip(self.points[1:], heights)]
+        return points
+            
+    def getSymExtent(self, printerScale):
+        """Width and Height of bar symbol"""
+        s= 5*self.attributes['size'] * printerScale
+        return (s,s)
+
+    def getLegend(self):
+        """This returns a comma-separated list of bar labels (one string)"""
+        try: legendItem = self.attributes['legend']
+        except: 
+            legendItem = ", ".join(self.labels)
+        return legendItem
+    
+    def _drawbar(self, dc, coords, size=1):
+        width = 5.0*size
+        y0 = coords[0][1]
+        bars = []
+        for coord in coords[1:]:
+            x = coord[0] # - width/2 not as good
+            y = coord[1]
+            height = int(y0) - int(y)
+            bars.append((int(x),int(y),int(width),int(height)))
+            #print x,y,width, height
+        dc.DrawRectangleList(bars)
+
+    def getTicks(self):
+        """Get the list of (tick pos, label) pairs for this PolyBar. 
+        It is unlikely you need this, but it is used by 
+        getBarTicksGen."""
+        ticks = []
+        # remember to skip first point:
+        for point, label in zip(self.points[1:], self.labels):
+            ticks.append((point[0], label))
+        return ticks
+
+def getBarTicksGen(*polyBars):
+    """Get a function that can be given as xTicks argument when
+    calling PolyCanvas.Draw() when plotting one or more PolyBar.
+    The returned function allows the bar chart to have the bars 
+    labelled at the x axis. """
+    ticks = []
+    for polyBar in polyBars:
+        ticks.extend(polyBar.getTicks())
+            
+    def tickGenerator(lower,upper,gg,ff):
+        tickPairs = []
+        for tick in ticks:
+            if lower <= tick[0] < upper:
+                tickPairs.append(tick)
+        return tickPairs
+
+    return tickGenerator
+
 
 class PlotGraphics:
     """Container to hold PolyXXX objects and graph labels
@@ -318,8 +516,8 @@ class PlotGraphics:
         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)
+            p1 = _Numeric.minimum(p1, p1o)
+            p2 = _Numeric.maximum(p2, p2o)
         return p1, p2
 
     def scaleAndShift(self, scale=(1,1), shift=(0,0)):
@@ -356,9 +554,9 @@ class PlotGraphics:
 
     def draw(self, dc):
         for o in self.objects:
-            #t=time.clock()          # profile info
+            #t=_time.clock()          # profile info
             o.draw(dc, self.printerScale)
-            #dt= time.clock()-t
+            #dt= _time.clock()-t
             #print o, "time=", dt
 
     def getSymExtent(self, printerScale):
@@ -366,7 +564,7 @@ class PlotGraphics:
         symExt = self.objects[0].getSymExtent(printerScale)
         for o in self.objects[1:]:
             oSymExt = o.getSymExtent(printerScale)
-            symExt = Numeric.maximum(symExt, oSymExt)
+            symExt = _Numeric.maximum(symExt, oSymExt)
         return symExt
     
     def getLegendNames(self):
@@ -424,13 +622,13 @@ class PlotCanvas(wx.Window):
         # 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._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._drawCmd = None
         self._pointScale= 1
         self._pointShift= 0
         self._xSpec= 'auto'
@@ -444,13 +642,19 @@ class PlotCanvas(wx.Window):
         self._fontSizeTitle= 15
         self._fontSizeLegend= 7
 
+        # pointLabels
+        self._pointLabelEnabled= False
+        self.last_PointLabel= None
+        self._pointLabelFunc= None
+        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
+
         self.Bind(wx.EVT_PAINT, self.OnPaint)
         self.Bind(wx.EVT_SIZE, self.OnSize)
         # 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.
-               if wx.Platform != "__WXMAC__":
-                   self.OnSize(None) # sets the initial size based on client size
+        if wx.Platform != "__WXMAC__":
+            self.OnSize(None) # sets the initial size based on client size
 
         
     # SaveFile
@@ -465,7 +669,7 @@ class PlotCanvas(wx.Window):
         .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']:
+        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", ".", "",
@@ -477,7 +681,7 @@ class PlotCanvas(wx.Window):
                     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']:
+                        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',
@@ -494,7 +698,7 @@ class PlotCanvas(wx.Window):
                 dlg1.Destroy()
 
         # File name has required extension
-        fType = string.lower(fileName[-3:])
+        fType = _string.lower(fileName[-3:])
         if fType == "bmp":
             tp= wx.BITMAP_TYPE_BMP       # Save a Windows bitmap file.
         elif fType == "xbm":
@@ -547,7 +751,7 @@ class PlotCanvas(wx.Window):
             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)
+        self.preview.SetZoom(40)
         # search up tree to find frame instance
         frameInst= self
         while not isinstance(frameInst, wx.Frame):
@@ -555,7 +759,7 @@ class PlotCanvas(wx.Window):
         frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
         frame.Initialize()
         frame.SetPosition(self.GetPosition())
-        frame.SetSize((500,400))
+        frame.SetSize((600,550))
         frame.Centre(wx.BOTH)
         frame.Show(True)
 
@@ -615,31 +819,66 @@ class PlotCanvas(wx.Window):
         """True if Legend enabled."""
         return self._legendEnabled
 
+    def SetEnablePointLabel(self, value):
+        """Set True to enable pointLabel."""
+        if value not in [True,False]:
+            raise TypeError, "Value should be True or False"
+        self._pointLabelEnabled= value 
+        self.Redraw()  #will erase existing pointLabel if present
+        self.last_PointLabel = None
+
+    def GetEnablePointLabel(self):
+        """True if pointLabel enabled."""
+        return self._pointLabelEnabled
+
+    def SetPointLabelFunc(self, func):
+        """Sets the function with custom code for pointLabel drawing
+            ******** more info needed ***************
+        """
+        self._pointLabelFunc= func
+
+    def GetPointLabelFunc(self):
+        """Returns pointLabel Drawing Function"""
+        return self._pointLabelFunc
+
     def Reset(self):
         """Unzoom the plot."""
-        if self.last_draw is not None:
-            self.Draw(self.last_draw[0])
+        self.last_PointLabel = None        #reset pointLabel
+        if self.BeenDrawn():
+            self._drawCmd.resetAxes(self._xSpec, self._ySpec)
+            self._draw()
         
     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)
+        self.last_PointLabel = None        #reset pointLabel
+        if self.BeenDrawn():
+            self._drawCmd.scrollAxisX(units, self._xSpec)
+            self._draw()
 
     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)
+        self.last_PointLabel = None        #reset pointLabel
+        if self.BeenDrawn():
+            self._drawCmd.scrollAxisY(units, self._ySpec)
+            self._draw()
         
     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
+        x,y= self.PositionScreenToUser(event.GetPosition())
         return x,y
 
+    def PositionUserToScreen(self, pntXY):
+        """Converts User position to Screen Coordinates"""
+        userPos= _Numeric.array(pntXY)
+        x,y= userPos * self._pointScale + self._pointShift
+        return x,y
+        
+    def PositionScreenToUser(self, pntXY):
+        """Converts Screen position to User Coordinates"""
+        screenPos= _Numeric.array(pntXY)
+        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:
@@ -668,33 +907,43 @@ class PlotCanvas(wx.Window):
     
     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
+        return self._drawCmd.getXMaxRange()
 
     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
+        return self._drawCmd.getYMaxRange()
 
     def GetXCurrentRange(self):
         """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
-        return self.last_draw[1]
+        return self._drawCmd.xAxis
     
     def GetYCurrentRange(self):
         """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
-        return self.last_draw[2]
+        return self._drawCmd.yAxis
         
-    def Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
+    def BeenDrawn(self):
+        """Return true if Draw() has been called once, false otherwise."""
+        return self._drawCmd is not None
+    
+    def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, 
+        xTicks = None, yTicks = 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
+        If it's not, the offscreen buffer is used.
+        xTicks and yTicks - can be either 
+          - a list of floats where the ticks should be, only visible 
+            ticks will be used;
+          - a function that generates a list of (tick, label) pairs; 
+            The function must be of form func(lower, upper, grid,
+            format), and the pairs must contain only ticks between lower and
+            upper. Function can use grid and format that are computed by
+            PlotCanvas, if desired, where grid is the spacing between ticks,
+            and format is the format string for how tick labels
+            will appear. See tickGeneratorDefault and
+            tickGeneratorFromList for examples of such functions.
         """
         # check Axis is either tuple or none
         if type(xAxis) not in [type(None),tuple]:
@@ -702,16 +951,22 @@ class PlotCanvas(wx.Window):
         if type(yAxis) not in [type(None),tuple]:
             raise TypeError, "yAxis should be None or (minY,maxY)"
              
+        self._drawCmd = DrawCmd(graphics, xAxis, yAxis, 
+                                 self._xSpec, self._ySpec,
+                                 xTicks, yTicks)
+        self._draw(dc)
+    
+    def _draw(self, dc=None):
+        """Implement the draw command, with dc if given, using any new 
+        settings (legend toggled, axis specs, zoom, etc). """
+        assert self._drawCmd is not None
+        
         # 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]:
+        if self._drawCmd.isEmpty():
                 return
             
         if dc == None:
-            # allows using floats for certain functions 
+            # sets new dc and clears it 
             dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
             dc.Clear()
             
@@ -721,33 +976,15 @@ class PlotCanvas(wx.Window):
         # 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])
+            xticks = self._drawCmd.getTicksX()
             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])
+            yticks = self._drawCmd.getTicksY()
             yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
             yTextExtentTop   = dc.GetTextExtent(yticks[-1][1])
             yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
@@ -757,18 +994,20 @@ class PlotCanvas(wx.Window):
             yTextExtent= (0,0) # No text for ticks
 
         # TextExtents for Title and Axis Labels
+        graphics = self._drawCmd.graphics
         titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
 
         # TextExtents for Legend
-        legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
+        legendBoxWH, legendSymExt, legendHGap, 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
+        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))
@@ -786,11 +1025,15 @@ class PlotCanvas(wx.Window):
 
         # drawing legend makers and text
         if self._legendEnabled:
-            self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
+            self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, 
+                legendSymExt, legendHGap, legendTextExt)
+
+        #sizes axis to axis type, create lower left and upper right corners of plot
+        p1, p2 = self._drawCmd.getBoundingBox()
 
         # 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))
+        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)
@@ -802,42 +1045,108 @@ class PlotCanvas(wx.Window):
         ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
         dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
         # Draw the lines and markers
-        #start = time.clock()
+        #start = _time.clock()
         graphics.draw(dc)
-        # print "entire graphics drawing took: %f second"%(time.clock() - start)
+        # 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)
+        if self.BeenDrawn():
+            self._draw(dc)
 
     def Clear(self):
         """Erase the window."""
+        self.last_PointLabel = None        #reset pointLabel
         dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
         dc.Clear()
-        self.last_draw = None
+        self._drawCmd = 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)
+        self.last_PointLabel = None   #reset maker
+        if self.BeenDrawn():
+            self._drawCmd.zoom(Center, Ratio)
+            self._draw()
         
+    def ChangeAxes(self, xAxis, yAxis):
+        """Change axes specified at last Draw() command. If 
+        Draw() never called yet, RuntimeError raised. """
+        if not self.BeenDrawn(): 
+            raise RuntimeError, "Must call Draw() at least once"
+        self._drawCmd.changeAxisX(xAxis, self._xSpec)
+        self._drawCmd.changeAxisY(yAxis, self._ySpec)
+        self._draw()
+
+    def GetClosestPoints(self, pntXY, pointScaled= True):
+        """Returns list with
+            [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+            list for each curve.
+            Returns [] if no curves are being plotted.
+            
+            x, y in user coords
+            if pointScaled == True based on screen coords
+            if pointScaled == False based on user coords
+        """
+        if self.BeenDrawn():
+            return self._drawCmd.getClosestPoints(pntXY, pointScaled)
+        else:
+            #no graph available
+            return []
+
+    def GetClosetPoint(self, pntXY, pointScaled= True):
+        """Returns list with
+            [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+            list for only the closest curve.
+            Returns [] if no curves are being plotted.
+            
+            x, y in user coords
+            if pointScaled == True based on screen coords
+            if pointScaled == False based on user coords
+        """
+        #closest points on screen based on screen scaling (pointScaled= True)
+        #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
+        closestPts= self.GetClosestPoints(pntXY, pointScaled)
+        if closestPts == []:
+            return []  #no graph present
+        #find one with least distance
+        dists = [c[-1] for c in closestPts]
+        mdist = min(dists)  #Min dist
+        i = dists.index(mdist)  #index for min dist
+        return closestPts[i]  #this is the closest point on closest curve
+
+    def UpdatePointLabel(self, mDataDict):
+        """Updates the pointLabel point on screen with data contained in
+            mDataDict.
+
+            mDataDict will be passed to your function set by
+            SetPointLabelFunc.  It can contain anything you
+            want to display on the screen at the scaledXY point
+            you specify.
+
+            This function can be called from parent window with onClick,
+            onMotion events etc.            
+        """
+        if self.last_PointLabel != None:
+            #compare pointXY
+            if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
+                #closest changed
+                self._drawPointLabel(self.last_PointLabel) #erase old
+                self._drawPointLabel(mDataDict) #plot new
+        else:
+            #just plot new with no erase
+            self._drawPointLabel(mDataDict) #plot new
+        #save for next erase
+        self.last_PointLabel = mDataDict
 
     # event handlers **********************************
     def OnMotion(self, event):
+        if not self.BeenDrawn(): return
         if self._zoomEnabled and event.LeftIsDown():
             if self._hasDragged:
                 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
@@ -847,18 +1156,22 @@ class PlotCanvas(wx.Window):
             self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
 
     def OnMouseLeftDown(self,event):
-        self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
+        if self.BeenDrawn(): 
+               self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
 
     def OnMouseLeftUp(self, event):
+        if not self.BeenDrawn(): return
         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)
+                minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
+                maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
+                self.last_PointLabel = None        #reset pointLabel
+                self._drawCmd.changeAxisX((minX,maxX))
+                self._drawCmd.changeAxisY((minY,maxY))
+                self._draw()
             #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)
@@ -875,7 +1188,10 @@ class PlotCanvas(wx.Window):
 
     def OnPaint(self, event):
         # All that is needed here is to draw the buffer to screen
-        dc = wx.BufferedPaintDC(self, self._Buffer)        
+        if self.last_PointLabel != None:
+            self._drawPointLabel(self.last_PointLabel) #erase old
+            self.last_PointLabel = None
+        dc = wx.BufferedPaintDC(self, self._Buffer)
 
     def OnSize(self,event):
         # The Buffer init is done here, to make sure the buffer is always
@@ -887,11 +1203,19 @@ class PlotCanvas(wx.Window):
         # a file, or whatever.
         self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
         self._setSize()
-        if self.last_draw is None:
-            self.Clear()
+
+        self.last_PointLabel = None        #reset pointLabel
+
+        if self.BeenDrawn():
+            self._draw()
         else:
-            graphics, xSpec, ySpec = self.last_draw
-            self.Draw(graphics,xSpec,ySpec)
+            self.Clear()
+
+    def OnLeave(self, event):
+        """Used to erase pointLabel when mouse outside window"""
+        if self.last_PointLabel != None:
+            self._drawPointLabel(self.last_PointLabel) #erase old
+            self.last_PointLabel = None
 
         
     # Private Methods **************************************************
@@ -901,10 +1225,10 @@ class PlotCanvas(wx.Window):
             (self.width,self.height) = self.GetClientSize()
         else:
             self.width, self.height= width,height    
-        self.plotbox_size = 0.97*Numeric.array([self.width, self.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])
+        self.plotbox_origin = _Numeric.array([xo, yo])
     
     def _setPrinterScale(self, scale):
         """Used to thicken lines and increase marker size for print out."""
@@ -913,34 +1237,41 @@ class PlotCanvas(wx.Window):
      
     def _printDraw(self, printDC):
         """Used for printing."""
-        if self.last_draw != None:
-            graphics, xSpec, ySpec= self.last_draw
-            self.Draw(graphics,xSpec,ySpec,printDC)
+        if self.BeenDrawn():
+            self._draw(printDC)
+
+    def _drawPointLabel(self, mDataDict):
+        """Draws and erases pointLabels"""
+        width = self._Buffer.GetWidth()
+        height = self._Buffer.GetHeight()
+        tmp_Buffer = wx.EmptyBitmap(width,height)
+        dcs = wx.MemoryDC()
+        dcs.SelectObject(tmp_Buffer)
+        dcs.Clear()
+        dcs.BeginDrawing()
+        self._pointLabelFunc(dcs,mDataDict)  #custom user pointLabel function
+        dcs.EndDrawing()
+
+        dc = wx.ClientDC( self )
+        #this will erase if called twice
+        dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV)  #(NOT src) XOR dst
+        
 
-    def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
+    def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, hgap, 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])
+        legendIdx = 0
+        for gr in graphics:
+            s = legendIdx * lineHeight
+            drew = gr.drawLegend(dc, self.printerScale, 
+                                 legendSymExt[0], hgap, legendTextExt[1], 
+                                 trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
+            if drew:
+                legendIdx += 1
         dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
 
     def _titleLablesWH(self, dc, graphics):
@@ -959,23 +1290,25 @@ class PlotCanvas(wx.Window):
         """Returns the size in screen units for legend box"""
         if self._legendEnabled != True:
             legendBoxWH= symExt= txtExt= (0,0)
+            hgap= 0
         else:
-            # find max symbol size
+            #find max symbol size and gap b/w symbol and text
             symExt= graphics.getSymExtent(self.printerScale)
+            hgap = symExt[0]*0.1
             # 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]    
+                txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt))
+            maxW= symExt[0]+hgap+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)
+        return (legendBoxWH, symExt, hgap, txtExt)
 
     def _drawRubberBand(self, corner1, corner2):
         """Draws/erases rect box from corner1 to corner2"""
@@ -1007,14 +1340,14 @@ class PlotCanvas(wx.Window):
 
     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)
+        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
+        pul= _Numeric.minimum(pt1,pt2) # Upper left corner
+        plr= _Numeric.maximum(pt1,pt2) # Lower right corner
         rectWidth, rectHeight= plr-pul
         ptx,pty= pul
         return ptx, pty, rectWidth, rectHeight 
@@ -1030,8 +1363,8 @@ class PlotCanvas(wx.Window):
             range = upper-lower
             if range == 0.:
                 return lower-0.5, upper+0.5
-            log = Numeric.log10(range)
-            power = Numeric.floor(log)
+            log = _Numeric.log10(range)
+            power = _Numeric.floor(log)
             fraction = log-power
             if fraction <= 0.05:
                 power = power-1
@@ -1053,7 +1386,7 @@ class PlotCanvas(wx.Window):
     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'), penWidth))
+        dc.SetPen(wx.Pen(wx.BLACK, penWidth))
         
         # set length of tick marks--long ones make grid
         if self._gridEnabled:
@@ -1068,11 +1401,11 @@ class PlotCanvas(wx.Window):
             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
+                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
+                    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])
@@ -1083,26 +1416,166 @@ class PlotCanvas(wx.Window):
             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
+                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
+                    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):
+
+class DrawCmd:
+    """Represent a "draw" command, i.e. the one given in call to 
+    PlotCanvas.Draw(). The axis specs are not saved to preserve 
+    backward compatibility: they could be specified before the 
+    first Draw() command."""
+    def __init__(self, graphics, xAxis, yAxis, xSpec, ySpec, xTicks, yTicks):
+        """Same args as PlotCanvas.Draw(), plus axis specs."""
+        self.graphics = graphics
+        self.xTickGen = xTicks
+        self.yTickGen = yTicks
+        self.xAxisInit, self.yAxisInit = xAxis, yAxis
+
+        self.xAxis = None
+        self.yAxis = None
+        self.changeAxisX(xAxis, xSpec)
+        self.changeAxisY(yAxis, ySpec)
+        assert self.xAxis is not None
+        assert self.yAxis is not None
+    
+    def isEmpty(self):
+        """Return true if either axis has 0 size."""
+        if self.xAxis[0] == self.xAxis[1]:
+            return True
+        if self.yAxis[0] == self.yAxis[1]:
+            return True
+        
+        return False
+    
+    def resetAxes(self, xSpec, ySpec):
+        """Reset the axes to what they were initially, using axes specs given."""
+        self.changeAxisX(self.xAxisInit, xSpec)
+        self.changeAxisY(self.yAxisInit, ySpec)
+        
+    def getBoundingBox(self):
+        """Returns p1, p2 (two pairs)"""
+        p1 = _Numeric.array((self.xAxis[0], self.yAxis[0]))
+        p2 = _Numeric.array((self.xAxis[1], self.yAxis[1]))
+        return p1, p2
+    
+    def getClosestPoints(self, pntXY, pointScaled=True):
+        ll = []
+        for curveNum,obj in enumerate(self.graphics):
+            #check there are points in the curve
+            if len(obj.points) == 0:
+                continue  #go to next obj
+            #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+            cn = [curveNum, obj.getLegend()]+\
+                  obj.getClosestPoint( pntXY, pointScaled)
+            ll.append(cn)
+        return ll
+
+    def scrollAxisX(self, units, xSpec):
+        """Scroll y axis by units, using ySpec for axis spec."""
+        self.changeAxisX((self.xAxis[0]+units, self.xAxis[1]+units), xSpec)
+        
+    def scrollAxisY(self, units, ySpec):
+        """Scroll y axis by units, using ySpec for axis spec."""
+        self.changeAxisY((self.yAxis[0]+units, self.yAxis[1]+units), ySpec)
+        
+    def changeAxisX(self, xAxis=None, xSpec=None):
+        """Change the x axis to new given, or if None use ySpec to get it. """
+        assert type(xAxis) in [type(None),tuple]
+        if xAxis is None:
+            p1, p2 = self.graphics.boundingBox()     #min, max points of graphics
+            self.xAxis = self._axisInterval(xSpec, p1[0], p2[0]) #in user units
+        else: 
+            self.xAxis = xAxis
+            
+    def changeAxisY(self, yAxis=None, ySpec=None):
+        """Change the y axis to new given, or if None use ySpec to get it. """
+        assert type(yAxis) in [type(None),tuple]
+        if yAxis is None:
+            p1, p2 = self.graphics.boundingBox()     #min, max points of graphics
+            self.yAxis = self._axisInterval(ySpec, p1[1], p2[1])
+        else: 
+            self.yAxis = yAxis
+
+    def zoom(self, center, ratio):
+        """Zoom to center and ratio."""
+        x,y = Center
+        w = (self.xAxis[1] - self.xAxis[0]) * Ratio[0]
+        h = (self.yAxis[1] - self.yAxis[0]) * Ratio[1]
+        self.xAxis = ( x - w/2, x + w/2 )
+        self.yAxis = ( y - h/2, y + h/2 )
+        
+    def getXMaxRange(self):
+        p1, p2 = self.graphics.boundingBox()     #min, max points of graphics
+        return self._axisInterval(self._xSpec, p1[0], p2[0]) #in user units
+        
+    def getYMaxRange(self):
+        p1, p2 = self.graphics.boundingBox()     #min, max points of graphics
+        return self._axisInterval(self._ySpec, p1[1], p2[1]) #in user units
+        
+    def getTicksX(self):
+        """Get the ticks along y axis. Actually pairs of (t, str)."""
+        xticks = self._ticks(self.xAxis[0], self.xAxis[1], self.xTickGen)
+        if xticks == []: # try without the generator
+            xticks = self._ticks(self.xAxis[0], self.xAxis[1])
+        return xticks
+
+    def getTicksY(self):
+        """Get the ticks along y axis. Actually pairs of (t, str)."""
+        yticks = self._ticks(self.yAxis[0], self.yAxis[1], self.yTickGen)
+        if yticks == []: # try without the generator
+            yticks = self._ticks(self.yAxis[0], self.yAxis[1])
+        return yticks
+
+    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 _ticks(self, lower, upper, generator=None):
+        """Get ticks between lower and upper, using generator if given. """
         ideal = (upper-lower)/7.
-        log = Numeric.log10(ideal)
-        power = Numeric.floor(log)
+        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)
+            e = _Numeric.fabs(fraction-lf)
             if e < error:
                 error = e
                 factor = f
@@ -1115,15 +1588,34 @@ class PlotCanvas(wx.Window):
         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
+        
+        if generator is None:
+            return tickGeneratorDefault(lower, upper, grid, format)
+        elif isinstance(generator, list):
+            return tickGeneratorFromList(generator, lower, upper, format)
+        else:
+            return generator(lower, upper, grid, format)
+
+    _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))]
+
 
-    _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))]
+def tickGeneratorDefault(lower, upper, grid, format):
+    """Default tick generator used by PlotCanvas.Draw() if not specified."""
+    ticks = []
+    t = -grid*_Numeric.floor(-lower/grid)
+    while t <= upper:
+        ticks.append( (t, format % (t,)) )
+        t = t + grid
+    return ticks
 
+def tickGeneratorFromList(ticks, lower, upper, format):
+    """Tick generator used by PlotCanvas.Draw() if
+    a list is given for xTicks or yTicks. """
+    tickPairs = []
+    for tick in ticks:
+        if lower <= tick <= upper:
+            tickPairs.append((tick, format % tick))
+    return tickPairs
 
 #-------------------------------------------------------------------------------
 # Used to layout the printer page
@@ -1198,6 +1690,7 @@ class PlotPrintout(wx.Printout):
         # rescale back to original
         self.graph._setSize()
         self.graph._setPrinterScale(1)
+        self.graph.Redraw()     #to get point label scale and shift correct
 
         return True
 
@@ -1212,19 +1705,19 @@ class PlotPrintout(wx.Printout):
 
 def _draw1Objects():
     # 100 points sin function, plotted as green circles
-    data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
+    data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
     data1.shape = (100, 2)
-    data1[:,1] = Numeric.sin(data1[:,0])
+    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 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
     data1.shape = (50,2)
-    data1[:,1] = Numeric.cos(data1[:,0])
+    data1[:,1] = _Numeric.cos(data1[:,0])
     lines = PolyLine(data1, legend= 'Red Line', colour='red')
 
     # A few more points...
-    pi = Numeric.pi
+    pi = _Numeric.pi
     markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
                           (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
                           marker='cross')
@@ -1233,19 +1726,19 @@ def _draw1Objects():
 
 def _draw2Objects():
     # 100 points sin function, plotted as green dots
-    data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
+    data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
     data1.shape = (100, 2)
-    data1[:,1] = Numeric.sin(data1[:,0])
+    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 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
     data1.shape = (50,2)
-    data1[:,1] = Numeric.cos(data1[:,0])
+    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
+    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,
@@ -1264,7 +1757,7 @@ def _draw3Objects():
 
 def _draw4Objects():
     # 25,000 point line
-    data1 = Numeric.arange(5e5,1e6,10)
+    data1 = _Numeric.arange(5e5,1e6,10)
     data1.shape = (25000, 2)
     line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
 
@@ -1298,6 +1791,28 @@ def _draw6Objects():
     return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
                         "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
 
+def _draw7Objects():
+    # four bars of various styles colors and sizes
+    bar1 = PolyBar((0,1), colour='green', size=4, labels='Green bar')
+    bar2 = PolyBar((2,1), colour='red',   size=7, labels='Red bar',
+                    fillstyle=wx.CROSSDIAG_HATCH)
+    bar3 = PolyBar([(1,3),(3,4)], colour='blue',  
+                    labels=['Blue bar 1', 'Blue bar 2'], 
+                    fillstyle=wx.TRANSPARENT)
+    bars = [bar1, bar2, bar3]
+    return PlotGraphics(bars,
+        "Graph Title", "Bar names", "Bar height"), getBarTicksGen(*bars)
+
+def _draw8Objects():
+    # four bars in two groups, overlayed to a line plot
+    x1,x2 = 0.0,0.5
+    bar1 = PolyBar([(x1,1.),(x1+2,2.)], colour='green', size=4, 
+                    legend="1998", fillstyle=wx.CROSSDIAG_HATCH)
+    bar2 = PolyBar(bar1.offset([1.2,2.5]), colour='red', size=7,
+                    legend="2000", labels=['cars','trucks'])
+    line1 = PolyLine([(x1,1.5), (x2,1.2), (x1+2,1), (x2+2,2)], colour='blue')
+    return PlotGraphics([bar1, bar2, line1],
+        "Graph Title", "Bar names", "Bar height"), getBarTicksGen(bar1, bar2)
 
 class TestFrame(wx.Frame):
     def __init__(self, parent, id, title):
@@ -1337,6 +1852,10 @@ class TestFrame(wx.Frame):
         self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
         menu.Append(260, 'Draw6', 'Draw plots6')
         self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
+        menu.Append(261, 'Draw7', 'Draw plots7')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw7, id=261)
+        menu.Append(262, 'Draw8', 'Draw plots8')
+        self.Bind(wx.EVT_MENU,self.OnPlotDraw8, id=262)
        
 
         menu.Append(211, '&Redraw', 'Redraw plots')
@@ -1350,7 +1869,10 @@ class TestFrame(wx.Frame):
         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) 
+        self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
+        menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
+        self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
+       
         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')
@@ -1371,14 +1893,58 @@ class TestFrame(wx.Frame):
         self.CreateStatusBar(1)
         
         self.client = PlotCanvas(self)
+        #define the function for drawing pointLabels
+        self.client.SetPointLabelFunc(self.DrawPointLabel)
         # Create mouse event for showing cursor coords in status bar
         self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+        # Show closest point when enabled
+        self.client.Bind(wx.EVT_MOTION, self.OnMotion)
+
         self.Show(True)
 
+    def DrawPointLabel(self, dc, mDataDict):
+        """This is the fuction that defines how the pointLabels are plotted
+            dc - DC that will be passed
+            mDataDict - Dictionary of data that you want to use for the pointLabel
+
+            As an example I have decided I want a box at the curve point
+            with some text information about the curve plotted below.
+            Any wxDC method can be used.
+        """
+        # ----------
+        dc.SetPen(wx.Pen(wx.BLACK))
+        dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
+        
+        sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
+        dc.DrawRectangle( sx-5,sy-5, 10, 10)  #10by10 square centered on point
+        px,py = mDataDict["pointXY"]
+        cNum = mDataDict["curveNum"]
+        pntIn = mDataDict["pIndex"]
+        legend = mDataDict["legend"]
+        #make a string to display
+        s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
+        dc.DrawText(s, sx , sy+1)
+        # -----------
+
     def OnMouseLeftDown(self,event):
         s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
         self.SetStatusText(s)
-        event.Skip()
+        event.Skip()            #allows plotCanvas OnMouseLeftDown to be called
+
+    def OnMotion(self, event):
+        #show closest point (when enbled)
+        if self.client.GetEnablePointLabel() == True:
+            #make up dict with info for the pointLabel
+            #I've decided to mark the closest point on the closest curve
+            dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
+            if dlst != []:    #returns [] if none
+                curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
+                #make up dictionary to pass to my user function (see DrawPointLabel) 
+                mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
+                            "pointXY":pointXY, "scaledXY":scaledXY}
+                #pass dict to update the pointLabel
+                self.client.UpdatePointLabel(mDataDict)
+        event.Skip()           #go to next handler
 
     def OnFilePageSetup(self, event):
         self.client.PageSetup()
@@ -1416,12 +1982,12 @@ class TestFrame(wx.Frame):
         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
+##        # 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
@@ -1440,6 +2006,22 @@ class TestFrame(wx.Frame):
         self.client.SetYSpec('auto')
         self.client.Draw(_draw6Objects(), xAxis= (0,7))
 
+    def OnPlotDraw7(self, event):
+        #Bar Graph and custom ticks Example, using PolyBar
+        self.resetDefaults()
+        bars, tickGenerator = _draw7Objects()
+        #note that yTicks is not necessary here, used only 
+        #to show off custom ticks
+        self.client.Draw(bars, xAxis=(0,4),yAxis = (0,5), 
+            xTicks=tickGenerator, yTicks=[0,1,3])
+    
+    def OnPlotDraw8(self, event):
+        #Bar Graph and custom ticks Example, using PolyBar
+        self.resetDefaults()
+        plot, tickGenerator = _draw8Objects()
+        self.client.Draw(plot, xAxis=(0,4),yAxis = (0,5), xTicks=tickGenerator)
+    
+
     def OnPlotRedraw(self,event):
         self.client.Redraw()
 
@@ -1447,9 +2029,9 @@ class TestFrame(wx.Frame):
         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))
+        if self.client.BeenDrawn():
+            self.client.ChangeAxes((1,3.05),(0,1))
+
 
     def OnEnableZoom(self, event):
         self.client.SetEnableZoom(event.IsChecked())
@@ -1460,6 +2042,9 @@ class TestFrame(wx.Frame):
     def OnEnableLegend(self, event):
         self.client.SetEnableLegend(event.IsChecked())
 
+    def OnEnablePointLabel(self, event):
+        self.client.SetEnablePointLabel(event.IsChecked())
+
     def OnScrUp(self, event):
         self.client.ScrollUp(1)