gr-wxgui: update copyrights
[debian/gnuradio] / gr-wxgui / src / python / plot.py
1 #-----------------------------------------------------------------------------
2 # Name:        wx.lib.plot.py
3 # Purpose:     Line, Bar and Scatter Graphs
4 #
5 # Author:      Gordon Williams
6 #
7 # Created:     2003/11/03
8 # RCS-ID:      $Id$
9 # Copyright:   (c) 2002,2007,2010
10 # Licence:     Use as you wish.
11 #-----------------------------------------------------------------------------
12 # 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13 #
14 # o 2.5 compatability update.
15 # o Renamed to plot.py in the wx.lib directory.
16 # o Reworked test frame to work with wx demo framework. This saves a bit
17 #   of tedious cut and paste, and the test app is excellent.
18 #
19 # 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
20 #
21 # o wxScrolledMessageDialog -> ScrolledMessageDialog
22 #
23 # Oct 6, 2004  Gordon Williams (g_will@cyberus.ca)
24 #   - Added bar graph demo
25 #   - Modified line end shape from round to square.
26 #   - Removed FloatDCWrapper for conversion to ints and ints in arguments
27 #
28 # Oct 15, 2004  Gordon Williams (g_will@cyberus.ca)
29 #   - Imported modules given leading underscore to name.
30 #   - Added Cursor Line Tracking and User Point Labels. 
31 #   - Demo for Cursor Line Tracking and Point Labels.
32 #   - Size of plot preview frame adjusted to show page better.
33 #   - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
34 #   - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
35 #       can be in either user coords or screen coords.
36 #   
37 # May 27, 2007 Johnathan Corgan (jcorgan@corganenterprises.com)
38 #   - Converted from numarray to numpy
39 #
40 # Apr 23, 2010 Martin Dudok van Heel (http://www.olifantasia.com/gnuradio/contact_olifantasia.gif)
41 #   - Added Persistence option (emulate after glow of an analog CRT display using IIR)
42
43 """
44 This is a simple light weight plotting module that can be used with
45 Boa or easily integrated into your own wxPython application.  The
46 emphasis is on small size and fast plotting for large data sets.  It
47 has a reasonable number of features to do line and scatter graphs
48 easily as well as simple bar graphs.  It is not as sophisticated or 
49 as powerful as SciPy Plt or Chaco.  Both of these are great packages 
50 but consume huge amounts of computer resources for simple plots.
51 They can be found at http://scipy.com
52
53 This file contains two parts; first the re-usable library stuff, then,
54 after a "if __name__=='__main__'" test, a simple frame and a few default
55 plots for examples and testing.
56
57 Based on wxPlotCanvas
58 Written by K.Hinsen, R. Srinivasan;
59 Ported to wxPython Harm van der Heijden, feb 1999
60
61 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
62     -More style options
63     -Zooming using mouse 'rubber band'
64     -Scroll left, right
65     -Grid(graticule)
66     -Printing, preview, and page set up (margins)
67     -Axis and title labels
68     -Cursor xy axis values
69     -Doc strings and lots of comments
70     -Optimizations for large number of points
71     -Legends
72     
73 Did a lot of work here to speed markers up. Only a factor of 4
74 improvement though. Lines are much faster than markers, especially
75 filled markers.  Stay away from circles and triangles unless you
76 only have a few thousand points.
77
78 Times for 25,000 points
79 Line - 0.078 sec
80 Markers
81 Square -                   0.22 sec
82 dot -                      0.10
83 circle -                   0.87
84 cross,plus -               0.28
85 triangle, triangle_down -  0.90
86
87 Thanks to Chris Barker for getting this version working on Linux.
88
89 Zooming controls with mouse (when enabled):
90     Left mouse drag - Zoom box.
91     Left mouse double click - reset zoom.
92     Right mouse click - zoom out centred on click location.
93 """
94
95 import  string as _string
96 import  time as _time
97 import  wx
98
99 # Needs numpy or numarray
100 try:
101     import numpy as _numpy
102 except:
103     try:
104         import numarray as _numpy  #if numarray is used it is renamed numpy
105     except:
106         msg= """
107         This module requires the numpy or numarray module,
108         which could not be imported.  It probably is not installed
109         (it's not part of the standard Python distribution). See the
110         Python site (http://www.python.org) for information on
111         downloading source or binaries."""
112         raise ImportError, "numpy or numarray not found. \n" + msg
113
114
115
116 #
117 # Plotting classes...
118 #
119 class PolyPoints:
120     """Base Class for lines and markers
121         - All methods are private.
122     """
123
124     def __init__(self, points, attr):
125         self.points = _numpy.array(points)
126         self.currentScale= (1,1)
127         self.currentShift= (0,0)
128         self.scaled = self.points
129         self.attributes = {}
130         self.attributes.update(self._attributes)
131         for name, value in attr.items():   
132             if name not in self._attributes.keys():
133                 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
134             self.attributes[name] = value
135         
136     def boundingBox(self):
137         if len(self.points) == 0:
138             # no curves to draw
139             # defaults to (-1,-1) and (1,1) but axis can be set in Draw
140             minXY= _numpy.array([-1,-1])
141             maxXY= _numpy.array([ 1, 1])
142         else:
143             minXY= _numpy.minimum.reduce(self.points)
144             maxXY= _numpy.maximum.reduce(self.points)
145         return minXY, maxXY
146
147     def scaleAndShift(self, scale=(1,1), shift=(0,0)):
148         if len(self.points) == 0:
149             # no curves to draw
150             return
151         if (scale is not self.currentScale) or (shift is not self.currentShift):
152             # update point scaling
153             self.scaled = scale*self.points+shift
154             self.currentScale= scale
155             self.currentShift= shift
156         # else unchanged use the current scaling
157         
158     def getLegend(self):
159         return self.attributes['legend']
160
161     def getClosestPoint(self, pntXY, pointScaled= True):
162         """Returns the index of closest point on the curve, pointXY, scaledXY, distance
163             x, y in user coords
164             if pointScaled == True based on screen coords
165             if pointScaled == False based on user coords
166         """
167         if pointScaled == True:
168             #Using screen coords
169             p = self.scaled
170             pxy = self.currentScale * _numpy.array(pntXY)+ self.currentShift
171         else:
172             #Using user coords
173             p = self.points
174             pxy = _numpy.array(pntXY)
175         #determine distance for each point
176         d= _numpy.sqrt(_numpy.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
177         pntIndex = _numpy.argmin(d)
178         dist = d[pntIndex]
179         return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
180         
181         
182 class PolyLine(PolyPoints):
183     """Class to define line type and style
184         - All methods except __init__ are private.
185     """
186     
187     _attributes = {'colour': 'black',
188                    'width': 1,
189                    'style': wx.SOLID,
190                    'legend': ''}
191
192     def __init__(self, points, **attr):
193         """Creates PolyLine object
194             points - sequence (array, tuple or list) of (x,y) points making up line
195             **attr - key word attributes
196                 Defaults:
197                     'colour'= 'black',          - wx.Pen Colour any wx.NamedColour
198                     'width'= 1,                 - Pen width
199                     'style'= wx.SOLID,          - wx.Pen style
200                     'legend'= ''                - Line Legend to display
201         """
202         PolyPoints.__init__(self, points, attr)
203
204     def draw(self, dc, printerScale, coord= None):
205         colour = self.attributes['colour']
206         width = self.attributes['width'] * printerScale
207         style= self.attributes['style']
208         pen = wx.Pen(wx.NamedColour(colour), width, style)
209         pen.SetCap(wx.CAP_BUTT)
210         dc.SetPen(pen)
211         if coord == None:
212             dc.DrawLines(self.scaled)
213         else:
214             dc.DrawLines(coord) # draw legend line
215
216     def getSymExtent(self, printerScale):
217         """Width and Height of Marker"""
218         h= self.attributes['width'] * printerScale
219         w= 5 * h
220         return (w,h)
221
222
223 class PolyMarker(PolyPoints):
224     """Class to define marker type and style
225         - All methods except __init__ are private.
226     """
227   
228     _attributes = {'colour': 'black',
229                    'width': 1,
230                    'size': 2,
231                    'fillcolour': None,
232                    'fillstyle': wx.SOLID,
233                    'marker': 'circle',
234                    'legend': ''}
235
236     def __init__(self, points, **attr):
237         """Creates PolyMarker object
238         points - sequence (array, tuple or list) of (x,y) points
239         **attr - key word attributes
240             Defaults:
241                 'colour'= 'black',          - wx.Pen Colour any wx.NamedColour
242                 'width'= 1,                 - Pen width
243                 'size'= 2,                  - Marker size
244                 'fillcolour'= same as colour,      - wx.Brush Colour any wx.NamedColour
245                 'fillstyle'= wx.SOLID,      - wx.Brush fill style (use wx.TRANSPARENT for no fill)
246                 'marker'= 'circle'          - Marker shape
247                 'legend'= ''                - Marker Legend to display
248               
249             Marker Shapes:
250                 - 'circle'
251                 - 'dot'
252                 - 'square'
253                 - 'triangle'
254                 - 'triangle_down'
255                 - 'cross'
256                 - 'plus'
257         """
258       
259         PolyPoints.__init__(self, points, attr)
260
261     def draw(self, dc, printerScale, coord= None):
262         colour = self.attributes['colour']
263         width = self.attributes['width'] * printerScale
264         size = self.attributes['size'] * printerScale
265         fillcolour = self.attributes['fillcolour']
266         fillstyle = self.attributes['fillstyle']
267         marker = self.attributes['marker']
268
269         dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
270         if fillcolour:
271             dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
272         else:
273             dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
274         if coord == None:
275             self._drawmarkers(dc, self.scaled, marker, size)
276         else:
277             self._drawmarkers(dc, coord, marker, size) # draw legend marker
278
279     def getSymExtent(self, printerScale):
280         """Width and Height of Marker"""
281         s= 5*self.attributes['size'] * printerScale
282         return (s,s)
283
284     def _drawmarkers(self, dc, coords, marker,size=1):
285         f = eval('self._' +marker)
286         f(dc, coords, size)
287
288     def _circle(self, dc, coords, size=1):
289         fact= 2.5*size
290         wh= 5.0*size
291         rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
292         rect[:,0:2]= coords-[fact,fact]
293         dc.DrawEllipseList(rect.astype(_numpy.int32))
294
295     def _dot(self, dc, coords, size=1):
296         dc.DrawPointList(coords)
297
298     def _square(self, dc, coords, size=1):
299         fact= 2.5*size
300         wh= 5.0*size
301         rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
302         rect[:,0:2]= coords-[fact,fact]
303         dc.DrawRectangleList(rect.astype(_numpy.int32))
304
305     def _triangle(self, dc, coords, size=1):
306         shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
307         poly= _numpy.repeat(coords,3)
308         poly.shape= (len(coords),3,2)
309         poly += shape
310         dc.DrawPolygonList(poly.astype(_numpy.int32))
311
312     def _triangle_down(self, dc, coords, size=1):
313         shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
314         poly= _numpy.repeat(coords,3)
315         poly.shape= (len(coords),3,2)
316         poly += shape
317         dc.DrawPolygonList(poly.astype(_numpy.int32))
318       
319     def _cross(self, dc, coords, size=1):
320         fact= 2.5*size
321         for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
322             lines= _numpy.concatenate((coords,coords),axis=1)+f
323             dc.DrawLineList(lines.astype(_numpy.int32))
324
325     def _plus(self, dc, coords, size=1):
326         fact= 2.5*size
327         for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
328             lines= _numpy.concatenate((coords,coords),axis=1)+f
329             dc.DrawLineList(lines.astype(_numpy.int32))
330
331 class PlotGraphics:
332     """Container to hold PolyXXX objects and graph labels
333         - All methods except __init__ are private.
334     """
335
336     def __init__(self, objects, title='', xLabel='', yLabel= ''):
337         """Creates PlotGraphics object
338         objects - list of PolyXXX objects to make graph
339         title - title shown at top of graph
340         xLabel - label shown on x-axis
341         yLabel - label shown on y-axis
342         """
343         if type(objects) not in [list,tuple]:
344             raise TypeError, "objects argument should be list or tuple"
345         self.objects = objects
346         self.title= title
347         self.xLabel= xLabel
348         self.yLabel= yLabel
349
350     def boundingBox(self):
351         p1, p2 = self.objects[0].boundingBox()
352         for o in self.objects[1:]:
353             p1o, p2o = o.boundingBox()
354             p1 = _numpy.minimum(p1, p1o)
355             p2 = _numpy.maximum(p2, p2o)
356         return p1, p2
357
358     def scaleAndShift(self, scale=(1,1), shift=(0,0)):
359         for o in self.objects:
360             o.scaleAndShift(scale, shift)
361
362     def setPrinterScale(self, scale):
363         """Thickens up lines and markers only for printing"""
364         self.printerScale= scale
365
366     def setXLabel(self, xLabel= ''):
367         """Set the X axis label on the graph"""
368         self.xLabel= xLabel
369
370     def setYLabel(self, yLabel= ''):
371         """Set the Y axis label on the graph"""
372         self.yLabel= yLabel
373         
374     def setTitle(self, title= ''):
375         """Set the title at the top of graph"""
376         self.title= title
377
378     def getXLabel(self):
379         """Get x axis label string"""
380         return self.xLabel
381
382     def getYLabel(self):
383         """Get y axis label string"""
384         return self.yLabel
385
386     def getTitle(self, title= ''):
387         """Get the title at the top of graph"""
388         return self.title
389
390     def draw(self, dc):
391         for o in self.objects:
392             #t=_time.clock()          # profile info
393             o.draw(dc, self.printerScale)
394             #dt= _time.clock()-t
395             #print o, "time=", dt
396
397     def getSymExtent(self, printerScale):
398         """Get max width and height of lines and markers symbols for legend"""
399         symExt = self.objects[0].getSymExtent(printerScale)
400         for o in self.objects[1:]:
401             oSymExt = o.getSymExtent(printerScale)
402             symExt = _numpy.maximum(symExt, oSymExt)
403         return symExt
404     
405     def getLegendNames(self):
406         """Returns list of legend names"""
407         lst = [None]*len(self)
408         for i in range(len(self)):
409             lst[i]= self.objects[i].getLegend()
410         return lst
411             
412     def __len__(self):
413         return len(self.objects)
414
415     def __getitem__(self, item):
416         return self.objects[item]
417
418
419 #-------------------------------------------------------------------------------
420 # Main window that you will want to import into your application.
421
422 class PlotCanvas(wx.Window):
423     """Subclass of a wx.Window to allow simple general plotting
424     of data with zoom, labels, and automatic axis scaling."""
425
426     def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
427             size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
428
429         self.use_persistence=False
430         self.alpha=0.3
431         self.decimation=10
432         self.decim_counter=0
433         """Constucts a window, which can be a child of a frame, dialog or
434         any other non-control window"""
435     
436         wx.Window.__init__(self, parent, id, pos, size, style, name)
437         self.border = (1,1)
438
439         self.SetBackgroundColour("white")
440         
441         # Create some mouse events for zooming
442         self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
443         self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
444         self.Bind(wx.EVT_MOTION, self.OnMotion)
445         self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
446         self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
447
448         # set curser as cross-hairs
449         self.SetCursor(wx.CROSS_CURSOR)
450
451         # Things for printing
452         self.print_data = wx.PrintData()
453         self.print_data.SetPaperId(wx.PAPER_LETTER)
454         self.print_data.SetOrientation(wx.LANDSCAPE)
455         self.pageSetupData= wx.PageSetupDialogData()
456         self.pageSetupData.SetMarginBottomRight((25,25))
457         self.pageSetupData.SetMarginTopLeft((25,25))
458         self.pageSetupData.SetPrintData(self.print_data)
459         self.printerScale = 1
460         self.parent= parent
461
462         # Zooming variables
463         self._zoomInFactor =  0.5
464         self._zoomOutFactor = 2
465         self._zoomCorner1= _numpy.array([0.0, 0.0]) # left mouse down corner
466         self._zoomCorner2= _numpy.array([0.0, 0.0])   # left mouse up corner
467         self._zoomEnabled= False
468         self._hasDragged= False
469         
470         # Drawing Variables
471         self.last_draw = None
472         self._pointScale= 1
473         self._pointShift= 0
474         self._xSpec= 'auto'
475         self._ySpec= 'auto'
476         self._gridEnabled= False
477         self._legendEnabled= False
478         self._xUseScopeTicks= False
479         
480         # Fonts
481         self._fontCache = {}
482         self._fontSizeAxis= 10
483         self._fontSizeTitle= 15
484         self._fontSizeLegend= 7
485
486         # pointLabels
487         self._pointLabelEnabled= False
488         self.last_PointLabel= None
489         self._pointLabelFunc= None
490         self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
491
492         self.Bind(wx.EVT_PAINT, self.OnPaint)
493         self.Bind(wx.EVT_SIZE, self.OnSize)
494         # OnSize called to make sure the buffer is initialized.
495         # This might result in OnSize getting called twice on some
496         # platforms at initialization, but little harm done.
497         self.OnSize(None) # sets the initial size based on client size
498                           # UNCONDITIONAL, needed to create self._Buffer
499
500
501     def set_use_persistence(self, enable):
502         self.use_persistence = enable
503
504     def set_persist_alpha(self, persist_alpha):
505         self.alpha = persist_alpha
506
507         
508     # SaveFile
509     def SaveFile(self, fileName= ''):
510         """Saves the file to the type specified in the extension. If no file
511         name is specified a dialog box is provided.  Returns True if sucessful,
512         otherwise False.
513         
514         .bmp  Save a Windows bitmap file.
515         .xbm  Save an X bitmap file.
516         .xpm  Save an XPM bitmap file.
517         .png  Save a Portable Network Graphics file.
518         .jpg  Save a Joint Photographic Experts Group file.
519         """
520         if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
521             dlg1 = wx.FileDialog(
522                     self, 
523                     "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
524                     "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
525                     wx.SAVE|wx.OVERWRITE_PROMPT
526                     )
527             try:
528                 while 1:
529                     if dlg1.ShowModal() == wx.ID_OK:
530                         fileName = dlg1.GetPath()
531                         # Check for proper exension
532                         if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
533                             dlg2 = wx.MessageDialog(self, 'File name extension\n'
534                             'must be one of\n'
535                             'bmp, xbm, xpm, png, or jpg',
536                               'File Name Error', wx.OK | wx.ICON_ERROR)
537                             try:
538                                 dlg2.ShowModal()
539                             finally:
540                                 dlg2.Destroy()
541                         else:
542                             break # now save file
543                     else: # exit without saving
544                         return False
545             finally:
546                 dlg1.Destroy()
547
548         # File name has required extension
549         fType = _string.lower(fileName[-3:])
550         if fType == "bmp":
551             tp= wx.BITMAP_TYPE_BMP       # Save a Windows bitmap file.
552         elif fType == "xbm":
553             tp= wx.BITMAP_TYPE_XBM       # Save an X bitmap file.
554         elif fType == "xpm":
555             tp= wx.BITMAP_TYPE_XPM       # Save an XPM bitmap file.
556         elif fType == "jpg":
557             tp= wx.BITMAP_TYPE_JPEG      # Save a JPG file.
558         else:
559             tp= wx.BITMAP_TYPE_PNG       # Save a PNG file.
560         # Save Bitmap
561         res= self._Buffer.SaveFile(fileName, tp)
562         return res
563
564     def PageSetup(self):
565         """Brings up the page setup dialog"""
566         data = self.pageSetupData
567         data.SetPrintData(self.print_data)
568         dlg = wx.PageSetupDialog(self.parent, data)
569         try:
570             if dlg.ShowModal() == wx.ID_OK:
571                 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
572                 # updates page parameters from dialog
573                 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
574                 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
575                 self.pageSetupData.SetPrintData(data.GetPrintData())
576                 self.print_data=data.GetPrintData() # updates print_data
577         finally:
578             dlg.Destroy()
579                 
580     def Printout(self, paper=None):
581         """Print current plot."""
582         if paper != None:
583             self.print_data.SetPaperId(paper)
584         pdd = wx.PrintDialogData()
585         pdd.SetPrintData(self.print_data)
586         printer = wx.Printer(pdd)
587         out = PlotPrintout(self)
588         print_ok = printer.Print(self.parent, out)
589         if print_ok:
590             self.print_data = printer.GetPrintDialogData().GetPrintData()
591         out.Destroy()
592
593     def PrintPreview(self):
594         """Print-preview current plot."""
595         printout = PlotPrintout(self)
596         printout2 = PlotPrintout(self)
597         self.preview = wx.PrintPreview(printout, printout2, self.print_data)
598         if not self.preview.Ok():
599             wx.MessageDialog(self, "Print Preview failed.\n" \
600                                "Check that default printer is configured\n", \
601                                "Print error", wx.OK|wx.CENTRE).ShowModal()
602         self.preview.SetZoom(40)
603         # search up tree to find frame instance
604         frameInst= self
605         while not isinstance(frameInst, wx.Frame):
606             frameInst= frameInst.GetParent()
607         frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
608         frame.Initialize()
609         frame.SetPosition(self.GetPosition())
610         frame.SetSize((600,550))
611         frame.Centre(wx.BOTH)
612         frame.Show(True)
613
614     def SetFontSizeAxis(self, point= 10):
615         """Set the tick and axis label font size (default is 10 point)"""
616         self._fontSizeAxis= point
617         
618     def GetFontSizeAxis(self):
619         """Get current tick and axis label font size in points"""
620         return self._fontSizeAxis
621     
622     def SetFontSizeTitle(self, point= 15):
623         """Set Title font size (default is 15 point)"""
624         self._fontSizeTitle= point
625
626     def GetFontSizeTitle(self):
627         """Get current Title font size in points"""
628         return self._fontSizeTitle
629     
630     def SetFontSizeLegend(self, point= 7):
631         """Set Legend font size (default is 7 point)"""
632         self._fontSizeLegend= point
633         
634     def GetFontSizeLegend(self):
635         """Get current Legend font size in points"""
636         return self._fontSizeLegend
637
638     def SetEnableZoom(self, value):
639         """Set True to enable zooming."""
640         if value not in [True,False]:
641             raise TypeError, "Value should be True or False"
642         self._zoomEnabled= value
643
644     def GetEnableZoom(self):
645         """True if zooming enabled."""
646         return self._zoomEnabled
647
648     def SetEnableGrid(self, value):
649         """Set True to enable grid."""
650         if value not in [True,False]:
651             raise TypeError, "Value should be True or False"
652         self._gridEnabled= value
653         self.Redraw()
654
655     def GetEnableGrid(self):
656         """True if grid enabled."""
657         return self._gridEnabled
658
659     def SetEnableLegend(self, value):
660         """Set True to enable legend."""
661         if value not in [True,False]:
662             raise TypeError, "Value should be True or False"
663         self._legendEnabled= value 
664         self.Redraw()
665
666     def GetEnableLegend(self):
667         """True if Legend enabled."""
668         return self._legendEnabled
669
670     def SetEnablePointLabel(self, value):
671         """Set True to enable pointLabel."""
672         if value not in [True,False]:
673             raise TypeError, "Value should be True or False"
674         self._pointLabelEnabled= value 
675         self.Redraw()  #will erase existing pointLabel if present
676         self.last_PointLabel = None
677
678     def GetEnablePointLabel(self):
679         """True if pointLabel enabled."""
680         return self._pointLabelEnabled
681
682     def SetPointLabelFunc(self, func):
683         """Sets the function with custom code for pointLabel drawing
684             ******** more info needed ***************
685         """
686         self._pointLabelFunc= func
687
688     def GetPointLabelFunc(self):
689         """Returns pointLabel Drawing Function"""
690         return self._pointLabelFunc
691
692     def Reset(self):
693         """Unzoom the plot."""
694         self.last_PointLabel = None        #reset pointLabel
695         if self.last_draw is not None:
696             self.Draw(self.last_draw[0])
697         
698     def ScrollRight(self, units):          
699         """Move view right number of axis units."""
700         self.last_PointLabel = None        #reset pointLabel
701         if self.last_draw is not None:
702             graphics, xAxis, yAxis= self.last_draw
703             xAxis= (xAxis[0]+units, xAxis[1]+units)
704             self.Draw(graphics,xAxis,yAxis)
705
706     def ScrollUp(self, units):
707         """Move view up number of axis units."""
708         self.last_PointLabel = None        #reset pointLabel
709         if self.last_draw is not None:
710             graphics, xAxis, yAxis= self.last_draw
711             yAxis= (yAxis[0]+units, yAxis[1]+units)
712             self.Draw(graphics,xAxis,yAxis)
713         
714     def GetXY(self,event):
715         """Takes a mouse event and returns the XY user axis values."""
716         x,y= self.PositionScreenToUser(event.GetPosition())
717         return x,y
718
719     def PositionUserToScreen(self, pntXY):
720         """Converts User position to Screen Coordinates"""
721         userPos= _numpy.array(pntXY)
722         x,y= userPos * self._pointScale + self._pointShift
723         return x,y
724         
725     def PositionScreenToUser(self, pntXY):
726         """Converts Screen position to User Coordinates"""
727         screenPos= _numpy.array(pntXY)
728         x,y= (screenPos-self._pointShift)/self._pointScale
729         return x,y
730         
731     def SetXSpec(self, type= 'auto'):
732         """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
733         where:
734             'none' - shows no axis or tick mark values
735             'min' - shows min bounding box values
736             'auto' - rounds axis range to sensible values
737         """
738         self._xSpec= type
739         
740     def SetYSpec(self, type= 'auto'):
741         """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
742         where:
743             'none' - shows no axis or tick mark values
744             'min' - shows min bounding box values
745             'auto' - rounds axis range to sensible values
746         """
747         self._ySpec= type
748
749     def GetXSpec(self):
750         """Returns current XSpec for axis"""
751         return self._xSpec
752     
753     def GetYSpec(self):
754         """Returns current YSpec for axis"""
755         return self._ySpec
756     
757     def GetXMaxRange(self):
758         """Returns (minX, maxX) x-axis range for displayed graph"""
759         graphics= self.last_draw[0]
760         p1, p2 = graphics.boundingBox()     # min, max points of graphics
761         xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
762         return xAxis
763
764     def GetYMaxRange(self):
765         """Returns (minY, maxY) y-axis range for displayed graph"""
766         graphics= self.last_draw[0]
767         p1, p2 = graphics.boundingBox()     # min, max points of graphics
768         yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
769         return yAxis
770
771     def GetXCurrentRange(self):
772         """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
773         return self.last_draw[1]
774     
775     def GetYCurrentRange(self):
776         """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
777         return self.last_draw[2]
778         
779     def SetXUseScopeTicks(self, v=False):
780         """Always 10 divisions, no labels"""
781         self._xUseScopeTicks = v
782         
783     def GetXUseScopeTicks(self):
784         return self._xUseScopeTicks
785
786     def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, step=None):
787         """Draw objects in graphics with specified x and y axis.
788         graphics- instance of PlotGraphics with list of PolyXXX objects
789         xAxis - tuple with (min, max) axis range to view
790         yAxis - same as xAxis
791         dc - drawing context - doesn't have to be specified.    
792         If it's not, the offscreen buffer is used
793         """
794         # check Axis is either tuple or none
795         if type(xAxis) not in [type(None),tuple]:
796             raise TypeError, "xAxis should be None or (minX,maxX)"
797         if type(yAxis) not in [type(None),tuple]:
798             raise TypeError, "yAxis should be None or (minY,maxY)"
799              
800         # check case for axis = (a,b) where a==b caused by improper zooms
801         if xAxis != None:
802             if xAxis[0] == xAxis[1]:
803                 return
804         if yAxis != None:
805             if yAxis[0] == yAxis[1]:
806                 return
807             
808         if dc == None:
809             # sets new dc and clears it 
810             if self.use_persistence:
811               dc = wx.MemoryDC()
812               dc.SelectObject(self._Buffer)
813               dc.Clear()
814             else:
815               dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
816               dc.Clear() 
817            
818         dc.BeginDrawing()
819         # dc.Clear()
820  
821
822        
823         # set font size for every thing but title and legend
824         dc.SetFont(self._getFont(self._fontSizeAxis))
825
826         # sizes axis to axis type, create lower left and upper right corners of plot
827         if xAxis == None or yAxis == None:
828             # One or both axis not specified in Draw
829             p1, p2 = graphics.boundingBox()     # min, max points of graphics
830             if xAxis == None:
831                 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
832             if yAxis == None:
833                 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
834             # Adjust bounding box for axis spec
835             p1[0],p1[1] = xAxis[0], yAxis[0]     # lower left corner user scale (xmin,ymin)
836             p2[0],p2[1] = xAxis[1], yAxis[1]     # upper right corner user scale (xmax,ymax)
837         else:
838             # Both axis specified in Draw
839             p1= _numpy.array([xAxis[0], yAxis[0]])    # lower left corner user scale (xmin,ymin)
840             p2= _numpy.array([xAxis[1], yAxis[1]])     # upper right corner user scale (xmax,ymax)
841
842         self.last_draw = (graphics, xAxis, yAxis)       # saves most recient values
843
844         if False:
845           ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
846           #dc.SetPen(wx.Pen(wx.BLACK))
847           dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) #wx.SOLID wx.TRANSPARENT ) )
848           #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT
849           dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
850           #dc.SetBrush(wx.Brush( wx.WHITE, wx.SOLID ) ) 
851           #dc.SetLogicalFunction(wx.COPY)
852
853         # Get ticks and textExtents for axis if required
854         if self._xSpec is not 'none':
855             if self._xUseScopeTicks:
856                 xticks = self._scope_ticks(xAxis[0], xAxis[1])
857             else:
858                 xticks = self._ticks(xAxis[0], xAxis[1])
859             xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
860         else:
861             xticks = None
862             xTextExtent= (0,0) # No text for ticks
863         if self._ySpec is not 'none':
864             yticks = self._ticks(yAxis[0], yAxis[1], step)
865             yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
866             yTextExtentTop   = dc.GetTextExtent(yticks[-1][1])
867             yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
868                           max(yTextExtentBottom[1],yTextExtentTop[1]))
869         else:
870             yticks = None
871             yTextExtent= (0,0) # No text for ticks
872
873         # TextExtents for Title and Axis Labels
874         titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
875
876         # TextExtents for Legend
877         legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
878
879         # room around graph area
880         rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
881         lhsW= yTextExtent[0]+ yLabelWH[1]
882         bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
883         topH= yTextExtent[1]/2. + titleWH[1]
884         textSize_scale= _numpy.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
885         textSize_shift= _numpy.array([lhsW, bottomH])          # shift plot area by this amount
886
887         # drawing title and labels text
888         dc.SetFont(self._getFont(self._fontSizeTitle))
889         titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
890                  self.plotbox_origin[1]- self.plotbox_size[1])
891         dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
892         dc.SetFont(self._getFont(self._fontSizeAxis))
893         xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
894                  self.plotbox_origin[1]- xLabelWH[1])
895         dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
896         yLabelPos= (self.plotbox_origin[0],
897                  self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
898         if graphics.getYLabel():  # bug fix for Linux
899             dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
900
901         # drawing legend makers and text
902         if self._legendEnabled:
903             self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
904
905         # allow for scaling and shifting plotted points
906         scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _numpy.array((1,-1))
907         shift = -p1*scale + self.plotbox_origin + textSize_shift * _numpy.array((1,-1))
908         self._pointScale= scale  # make available for mouse events
909         self._pointShift= shift
910
911         #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT      
912         self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
913         #dc.SetLogicalFunction(wx.COPY) 
914         
915         graphics.scaleAndShift(scale, shift)
916         graphics.setPrinterScale(self.printerScale)  # thicken up lines and markers if printing
917         
918         # set clipping area so drawing does not occur outside axis box
919         ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
920         dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
921         # Draw the lines and markers
922         #start = _time.clock()
923
924         graphics.draw(dc)
925         # print "entire graphics drawing took: %f second"%(_time.clock() - start)
926         # remove the clipping region
927         dc.DestroyClippingRegion()
928         dc.EndDrawing()
929
930
931         if self.use_persistence:
932           dc=None
933           self._Buffer.CopyToBuffer(self._Bufferarray) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
934           ## do the IIR filter
935           alpha_int=int(float(self.alpha*256))
936           if True:
937             _numpy.add(self._Bufferarray,0,self._Buffer3array)
938             _numpy.multiply(self._Buffer3array,alpha_int,self._Buffer3array)
939             _numpy.multiply(self._Buffer2array,(256-alpha_int),self._Buffer2array)
940             _numpy.add(self._Buffer3array,self._Buffer2array,self._Buffer2array)
941             _numpy.right_shift(self._Buffer2array,8,self._Buffer2array)
942           elif False:
943             self._Buffer2array=(self._Bufferarray.astype(_numpy.uint32) *alpha_int + self._Buffer2array*(256-alpha_int)).__rshift__(8)
944           elif False:
945             self._Buffer2array *=(256-alpha_int)
946             self._Buffer2array +=self._Bufferarray.astype(_numpy.uint32)*alpha_int
947             self._Buffer2array /=256
948
949           ##copy back to image buffer 
950           self._Buffer2.CopyFromBuffer(self._Buffer2array.astype(_numpy.uint8)) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
951
952           #draw to the screen
953           #self.decim_counter=self.decim_counter+1
954           if True: #self.decim_counter>self.decimation:
955             #self.decim_counter=0
956             dc2 = wx.ClientDC( self )
957             dc2.BeginDrawing()
958             dc2.DrawBitmap(self._Buffer2, 0, 0, False)
959             #dc2.DrawBitmap(self._Buffer, 0, 0, False)
960             dc2.EndDrawing()
961         
962     def Redraw(self, dc= None):
963         """Redraw the existing plot."""
964         if self.last_draw is not None:
965             graphics, xAxis, yAxis= self.last_draw
966             self.Draw(graphics,xAxis,yAxis,dc)
967
968     def Clear(self):
969         """Erase the window."""
970         self.last_PointLabel = None        #reset pointLabel
971         dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
972         dc.Clear()
973         self.last_draw = None
974
975     def Zoom(self, Center, Ratio):
976         """ Zoom on the plot
977             Centers on the X,Y coords given in Center
978             Zooms by the Ratio = (Xratio, Yratio) given
979         """
980         self.last_PointLabel = None   #reset maker
981         x,y = Center
982         if self.last_draw != None:
983             (graphics, xAxis, yAxis) = self.last_draw
984             w = (xAxis[1] - xAxis[0]) * Ratio[0]
985             h = (yAxis[1] - yAxis[0]) * Ratio[1]
986             xAxis = ( x - w/2, x + w/2 )
987             yAxis = ( y - h/2, y + h/2 )
988             self.Draw(graphics, xAxis, yAxis)
989         
990     def GetClosestPoints(self, pntXY, pointScaled= True):
991         """Returns list with
992             [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
993             list for each curve.
994             Returns [] if no curves are being plotted.
995             
996             x, y in user coords
997             if pointScaled == True based on screen coords
998             if pointScaled == False based on user coords
999         """
1000         if self.last_draw == None:
1001             #no graph available
1002             return []
1003         graphics, xAxis, yAxis= self.last_draw
1004         l = []
1005         for curveNum,obj in enumerate(graphics):
1006             #check there are points in the curve
1007             if len(obj.points) == 0:
1008                 continue  #go to next obj
1009             #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1010             cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
1011             l.append(cn)
1012         return l
1013
1014     def GetClosetPoint(self, pntXY, pointScaled= True):
1015         """Returns list with
1016             [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1017             list for only the closest curve.
1018             Returns [] if no curves are being plotted.
1019             
1020             x, y in user coords
1021             if pointScaled == True based on screen coords
1022             if pointScaled == False based on user coords
1023         """
1024         #closest points on screen based on screen scaling (pointScaled= True)
1025         #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
1026         closestPts= self.GetClosestPoints(pntXY, pointScaled)
1027         if closestPts == []:
1028             return []  #no graph present
1029         #find one with least distance
1030         dists = [c[-1] for c in closestPts]
1031         mdist = min(dists)  #Min dist
1032         i = dists.index(mdist)  #index for min dist
1033         return closestPts[i]  #this is the closest point on closest curve
1034
1035     def UpdatePointLabel(self, mDataDict):
1036         """Updates the pointLabel point on screen with data contained in
1037             mDataDict.
1038
1039             mDataDict will be passed to your function set by
1040             SetPointLabelFunc.  It can contain anything you
1041             want to display on the screen at the scaledXY point
1042             you specify.
1043
1044             This function can be called from parent window with onClick,
1045             onMotion events etc.            
1046         """
1047         if self.last_PointLabel != None:
1048             #compare pointXY
1049             if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
1050                 #closest changed
1051                 self._drawPointLabel(self.last_PointLabel) #erase old
1052                 self._drawPointLabel(mDataDict) #plot new
1053         else:
1054             #just plot new with no erase
1055             self._drawPointLabel(mDataDict) #plot new
1056         #save for next erase
1057         self.last_PointLabel = mDataDict
1058
1059     # event handlers **********************************
1060     def OnMotion(self, event):
1061         if self._zoomEnabled and event.LeftIsDown():
1062             if self._hasDragged:
1063                 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1064             else:
1065                 self._hasDragged= True
1066             self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
1067             self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
1068
1069     def OnMouseLeftDown(self,event):
1070         self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
1071
1072     def OnMouseLeftUp(self, event):
1073         if self._zoomEnabled:
1074             if self._hasDragged == True:
1075                 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1076                 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
1077                 self._hasDragged = False  # reset flag
1078                 minX, minY= _numpy.minimum( self._zoomCorner1, self._zoomCorner2)
1079                 maxX, maxY= _numpy.maximum( self._zoomCorner1, self._zoomCorner2)
1080                 self.last_PointLabel = None        #reset pointLabel
1081                 if self.last_draw != None:
1082                     self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
1083             #else: # A box has not been drawn, zoom in on a point
1084             ## this interfered with the double click, so I've disables it.
1085             #    X,Y = self.GetXY(event)
1086             #    self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1087
1088     def OnMouseDoubleClick(self,event):
1089         if self._zoomEnabled:
1090             self.Reset()
1091         
1092     def OnMouseRightDown(self,event):
1093         if self._zoomEnabled:
1094             X,Y = self.GetXY(event)
1095             self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1096
1097     def OnPaint(self, event):
1098         # All that is needed here is to draw the buffer to screen
1099         if self.last_PointLabel != None:
1100             self._drawPointLabel(self.last_PointLabel) #erase old
1101             self.last_PointLabel = None
1102
1103         #paint current buffer to screen
1104         dc = wx.BufferedPaintDC(self, self._Buffer)
1105
1106     def OnSize(self,event):
1107         # The Buffer init is done here, to make sure the buffer is always
1108         # the same size as the Window
1109         Size  = self.GetClientSize()
1110
1111         # Make new offscreen bitmap: this bitmap will always have the
1112         # current drawing in it, so it can be used to save the image to
1113         # a file, or whatever.
1114         self._Buffer = wx.EmptyBitmap(Size[0],Size[1],24)
1115
1116         
1117         if True: #self.use_persistence:
1118           #self._Bufferarray = _numpy.zeros((Size[0], Size[1],3), dtype=_numpy.uint8)
1119           self._Bufferarray = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint8)
1120
1121           # Make new second offscreen bitmap: this bitmap will always have the
1122           # last drawing in it, so it can be used to do display time dependent processing 
1123           # like averaging (IIR) or show differences between updates
1124           self._Buffer2 = wx.EmptyBitmap(Size[0],Size[1],24)
1125           # now the extra buffers for the IIR processing
1126           # note the different datatype uint32
1127           self._Buffer2array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
1128           self._Buffer3array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
1129           # optional you can set the ufunct buffer size to improve speed
1130           #_numpy.setbufsize(16*((Size[0]* Size[1]*3)/16 +1))
1131         self._setSize()
1132
1133         self.last_PointLabel = None        #reset pointLabel
1134
1135         if self.last_draw is None:
1136             self.Clear()
1137         else:
1138             graphics, xSpec, ySpec = self.last_draw
1139             self.Draw(graphics,xSpec,ySpec)
1140
1141     def OnLeave(self, event):
1142         """Used to erase pointLabel when mouse outside window"""
1143         if self.last_PointLabel != None:
1144             self._drawPointLabel(self.last_PointLabel) #erase old
1145             self.last_PointLabel = None
1146
1147         
1148     # Private Methods **************************************************
1149     def _setSize(self, width=None, height=None):
1150         """DC width and height."""
1151         if width == None:
1152             (self.width,self.height) = self.GetClientSize()
1153         else:
1154             self.width, self.height= width,height    
1155         self.plotbox_size = 0.97*_numpy.array([self.width, self.height])
1156         xo = 0.5*(self.width-self.plotbox_size[0])
1157         yo = self.height-0.5*(self.height-self.plotbox_size[1])
1158         self.plotbox_origin = _numpy.array([xo, yo])
1159     
1160     def _setPrinterScale(self, scale):
1161         """Used to thicken lines and increase marker size for print out."""
1162         # line thickness on printer is very thin at 600 dot/in. Markers small
1163         self.printerScale= scale
1164      
1165     def _printDraw(self, printDC):
1166         """Used for printing."""
1167         if self.last_draw != None:
1168             graphics, xSpec, ySpec= self.last_draw
1169             self.Draw(graphics,xSpec,ySpec,printDC)
1170
1171     def _drawPointLabel(self, mDataDict):
1172         """Draws and erases pointLabels"""
1173         width = self._Buffer.GetWidth()
1174         height = self._Buffer.GetHeight()
1175         tmp_Buffer = wx.EmptyBitmap(width,height)
1176         dcs = wx.MemoryDC()
1177         dcs.SelectObject(tmp_Buffer)
1178         dcs.Clear()
1179         dcs.BeginDrawing()
1180         self._pointLabelFunc(dcs,mDataDict)  #custom user pointLabel function
1181         dcs.EndDrawing()
1182
1183         dc = wx.ClientDC( self )
1184         #this will erase if called twice
1185         dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV)  #(NOT src) XOR dst
1186         
1187
1188     def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
1189         """Draws legend symbols and text"""
1190         # top right hand corner of graph box is ref corner
1191         trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1192         legendLHS= .091* legendBoxWH[0]  # border space between legend sym and graph box
1193         lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1194         dc.SetFont(self._getFont(self._fontSizeLegend))
1195         for i in range(len(graphics)):
1196             o = graphics[i]
1197             s= i*lineHeight
1198             if isinstance(o,PolyMarker):
1199                 # draw marker with legend
1200                 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
1201                 o.draw(dc, self.printerScale, coord= _numpy.array([pnt]))
1202             elif isinstance(o,PolyLine):
1203                 # draw line with legend
1204                 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1205                 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
1206                 o.draw(dc, self.printerScale, coord= _numpy.array([pnt1,pnt2]))
1207             else:
1208                 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1209             # draw legend txt
1210             pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
1211             dc.DrawText(o.getLegend(),pnt[0],pnt[1])
1212         dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1213
1214     def _titleLablesWH(self, dc, graphics):
1215         """Draws Title and labels and returns width and height for each"""
1216         # TextExtents for Title and Axis Labels
1217         dc.SetFont(self._getFont(self._fontSizeTitle))
1218         title= graphics.getTitle()
1219         titleWH= dc.GetTextExtent(title)
1220         dc.SetFont(self._getFont(self._fontSizeAxis))
1221         xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1222         xLabelWH= dc.GetTextExtent(xLabel)
1223         yLabelWH= dc.GetTextExtent(yLabel)
1224         return titleWH, xLabelWH, yLabelWH
1225     
1226     def _legendWH(self, dc, graphics):
1227         """Returns the size in screen units for legend box"""
1228         if self._legendEnabled != True:
1229             legendBoxWH= symExt= txtExt= (0,0)
1230         else:
1231             # find max symbol size
1232             symExt= graphics.getSymExtent(self.printerScale)
1233             # find max legend text extent
1234             dc.SetFont(self._getFont(self._fontSizeLegend))
1235             txtList= graphics.getLegendNames()
1236             txtExt= dc.GetTextExtent(txtList[0])
1237             for txt in graphics.getLegendNames()[1:]:
1238                 txtExt= _numpy.maximum(txtExt,dc.GetTextExtent(txt))
1239             maxW= symExt[0]+txtExt[0]    
1240             maxH= max(symExt[1],txtExt[1])
1241             # padding .1 for lhs of legend box and space between lines
1242             maxW= maxW* 1.1
1243             maxH= maxH* 1.1 * len(txtList)
1244             dc.SetFont(self._getFont(self._fontSizeAxis))
1245             legendBoxWH= (maxW,maxH)
1246         return (legendBoxWH, symExt, txtExt)
1247
1248     def _drawRubberBand(self, corner1, corner2):
1249         """Draws/erases rect box from corner1 to corner2"""
1250         ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1251         # draw rectangle
1252         dc = wx.ClientDC( self )
1253         dc.BeginDrawing()                 
1254         dc.SetPen(wx.Pen(wx.BLACK))
1255         dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1256         dc.SetLogicalFunction(wx.INVERT)
1257         dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
1258         dc.SetLogicalFunction(wx.COPY)
1259         dc.EndDrawing()
1260
1261     def _getFont(self,size):
1262         """Take font size, adjusts if printing and returns wx.Font"""
1263         s = size*self.printerScale
1264         of = self.GetFont()
1265         # Linux speed up to get font from cache rather than X font server
1266         key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1267         font = self._fontCache.get (key, None)
1268         if font:
1269             return font                 # yeah! cache hit
1270         else:
1271             font =  wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1272             self._fontCache[key] = font
1273             return font
1274
1275
1276     def _point2ClientCoord(self, corner1, corner2):
1277         """Converts user point coords to client screen int coords x,y,width,height"""
1278         c1= _numpy.array(corner1)
1279         c2= _numpy.array(corner2)
1280         # convert to screen coords
1281         pt1= c1*self._pointScale+self._pointShift
1282         pt2= c2*self._pointScale+self._pointShift
1283         # make height and width positive
1284         pul= _numpy.minimum(pt1,pt2) # Upper left corner
1285         plr= _numpy.maximum(pt1,pt2) # Lower right corner
1286         rectWidth, rectHeight= plr-pul
1287         ptx,pty= pul
1288         return ptx, pty, rectWidth, rectHeight 
1289     
1290     def _axisInterval(self, spec, lower, upper):
1291         """Returns sensible axis range for given spec"""
1292         if spec == 'none' or spec == 'min':
1293             if lower == upper:
1294                 return lower-0.5, upper+0.5
1295             else:
1296                 return lower, upper
1297         elif spec == 'auto':
1298             range = upper-lower
1299             # if range == 0.:
1300             if abs(range) < 1e-36:
1301                 return lower-0.5, upper+0.5
1302             log = _numpy.log10(range)
1303             power = _numpy.floor(log)
1304             fraction = log-power
1305             if fraction <= 0.05:
1306                 power = power-1
1307             grid = 10.**power
1308             lower = lower - lower % grid
1309             mod = upper % grid
1310             if mod != 0:
1311                 upper = upper - mod + grid
1312             return lower, upper
1313         elif type(spec) == type(()):
1314             lower, upper = spec
1315             if lower <= upper:
1316                 return lower, upper
1317             else:
1318                 return upper, lower
1319         else:
1320             raise ValueError, str(spec) + ': illegal axis specification'
1321
1322     def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1323         
1324         penWidth= self.printerScale        # increases thickness for printing only
1325         dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
1326         
1327         # set length of tick marks--long ones make grid
1328         if self._gridEnabled:
1329             x,y,width,height= self._point2ClientCoord(p1,p2)
1330             yTickLength= width/2.0 +1
1331             xTickLength= height/2.0 +1
1332         else:
1333             yTickLength= 3 * self.printerScale  # lengthens lines for printing
1334             xTickLength= 3 * self.printerScale
1335         
1336         if self._xSpec is not 'none':
1337             lower, upper = p1[0],p2[0]
1338             text = 1
1339             for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]:   # miny, maxy and tick lengths
1340                 a1 = scale*_numpy.array([lower, y])+shift
1341                 a2 = scale*_numpy.array([upper, y])+shift
1342                 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])  # draws upper and lower axis line
1343                 for x, label in xticks:
1344                     pt = scale*_numpy.array([x, y])+shift
1345                     dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1346                     if text:
1347                         dc.DrawText(label,pt[0],pt[1])
1348                 text = 0  # axis values not drawn on top side
1349
1350         if self._ySpec is not 'none':
1351             lower, upper = p1[1],p2[1]
1352             text = 1
1353             h = dc.GetCharHeight()
1354             for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1355                 a1 = scale*_numpy.array([x, lower])+shift
1356                 a2 = scale*_numpy.array([x, upper])+shift
1357                 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1358                 for y, label in yticks:
1359                     pt = scale*_numpy.array([x, y])+shift
1360                     dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1361                     if text:
1362                         dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1363                                     pt[1]-0.5*h)
1364                 text = 0    # axis values not drawn on right side
1365
1366     def _ticks(self, lower, upper, step=None):
1367         ideal = (upper-lower)/7.
1368         log = _numpy.log10(ideal)
1369         power = _numpy.floor(log)
1370         fraction = log-power
1371         factor = 1.
1372         error = fraction
1373         for f, lf in self._multiples:
1374             e = _numpy.fabs(fraction-lf)
1375             if e < error:
1376                 error = e
1377                 factor = f
1378         grid = factor * 10.**power
1379         if power > 4 or power < -4:
1380             format = '%+7.1e'        
1381         elif power >= 0:
1382             digits = max(1, int(power))
1383             format = '%' + `digits`+'.0f'
1384         else:
1385             digits = -int(power)
1386             format = '%'+`digits+2`+'.'+`digits`+'f'
1387         #force grid when step is not None
1388         if step is not None: grid = step
1389         ticks = []
1390         t = -grid*_numpy.floor(-lower/grid)
1391         while t <= upper:
1392             if t == -0: t = 0 #remove neg zero condition
1393             ticks.append( (t, format % (t,)) )
1394             t = t + grid
1395         return ticks
1396
1397     def _scope_ticks (self, lower, upper):
1398         '''Always 10 divisions, no labels'''
1399         grid = (upper - lower) / 10.0
1400         ticks = []
1401         t = lower
1402         while t <= upper:
1403             ticks.append( (t, ""))
1404             t = t + grid
1405         return ticks
1406
1407     _multiples = [(2., _numpy.log10(2.)), (5., _numpy.log10(5.))]
1408
1409
1410 #-------------------------------------------------------------------------------
1411 # Used to layout the printer page
1412
1413 class PlotPrintout(wx.Printout):
1414     """Controls how the plot is made in printing and previewing"""
1415     # Do not change method names in this class,
1416     # we have to override wx.Printout methods here!
1417     def __init__(self, graph):
1418         """graph is instance of plotCanvas to be printed or previewed"""
1419         wx.Printout.__init__(self)
1420         self.graph = graph
1421
1422     def HasPage(self, page):
1423         if page == 1:
1424             return True
1425         else:
1426             return False
1427
1428     def GetPageInfo(self):
1429         return (1, 1, 1, 1)  # disable page numbers
1430
1431     def OnPrintPage(self, page):
1432         dc = self.GetDC()  # allows using floats for certain functions
1433 ##        print "PPI Printer",self.GetPPIPrinter()
1434 ##        print "PPI Screen", self.GetPPIScreen()
1435 ##        print "DC GetSize", dc.GetSize()
1436 ##        print "GetPageSizePixels", self.GetPageSizePixels()
1437         # Note PPIScreen does not give the correct number
1438         # Calulate everything for printer and then scale for preview
1439         PPIPrinter= self.GetPPIPrinter()        # printer dots/inch (w,h)
1440         #PPIScreen= self.GetPPIScreen()          # screen dots/inch (w,h)
1441         dcSize= dc.GetSize()                    # DC size
1442         pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1443         clientDcSize= self.graph.GetClientSize()
1444         
1445         # find what the margins are (mm)
1446         margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1447         margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1448
1449         # calculate offset and scale for dc
1450         pixLeft= margLeftSize*PPIPrinter[0]/25.4  # mm*(dots/in)/(mm/in)
1451         pixRight= margRightSize*PPIPrinter[0]/25.4    
1452         pixTop= margTopSize*PPIPrinter[1]/25.4
1453         pixBottom= margBottomSize*PPIPrinter[1]/25.4
1454
1455         plotAreaW= pageSize[0]-(pixLeft+pixRight)
1456         plotAreaH= pageSize[1]-(pixTop+pixBottom)
1457
1458         # ratio offset and scale to screen size if preview
1459         if self.IsPreview():
1460             ratioW= float(dcSize[0])/pageSize[0]
1461             ratioH= float(dcSize[1])/pageSize[1]
1462             pixLeft *= ratioW
1463             pixTop *= ratioH
1464             plotAreaW *= ratioW
1465             plotAreaH *= ratioH
1466         
1467         # rescale plot to page or preview plot area
1468         self.graph._setSize(plotAreaW,plotAreaH)
1469         
1470         # Set offset and scale
1471         dc.SetDeviceOrigin(pixLeft,pixTop)
1472
1473         # Thicken up pens and increase marker size for printing
1474         ratioW= float(plotAreaW)/clientDcSize[0]
1475         ratioH= float(plotAreaH)/clientDcSize[1]
1476         aveScale= (ratioW+ratioH)/2
1477         self.graph._setPrinterScale(aveScale)  # tickens up pens for printing
1478
1479         self.graph._printDraw(dc)
1480         # rescale back to original
1481         self.graph._setSize()
1482         self.graph._setPrinterScale(1)
1483         self.graph.Redraw()     #to get point label scale and shift correct
1484
1485         return True
1486
1487
1488
1489
1490 #---------------------------------------------------------------------------
1491 # if running standalone...
1492 #
1493 #     ...a sample implementation using the above
1494 #
1495
1496 def _draw1Objects():
1497     # 100 points sin function, plotted as green circles
1498     data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1499     data1.shape = (100, 2)
1500     data1[:,1] = _numpy.sin(data1[:,0])
1501     markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1502
1503     # 50 points cos function, plotted as red line
1504     data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1505     data1.shape = (50,2)
1506     data1[:,1] = _numpy.cos(data1[:,0])
1507     lines = PolyLine(data1, legend= 'Red Line', colour='red')
1508
1509     # A few more points...
1510     pi = _numpy.pi
1511     markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1512                           (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1513                           marker='cross')
1514     
1515     return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1516
1517 def _draw2Objects():
1518     # 100 points sin function, plotted as green dots
1519     data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1520     data1.shape = (100, 2)
1521     data1[:,1] = _numpy.sin(data1[:,0])
1522     line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1523
1524     # 50 points cos function, plotted as red dot-dash
1525     data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1526     data1.shape = (50,2)
1527     data1[:,1] = _numpy.cos(data1[:,0])
1528     line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1529
1530     # A few more points...
1531     pi = _numpy.pi
1532     markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1533                           (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1534                           fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1535                           marker='square')
1536
1537     return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1538
1539 def _draw3Objects():
1540     markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1541                 'cross', 'plus', 'circle']
1542     m=[]
1543     for i in range(len(markerList)):
1544         m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1545                           marker=markerList[i]))
1546     return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1547
1548 def _draw4Objects():
1549     # 25,000 point line
1550     data1 = _numpy.arange(5e5,1e6,10)
1551     data1.shape = (25000, 2)
1552     line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1553
1554     # A few more points...
1555     markers2 = PolyMarker(data1, legend='Square', colour='blue',
1556                           marker='square')
1557     return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1558
1559 def _draw5Objects():
1560     # Empty graph with axis defined but no points/lines
1561     points=[]
1562     line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1563     return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1564
1565 def _draw6Objects():
1566     # Bar graph
1567     points1=[(1,0), (1,10)]
1568     line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1569     points1g=[(2,0), (2,4)]
1570     line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1571     points1b=[(3,0), (3,6)]
1572     line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1573
1574     points2=[(4,0), (4,12)]
1575     line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1576     points2g=[(5,0), (5,8)]
1577     line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1578     points2b=[(6,0), (6,4)]
1579     line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1580
1581     return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1582                         "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1583
1584
1585 class TestFrame(wx.Frame):
1586     def __init__(self, parent, id, title):
1587         wx.Frame.__init__(self, parent, id, title,
1588                           wx.DefaultPosition, (600, 400))
1589
1590         # Now Create the menu bar and items
1591         self.mainmenu = wx.MenuBar()
1592
1593         menu = wx.Menu()
1594         menu.Append(200, 'Page Setup...', 'Setup the printer page')
1595         self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1596         
1597         menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1598         self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1599         
1600         menu.Append(202, 'Print...', 'Print the current plot')
1601         self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1602         
1603         menu.Append(203, 'Save Plot...', 'Save current plot')
1604         self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1605         
1606         menu.Append(205, 'E&xit', 'Enough of this already!')
1607         self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1608         self.mainmenu.Append(menu, '&File')
1609
1610         menu = wx.Menu()
1611         menu.Append(206, 'Draw1', 'Draw plots1')
1612         self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1613         menu.Append(207, 'Draw2', 'Draw plots2')
1614         self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1615         menu.Append(208, 'Draw3', 'Draw plots3')
1616         self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1617         menu.Append(209, 'Draw4', 'Draw plots4')
1618         self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1619         menu.Append(210, 'Draw5', 'Draw plots5')
1620         self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1621         menu.Append(260, 'Draw6', 'Draw plots6')
1622         self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
1623        
1624
1625         menu.Append(211, '&Redraw', 'Redraw plots')
1626         self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1627         menu.Append(212, '&Clear', 'Clear canvas')
1628         self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1629         menu.Append(213, '&Scale', 'Scale canvas')
1630         self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213) 
1631         menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1632         self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214) 
1633         menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1634         self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1635         menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1636         self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1637         menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1638         self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1639        
1640         menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1641         self.Bind(wx.EVT_MENU,self.OnScrUp, id=225) 
1642         menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1643         self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1644         menu.Append(235, '&Plot Reset', 'Reset to original plot')
1645         self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1646
1647         self.mainmenu.Append(menu, '&Plot')
1648
1649         menu = wx.Menu()
1650         menu.Append(300, '&About', 'About this thing...')
1651         self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1652         self.mainmenu.Append(menu, '&Help')
1653
1654         self.SetMenuBar(self.mainmenu)
1655
1656         # A status bar to tell people what's happening
1657         self.CreateStatusBar(1)
1658         
1659         self.client = PlotCanvas(self)
1660         #define the function for drawing pointLabels
1661         self.client.SetPointLabelFunc(self.DrawPointLabel)
1662         # Create mouse event for showing cursor coords in status bar
1663         self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1664         # Show closest point when enabled
1665         self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1666
1667         self.Show(True)
1668
1669     def DrawPointLabel(self, dc, mDataDict):
1670         """This is the fuction that defines how the pointLabels are plotted
1671             dc - DC that will be passed
1672             mDataDict - Dictionary of data that you want to use for the pointLabel
1673
1674             As an example I have decided I want a box at the curve point
1675             with some text information about the curve plotted below.
1676             Any wxDC method can be used.
1677         """
1678         # ----------
1679         dc.SetPen(wx.Pen(wx.BLACK))
1680         dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1681         
1682         sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1683         dc.DrawRectangle( sx-5,sy-5, 10, 10)  #10by10 square centered on point
1684         px,py = mDataDict["pointXY"]
1685         cNum = mDataDict["curveNum"]
1686         pntIn = mDataDict["pIndex"]
1687         legend = mDataDict["legend"]
1688         #make a string to display
1689         s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1690         dc.DrawText(s, sx , sy+1)
1691         # -----------
1692
1693     def OnMouseLeftDown(self,event):
1694         s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1695         self.SetStatusText(s)
1696         event.Skip()            #allows plotCanvas OnMouseLeftDown to be called
1697
1698     def OnMotion(self, event):
1699         #show closest point (when enbled)
1700         if self.client.GetEnablePointLabel() == True:
1701             #make up dict with info for the pointLabel
1702             #I've decided to mark the closest point on the closest curve
1703             dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1704             if dlst != []:    #returns [] if none
1705                 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1706                 #make up dictionary to pass to my user function (see DrawPointLabel) 
1707                 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1708                             "pointXY":pointXY, "scaledXY":scaledXY}
1709                 #pass dict to update the pointLabel
1710                 self.client.UpdatePointLabel(mDataDict)
1711         event.Skip()           #go to next handler
1712
1713     def OnFilePageSetup(self, event):
1714         self.client.PageSetup()
1715         
1716     def OnFilePrintPreview(self, event):
1717         self.client.PrintPreview()
1718         
1719     def OnFilePrint(self, event):
1720         self.client.Printout()
1721         
1722     def OnSaveFile(self, event):
1723         self.client.SaveFile()
1724
1725     def OnFileExit(self, event):
1726         self.Close()
1727
1728     def OnPlotDraw1(self, event):
1729         self.resetDefaults()
1730         self.client.Draw(_draw1Objects())
1731     
1732     def OnPlotDraw2(self, event):
1733         self.resetDefaults()
1734         self.client.Draw(_draw2Objects())
1735     
1736     def OnPlotDraw3(self, event):
1737         self.resetDefaults()
1738         self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1739         self.client.SetFontSizeAxis(20)
1740         self.client.SetFontSizeLegend(12)
1741         self.client.SetXSpec('min')
1742         self.client.SetYSpec('none')
1743         self.client.Draw(_draw3Objects())
1744
1745     def OnPlotDraw4(self, event):
1746         self.resetDefaults()
1747         drawObj= _draw4Objects()
1748         self.client.Draw(drawObj)
1749 ##        # profile
1750 ##        start = _time.clock()            
1751 ##        for x in range(10):
1752 ##            self.client.Draw(drawObj)
1753 ##        print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1754 ##        # profile end
1755
1756     def OnPlotDraw5(self, event):
1757         # Empty plot with just axes
1758         self.resetDefaults()
1759         drawObj= _draw5Objects()
1760         # make the axis X= (0,5), Y=(0,10)
1761         # (default with None is X= (-1,1), Y= (-1,1))
1762         self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1763
1764     def OnPlotDraw6(self, event):
1765         #Bar Graph Example
1766         self.resetDefaults()
1767         #self.client.SetEnableLegend(True)   #turn on Legend
1768         #self.client.SetEnableGrid(True)     #turn on Grid
1769         self.client.SetXSpec('none')        #turns off x-axis scale
1770         self.client.SetYSpec('auto')
1771         self.client.Draw(_draw6Objects(), xAxis= (0,7))
1772
1773     def OnPlotRedraw(self,event):
1774         self.client.Redraw()
1775
1776     def OnPlotClear(self,event):
1777         self.client.Clear()
1778         
1779     def OnPlotScale(self, event):
1780         if self.client.last_draw != None:
1781             graphics, xAxis, yAxis= self.client.last_draw
1782             self.client.Draw(graphics,(1,3.05),(0,1))
1783
1784     def OnEnableZoom(self, event):
1785         self.client.SetEnableZoom(event.IsChecked())
1786         
1787     def OnEnableGrid(self, event):
1788         self.client.SetEnableGrid(event.IsChecked())
1789         
1790     def OnEnableLegend(self, event):
1791         self.client.SetEnableLegend(event.IsChecked())
1792
1793     def OnEnablePointLabel(self, event):
1794         self.client.SetEnablePointLabel(event.IsChecked())
1795
1796     def OnScrUp(self, event):
1797         self.client.ScrollUp(1)
1798         
1799     def OnScrRt(self,event):
1800         self.client.ScrollRight(2)
1801
1802     def OnReset(self,event):
1803         self.client.Reset()
1804
1805     def OnHelpAbout(self, event):
1806         from wx.lib.dialogs import ScrolledMessageDialog
1807         about = ScrolledMessageDialog(self, __doc__, "About...")
1808         about.ShowModal()
1809
1810     def resetDefaults(self):
1811         """Just to reset the fonts back to the PlotCanvas defaults"""
1812         self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1813         self.client.SetFontSizeAxis(10)
1814         self.client.SetFontSizeLegend(7)
1815         self.client.SetXSpec('auto')
1816         self.client.SetYSpec('auto')
1817         
1818
1819 def __test():
1820
1821     class MyApp(wx.App):
1822         def OnInit(self):
1823             wx.InitAllImageHandlers()
1824             frame = TestFrame(None, -1, "PlotCanvas")
1825             #frame.Show(True)
1826             self.SetTopWindow(frame)
1827             return True
1828
1829
1830     app = MyApp(0)
1831     app.MainLoop()
1832
1833 if __name__ == '__main__':
1834     __test()