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