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