]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/plot.py
Olly Betts' typo patch
[wxWidgets.git] / wxPython / wx / lib / plot.py
1 #-----------------------------------------------------------------------------
2 # Name: wx.lib.plot.py
3 # Purpose: Line, Bar and Scatter Graphs
4 #
5 # Author: Gordon Williams
6 #
7 # Created: 2003/11/03
8 # RCS-ID: $Id$
9 # Copyright: (c) 2002
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
470 # Fonts
471 self._fontCache = {}
472 self._fontSizeAxis= 10
473 self._fontSizeTitle= 15
474 self._fontSizeLegend= 7
475
476 # pointLabels
477 self._pointLabelEnabled= False
478 self.last_PointLabel= None
479 self._pointLabelFunc= None
480 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
481
482 self.Bind(wx.EVT_PAINT, self.OnPaint)
483 self.Bind(wx.EVT_SIZE, self.OnSize)
484 # OnSize called to make sure the buffer is initialized.
485 # This might result in OnSize getting called twice on some
486 # platforms at initialization, but little harm done.
487 if wx.Platform != "__WXMAC__":
488 self.OnSize(None) # sets the initial size based on client size
489
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=wx.PrintData(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 = wx.PrintData(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,'Horizontal','Vertical']:
634 raise TypeError, "Value should be True, False, Horizontal or Vertical"
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
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= _Numeric.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= _Numeric.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 Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
764 """Draw objects in graphics with specified x and y axis.
765 graphics- instance of PlotGraphics with list of PolyXXX objects
766 xAxis - tuple with (min, max) axis range to view
767 yAxis - same as xAxis
768 dc - drawing context - doesn't have to be specified.
769 If it's not, the offscreen buffer is used
770 """
771 # check Axis is either tuple or none
772 if type(xAxis) not in [type(None),tuple]:
773 raise TypeError, "xAxis should be None or (minX,maxX)"
774 if type(yAxis) not in [type(None),tuple]:
775 raise TypeError, "yAxis should be None or (minY,maxY)"
776
777 # check case for axis = (a,b) where a==b caused by improper zooms
778 if xAxis != None:
779 if xAxis[0] == xAxis[1]:
780 return
781 if yAxis != None:
782 if yAxis[0] == yAxis[1]:
783 return
784
785 if dc == None:
786 # sets new dc and clears it
787 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
788 dc.Clear()
789
790 dc.BeginDrawing()
791 # dc.Clear()
792
793 # set font size for every thing but title and legend
794 dc.SetFont(self._getFont(self._fontSizeAxis))
795
796 # sizes axis to axis type, create lower left and upper right corners of plot
797 if xAxis == None or yAxis == None:
798 # One or both axis not specified in Draw
799 p1, p2 = graphics.boundingBox() # min, max points of graphics
800 if xAxis == None:
801 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
802 if yAxis == None:
803 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
804 # Adjust bounding box for axis spec
805 p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
806 p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
807 else:
808 # Both axis specified in Draw
809 p1= _Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
810 p2= _Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
811
812 self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
813
814 # Get ticks and textExtents for axis if required
815 if self._xSpec is not 'none':
816 xticks = self._ticks(xAxis[0], xAxis[1])
817 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
818 else:
819 xticks = None
820 xTextExtent= (0,0) # No text for ticks
821 if self._ySpec is not 'none':
822 yticks = self._ticks(yAxis[0], yAxis[1])
823 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
824 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
825 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
826 max(yTextExtentBottom[1],yTextExtentTop[1]))
827 else:
828 yticks = None
829 yTextExtent= (0,0) # No text for ticks
830
831 # TextExtents for Title and Axis Labels
832 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
833
834 # TextExtents for Legend
835 legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
836
837 # room around graph area
838 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
839 lhsW= yTextExtent[0]+ yLabelWH[1]
840 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
841 topH= yTextExtent[1]/2. + titleWH[1]
842 textSize_scale= _Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
843 textSize_shift= _Numeric.array([lhsW, bottomH]) # shift plot area by this amount
844
845 # drawing title and labels text
846 dc.SetFont(self._getFont(self._fontSizeTitle))
847 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
848 self.plotbox_origin[1]- self.plotbox_size[1])
849 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
850 dc.SetFont(self._getFont(self._fontSizeAxis))
851 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
852 self.plotbox_origin[1]- xLabelWH[1])
853 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
854 yLabelPos= (self.plotbox_origin[0],
855 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
856 if graphics.getYLabel(): # bug fix for Linux
857 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
858
859 # drawing legend makers and text
860 if self._legendEnabled:
861 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
862
863 # allow for scaling and shifting plotted points
864 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1))
865 shift = -p1*scale + self.plotbox_origin + textSize_shift * _Numeric.array((1,-1))
866 self._pointScale= scale # make available for mouse events
867 self._pointShift= shift
868 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
869
870 graphics.scaleAndShift(scale, shift)
871 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
872
873 # set clipping area so drawing does not occur outside axis box
874 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
875 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
876 # Draw the lines and markers
877 #start = _time.clock()
878 graphics.draw(dc)
879 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
880 # remove the clipping region
881 dc.DestroyClippingRegion()
882 dc.EndDrawing()
883
884 def Redraw(self, dc= None):
885 """Redraw the existing plot."""
886 if self.last_draw is not None:
887 graphics, xAxis, yAxis= self.last_draw
888 self.Draw(graphics,xAxis,yAxis,dc)
889
890 def Clear(self):
891 """Erase the window."""
892 self.last_PointLabel = None #reset pointLabel
893 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
894 dc.Clear()
895 self.last_draw = None
896
897 def Zoom(self, Center, Ratio):
898 """ Zoom on the plot
899 Centers on the X,Y coords given in Center
900 Zooms by the Ratio = (Xratio, Yratio) given
901 """
902 self.last_PointLabel = None #reset maker
903 x,y = Center
904 if self.last_draw != None:
905 (graphics, xAxis, yAxis) = self.last_draw
906 w = (xAxis[1] - xAxis[0]) * Ratio[0]
907 h = (yAxis[1] - yAxis[0]) * Ratio[1]
908 xAxis = ( x - w/2, x + w/2 )
909 yAxis = ( y - h/2, y + h/2 )
910 self.Draw(graphics, xAxis, yAxis)
911
912 def GetClosestPoints(self, pntXY, pointScaled= True):
913 """Returns list with
914 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
915 list for each curve.
916 Returns [] if no curves are being plotted.
917
918 x, y in user coords
919 if pointScaled == True based on screen coords
920 if pointScaled == False based on user coords
921 """
922 if self.last_draw == None:
923 #no graph available
924 return []
925 graphics, xAxis, yAxis= self.last_draw
926 l = []
927 for curveNum,obj in enumerate(graphics):
928 #check there are points in the curve
929 if len(obj.points) == 0:
930 continue #go to next obj
931 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
932 cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
933 l.append(cn)
934 return l
935
936 def GetClosetPoint(self, pntXY, pointScaled= True):
937 """Returns list with
938 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
939 list for only the closest curve.
940 Returns [] if no curves are being plotted.
941
942 x, y in user coords
943 if pointScaled == True based on screen coords
944 if pointScaled == False based on user coords
945 """
946 #closest points on screen based on screen scaling (pointScaled= True)
947 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
948 closestPts= self.GetClosestPoints(pntXY, pointScaled)
949 if closestPts == []:
950 return [] #no graph present
951 #find one with least distance
952 dists = [c[-1] for c in closestPts]
953 mdist = min(dists) #Min dist
954 i = dists.index(mdist) #index for min dist
955 return closestPts[i] #this is the closest point on closest curve
956
957 def UpdatePointLabel(self, mDataDict):
958 """Updates the pointLabel point on screen with data contained in
959 mDataDict.
960
961 mDataDict will be passed to your function set by
962 SetPointLabelFunc. It can contain anything you
963 want to display on the screen at the scaledXY point
964 you specify.
965
966 This function can be called from parent window with onClick,
967 onMotion events etc.
968 """
969 if self.last_PointLabel != None:
970 #compare pointXY
971 if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
972 #closest changed
973 self._drawPointLabel(self.last_PointLabel) #erase old
974 self._drawPointLabel(mDataDict) #plot new
975 else:
976 #just plot new with no erase
977 self._drawPointLabel(mDataDict) #plot new
978 #save for next erase
979 self.last_PointLabel = mDataDict
980
981 # event handlers **********************************
982 def OnMotion(self, event):
983 if self._zoomEnabled and event.LeftIsDown():
984 if self._hasDragged:
985 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
986 else:
987 self._hasDragged= True
988 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
989 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
990
991 def OnMouseLeftDown(self,event):
992 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
993
994 def OnMouseLeftUp(self, event):
995 if self._zoomEnabled:
996 if self._hasDragged == True:
997 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
998 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
999 self._hasDragged = False # reset flag
1000 minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
1001 maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
1002 self.last_PointLabel = None #reset pointLabel
1003 if self.last_draw != None:
1004 self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
1005 #else: # A box has not been drawn, zoom in on a point
1006 ## this interfered with the double click, so I've disables it.
1007 # X,Y = self.GetXY(event)
1008 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1009
1010 def OnMouseDoubleClick(self,event):
1011 if self._zoomEnabled:
1012 self.Reset()
1013
1014 def OnMouseRightDown(self,event):
1015 if self._zoomEnabled:
1016 X,Y = self.GetXY(event)
1017 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1018
1019 def OnPaint(self, event):
1020 # All that is needed here is to draw the buffer to screen
1021 if self.last_PointLabel != None:
1022 self._drawPointLabel(self.last_PointLabel) #erase old
1023 self.last_PointLabel = None
1024 dc = wx.BufferedPaintDC(self, self._Buffer)
1025
1026 def OnSize(self,event):
1027 # The Buffer init is done here, to make sure the buffer is always
1028 # the same size as the Window
1029 Size = self.GetClientSize()
1030 if Size.width <= 0 or Size.height <= 0:
1031 return
1032
1033 # Make new offscreen bitmap: this bitmap will always have the
1034 # current drawing in it, so it can be used to save the image to
1035 # a file, or whatever.
1036 self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
1037 self._setSize()
1038
1039 self.last_PointLabel = None #reset pointLabel
1040
1041 if self.last_draw is None:
1042 self.Clear()
1043 else:
1044 graphics, xSpec, ySpec = self.last_draw
1045 self.Draw(graphics,xSpec,ySpec)
1046
1047 def OnLeave(self, event):
1048 """Used to erase pointLabel when mouse outside window"""
1049 if self.last_PointLabel != None:
1050 self._drawPointLabel(self.last_PointLabel) #erase old
1051 self.last_PointLabel = None
1052
1053
1054 # Private Methods **************************************************
1055 def _setSize(self, width=None, height=None):
1056 """DC width and height."""
1057 if width == None:
1058 (self.width,self.height) = self.GetClientSize()
1059 else:
1060 self.width, self.height= width,height
1061 self.plotbox_size = 0.97*_Numeric.array([self.width, self.height])
1062 xo = 0.5*(self.width-self.plotbox_size[0])
1063 yo = self.height-0.5*(self.height-self.plotbox_size[1])
1064 self.plotbox_origin = _Numeric.array([xo, yo])
1065
1066 def _setPrinterScale(self, scale):
1067 """Used to thicken lines and increase marker size for print out."""
1068 # line thickness on printer is very thin at 600 dot/in. Markers small
1069 self.printerScale= scale
1070
1071 def _printDraw(self, printDC):
1072 """Used for printing."""
1073 if self.last_draw != None:
1074 graphics, xSpec, ySpec= self.last_draw
1075 self.Draw(graphics,xSpec,ySpec,printDC)
1076
1077 def _drawPointLabel(self, mDataDict):
1078 """Draws and erases pointLabels"""
1079 width = self._Buffer.GetWidth()
1080 height = self._Buffer.GetHeight()
1081 tmp_Buffer = wx.EmptyBitmap(width,height)
1082 dcs = wx.MemoryDC()
1083 dcs.SelectObject(tmp_Buffer)
1084 dcs.Clear()
1085 dcs.BeginDrawing()
1086 self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
1087 dcs.EndDrawing()
1088
1089 dc = wx.ClientDC( self )
1090 #this will erase if called twice
1091 dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
1092
1093
1094 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
1095 """Draws legend symbols and text"""
1096 # top right hand corner of graph box is ref corner
1097 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1098 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
1099 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1100 dc.SetFont(self._getFont(self._fontSizeLegend))
1101 for i in range(len(graphics)):
1102 o = graphics[i]
1103 s= i*lineHeight
1104 if isinstance(o,PolyMarker):
1105 # draw marker with legend
1106 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
1107 o.draw(dc, self.printerScale, coord= _Numeric.array([pnt]))
1108 elif isinstance(o,PolyLine):
1109 # draw line with legend
1110 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1111 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
1112 o.draw(dc, self.printerScale, coord= _Numeric.array([pnt1,pnt2]))
1113 else:
1114 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1115 # draw legend txt
1116 pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
1117 dc.DrawText(o.getLegend(),pnt[0],pnt[1])
1118 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1119
1120 def _titleLablesWH(self, dc, graphics):
1121 """Draws Title and labels and returns width and height for each"""
1122 # TextExtents for Title and Axis Labels
1123 dc.SetFont(self._getFont(self._fontSizeTitle))
1124 title= graphics.getTitle()
1125 titleWH= dc.GetTextExtent(title)
1126 dc.SetFont(self._getFont(self._fontSizeAxis))
1127 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1128 xLabelWH= dc.GetTextExtent(xLabel)
1129 yLabelWH= dc.GetTextExtent(yLabel)
1130 return titleWH, xLabelWH, yLabelWH
1131
1132 def _legendWH(self, dc, graphics):
1133 """Returns the size in screen units for legend box"""
1134 if self._legendEnabled != True:
1135 legendBoxWH= symExt= txtExt= (0,0)
1136 else:
1137 # find max symbol size
1138 symExt= graphics.getSymExtent(self.printerScale)
1139 # find max legend text extent
1140 dc.SetFont(self._getFont(self._fontSizeLegend))
1141 txtList= graphics.getLegendNames()
1142 txtExt= dc.GetTextExtent(txtList[0])
1143 for txt in graphics.getLegendNames()[1:]:
1144 txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt))
1145 maxW= symExt[0]+txtExt[0]
1146 maxH= max(symExt[1],txtExt[1])
1147 # padding .1 for lhs of legend box and space between lines
1148 maxW= maxW* 1.1
1149 maxH= maxH* 1.1 * len(txtList)
1150 dc.SetFont(self._getFont(self._fontSizeAxis))
1151 legendBoxWH= (maxW,maxH)
1152 return (legendBoxWH, symExt, txtExt)
1153
1154 def _drawRubberBand(self, corner1, corner2):
1155 """Draws/erases rect box from corner1 to corner2"""
1156 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1157 # draw rectangle
1158 dc = wx.ClientDC( self )
1159 dc.BeginDrawing()
1160 dc.SetPen(wx.Pen(wx.BLACK))
1161 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1162 dc.SetLogicalFunction(wx.INVERT)
1163 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
1164 dc.SetLogicalFunction(wx.COPY)
1165 dc.EndDrawing()
1166
1167 def _getFont(self,size):
1168 """Take font size, adjusts if printing and returns wx.Font"""
1169 s = size*self.printerScale
1170 of = self.GetFont()
1171 # Linux speed up to get font from cache rather than X font server
1172 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1173 font = self._fontCache.get (key, None)
1174 if font:
1175 return font # yeah! cache hit
1176 else:
1177 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1178 self._fontCache[key] = font
1179 return font
1180
1181
1182 def _point2ClientCoord(self, corner1, corner2):
1183 """Converts user point coords to client screen int coords x,y,width,height"""
1184 c1= _Numeric.array(corner1)
1185 c2= _Numeric.array(corner2)
1186 # convert to screen coords
1187 pt1= c1*self._pointScale+self._pointShift
1188 pt2= c2*self._pointScale+self._pointShift
1189 # make height and width positive
1190 pul= _Numeric.minimum(pt1,pt2) # Upper left corner
1191 plr= _Numeric.maximum(pt1,pt2) # Lower right corner
1192 rectWidth, rectHeight= plr-pul
1193 ptx,pty= pul
1194 return ptx, pty, rectWidth, rectHeight
1195
1196 def _axisInterval(self, spec, lower, upper):
1197 """Returns sensible axis range for given spec"""
1198 if spec == 'none' or spec == 'min':
1199 if lower == upper:
1200 return lower-0.5, upper+0.5
1201 else:
1202 return lower, upper
1203 elif spec == 'auto':
1204 range = upper-lower
1205 if range == 0.:
1206 return lower-0.5, upper+0.5
1207 log = _Numeric.log10(range)
1208 power = _Numeric.floor(log)
1209 fraction = log-power
1210 if fraction <= 0.05:
1211 power = power-1
1212 grid = 10.**power
1213 lower = lower - lower % grid
1214 mod = upper % grid
1215 if mod != 0:
1216 upper = upper - mod + grid
1217 return lower, upper
1218 elif type(spec) == type(()):
1219 lower, upper = spec
1220 if lower <= upper:
1221 return lower, upper
1222 else:
1223 return upper, lower
1224 else:
1225 raise ValueError, str(spec) + ': illegal axis specification'
1226
1227 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1228
1229 penWidth= self.printerScale # increases thickness for printing only
1230 dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
1231
1232 # set length of tick marks--long ones make grid
1233 if self._gridEnabled:
1234 x,y,width,height= self._point2ClientCoord(p1,p2)
1235 if self._gridEnabled == 'Horizontal':
1236 yTickLength= width/2.0 +1
1237 xTickLength= 3 * self.printerScale
1238 elif self._gridEnabled == 'Vertical':
1239 yTickLength= 3 * self.printerScale
1240 xTickLength= height/2.0 +1
1241 else:
1242 yTickLength= width/2.0 +1
1243 xTickLength= height/2.0 +1
1244 else:
1245 yTickLength= 3 * self.printerScale # lengthens lines for printing
1246 xTickLength= 3 * self.printerScale
1247
1248 if self._xSpec is not 'none':
1249 lower, upper = p1[0],p2[0]
1250 text = 1
1251 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
1252 a1 = scale*_Numeric.array([lower, y])+shift
1253 a2 = scale*_Numeric.array([upper, y])+shift
1254 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1255 for x, label in xticks:
1256 pt = scale*_Numeric.array([x, y])+shift
1257 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1258 if text:
1259 dc.DrawText(label,pt[0],pt[1])
1260 text = 0 # axis values not drawn on top side
1261
1262 if self._ySpec is not 'none':
1263 lower, upper = p1[1],p2[1]
1264 text = 1
1265 h = dc.GetCharHeight()
1266 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1267 a1 = scale*_Numeric.array([x, lower])+shift
1268 a2 = scale*_Numeric.array([x, upper])+shift
1269 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1270 for y, label in yticks:
1271 pt = scale*_Numeric.array([x, y])+shift
1272 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1273 if text:
1274 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1275 pt[1]-0.5*h)
1276 text = 0 # axis values not drawn on right side
1277
1278 def _ticks(self, lower, upper):
1279 ideal = (upper-lower)/7.
1280 log = _Numeric.log10(ideal)
1281 power = _Numeric.floor(log)
1282 fraction = log-power
1283 factor = 1.
1284 error = fraction
1285 for f, lf in self._multiples:
1286 e = _Numeric.fabs(fraction-lf)
1287 if e < error:
1288 error = e
1289 factor = f
1290 grid = factor * 10.**power
1291 if power > 4 or power < -4:
1292 format = '%+7.1e'
1293 elif power >= 0:
1294 digits = max(1, int(power))
1295 format = '%' + `digits`+'.0f'
1296 else:
1297 digits = -int(power)
1298 format = '%'+`digits+2`+'.'+`digits`+'f'
1299 ticks = []
1300 t = -grid*_Numeric.floor(-lower/grid)
1301 while t <= upper:
1302 ticks.append( (t, format % (t,)) )
1303 t = t + grid
1304 return ticks
1305
1306 _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))]
1307
1308
1309 #-------------------------------------------------------------------------------
1310 # Used to layout the printer page
1311
1312 class PlotPrintout(wx.Printout):
1313 """Controls how the plot is made in printing and previewing"""
1314 # Do not change method names in this class,
1315 # we have to override wx.Printout methods here!
1316 def __init__(self, graph):
1317 """graph is instance of plotCanvas to be printed or previewed"""
1318 wx.Printout.__init__(self)
1319 self.graph = graph
1320
1321 def HasPage(self, page):
1322 if page == 1:
1323 return True
1324 else:
1325 return False
1326
1327 def GetPageInfo(self):
1328 return (1, 1, 1, 1) # disable page numbers
1329
1330 def OnPrintPage(self, page):
1331 dc = self.GetDC() # allows using floats for certain functions
1332 ## print "PPI Printer",self.GetPPIPrinter()
1333 ## print "PPI Screen", self.GetPPIScreen()
1334 ## print "DC GetSize", dc.GetSize()
1335 ## print "GetPageSizePixels", self.GetPageSizePixels()
1336 # Note PPIScreen does not give the correct number
1337 # Calulate everything for printer and then scale for preview
1338 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1339 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1340 dcSize= dc.GetSize() # DC size
1341 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1342 clientDcSize= self.graph.GetClientSize()
1343
1344 # find what the margins are (mm)
1345 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1346 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1347
1348 # calculate offset and scale for dc
1349 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1350 pixRight= margRightSize*PPIPrinter[0]/25.4
1351 pixTop= margTopSize*PPIPrinter[1]/25.4
1352 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1353
1354 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1355 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1356
1357 # ratio offset and scale to screen size if preview
1358 if self.IsPreview():
1359 ratioW= float(dcSize[0])/pageSize[0]
1360 ratioH= float(dcSize[1])/pageSize[1]
1361 pixLeft *= ratioW
1362 pixTop *= ratioH
1363 plotAreaW *= ratioW
1364 plotAreaH *= ratioH
1365
1366 # rescale plot to page or preview plot area
1367 self.graph._setSize(plotAreaW,plotAreaH)
1368
1369 # Set offset and scale
1370 dc.SetDeviceOrigin(pixLeft,pixTop)
1371
1372 # Thicken up pens and increase marker size for printing
1373 ratioW= float(plotAreaW)/clientDcSize[0]
1374 ratioH= float(plotAreaH)/clientDcSize[1]
1375 aveScale= (ratioW+ratioH)/2
1376 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1377
1378 self.graph._printDraw(dc)
1379 # rescale back to original
1380 self.graph._setSize()
1381 self.graph._setPrinterScale(1)
1382 self.graph.Redraw() #to get point label scale and shift correct
1383
1384 return True
1385
1386
1387
1388
1389 #---------------------------------------------------------------------------
1390 # if running standalone...
1391 #
1392 # ...a sample implementation using the above
1393 #
1394
1395 def _draw1Objects():
1396 # 100 points sin function, plotted as green circles
1397 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
1398 data1.shape = (100, 2)
1399 data1[:,1] = _Numeric.sin(data1[:,0])
1400 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1401
1402 # 50 points cos function, plotted as red line
1403 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
1404 data1.shape = (50,2)
1405 data1[:,1] = _Numeric.cos(data1[:,0])
1406 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1407
1408 # A few more points...
1409 pi = _Numeric.pi
1410 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1411 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1412 marker='cross')
1413
1414 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1415
1416 def _draw2Objects():
1417 # 100 points sin function, plotted as green dots
1418 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
1419 data1.shape = (100, 2)
1420 data1[:,1] = _Numeric.sin(data1[:,0])
1421 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1422
1423 # 50 points cos function, plotted as red dot-dash
1424 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
1425 data1.shape = (50,2)
1426 data1[:,1] = _Numeric.cos(data1[:,0])
1427 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1428
1429 # A few more points...
1430 pi = _Numeric.pi
1431 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1432 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1433 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1434 marker='square')
1435
1436 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1437
1438 def _draw3Objects():
1439 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1440 'cross', 'plus', 'circle']
1441 m=[]
1442 for i in range(len(markerList)):
1443 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1444 marker=markerList[i]))
1445 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1446
1447 def _draw4Objects():
1448 # 25,000 point line
1449 data1 = _Numeric.arange(5e5,1e6,10)
1450 data1.shape = (25000, 2)
1451 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1452
1453 # A few more points...
1454 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1455 marker='square')
1456 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1457
1458 def _draw5Objects():
1459 # Empty graph with axis defined but no points/lines
1460 points=[]
1461 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1462 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1463
1464 def _draw6Objects():
1465 # Bar graph
1466 points1=[(1,0), (1,10)]
1467 line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1468 points1g=[(2,0), (2,4)]
1469 line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1470 points1b=[(3,0), (3,6)]
1471 line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1472
1473 points2=[(4,0), (4,12)]
1474 line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1475 points2g=[(5,0), (5,8)]
1476 line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1477 points2b=[(6,0), (6,4)]
1478 line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1479
1480 return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1481 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1482
1483
1484 class TestFrame(wx.Frame):
1485 def __init__(self, parent, id, title):
1486 wx.Frame.__init__(self, parent, id, title,
1487 wx.DefaultPosition, (600, 400))
1488
1489 # Now Create the menu bar and items
1490 self.mainmenu = wx.MenuBar()
1491
1492 menu = wx.Menu()
1493 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1494 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1495
1496 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1497 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1498
1499 menu.Append(202, 'Print...', 'Print the current plot')
1500 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1501
1502 menu.Append(203, 'Save Plot...', 'Save current plot')
1503 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1504
1505 menu.Append(205, 'E&xit', 'Enough of this already!')
1506 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1507 self.mainmenu.Append(menu, '&File')
1508
1509 menu = wx.Menu()
1510 menu.Append(206, 'Draw1', 'Draw plots1')
1511 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1512 menu.Append(207, 'Draw2', 'Draw plots2')
1513 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1514 menu.Append(208, 'Draw3', 'Draw plots3')
1515 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1516 menu.Append(209, 'Draw4', 'Draw plots4')
1517 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1518 menu.Append(210, 'Draw5', 'Draw plots5')
1519 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1520 menu.Append(260, 'Draw6', 'Draw plots6')
1521 self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
1522
1523
1524 menu.Append(211, '&Redraw', 'Redraw plots')
1525 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1526 menu.Append(212, '&Clear', 'Clear canvas')
1527 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1528 menu.Append(213, '&Scale', 'Scale canvas')
1529 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1530 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1531 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1532 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1533 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1534 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1535 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1536 menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1537 self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1538
1539 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1540 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1541 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1542 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1543 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1544 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1545
1546 self.mainmenu.Append(menu, '&Plot')
1547
1548 menu = wx.Menu()
1549 menu.Append(300, '&About', 'About this thing...')
1550 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1551 self.mainmenu.Append(menu, '&Help')
1552
1553 self.SetMenuBar(self.mainmenu)
1554
1555 # A status bar to tell people what's happening
1556 self.CreateStatusBar(1)
1557
1558 self.client = PlotCanvas(self)
1559 #define the function for drawing pointLabels
1560 self.client.SetPointLabelFunc(self.DrawPointLabel)
1561 # Create mouse event for showing cursor coords in status bar
1562 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1563 # Show closest point when enabled
1564 self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1565
1566 self.Show(True)
1567
1568 def DrawPointLabel(self, dc, mDataDict):
1569 """This is the fuction that defines how the pointLabels are plotted
1570 dc - DC that will be passed
1571 mDataDict - Dictionary of data that you want to use for the pointLabel
1572
1573 As an example I have decided I want a box at the curve point
1574 with some text information about the curve plotted below.
1575 Any wxDC method can be used.
1576 """
1577 # ----------
1578 dc.SetPen(wx.Pen(wx.BLACK))
1579 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1580
1581 sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1582 dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
1583 px,py = mDataDict["pointXY"]
1584 cNum = mDataDict["curveNum"]
1585 pntIn = mDataDict["pIndex"]
1586 legend = mDataDict["legend"]
1587 #make a string to display
1588 s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1589 dc.DrawText(s, sx , sy+1)
1590 # -----------
1591
1592 def OnMouseLeftDown(self,event):
1593 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1594 self.SetStatusText(s)
1595 event.Skip() #allows plotCanvas OnMouseLeftDown to be called
1596
1597 def OnMotion(self, event):
1598 #show closest point (when enbled)
1599 if self.client.GetEnablePointLabel() == True:
1600 #make up dict with info for the pointLabel
1601 #I've decided to mark the closest point on the closest curve
1602 dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1603 if dlst != []: #returns [] if none
1604 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1605 #make up dictionary to pass to my user function (see DrawPointLabel)
1606 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1607 "pointXY":pointXY, "scaledXY":scaledXY}
1608 #pass dict to update the pointLabel
1609 self.client.UpdatePointLabel(mDataDict)
1610 event.Skip() #go to next handler
1611
1612 def OnFilePageSetup(self, event):
1613 self.client.PageSetup()
1614
1615 def OnFilePrintPreview(self, event):
1616 self.client.PrintPreview()
1617
1618 def OnFilePrint(self, event):
1619 self.client.Printout()
1620
1621 def OnSaveFile(self, event):
1622 self.client.SaveFile()
1623
1624 def OnFileExit(self, event):
1625 self.Close()
1626
1627 def OnPlotDraw1(self, event):
1628 self.resetDefaults()
1629 self.client.Draw(_draw1Objects())
1630
1631 def OnPlotDraw2(self, event):
1632 self.resetDefaults()
1633 self.client.Draw(_draw2Objects())
1634
1635 def OnPlotDraw3(self, event):
1636 self.resetDefaults()
1637 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1638 self.client.SetFontSizeAxis(20)
1639 self.client.SetFontSizeLegend(12)
1640 self.client.SetXSpec('min')
1641 self.client.SetYSpec('none')
1642 self.client.Draw(_draw3Objects())
1643
1644 def OnPlotDraw4(self, event):
1645 self.resetDefaults()
1646 drawObj= _draw4Objects()
1647 self.client.Draw(drawObj)
1648 ## # profile
1649 ## start = _time.clock()
1650 ## for x in range(10):
1651 ## self.client.Draw(drawObj)
1652 ## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1653 ## # profile end
1654
1655 def OnPlotDraw5(self, event):
1656 # Empty plot with just axes
1657 self.resetDefaults()
1658 drawObj= _draw5Objects()
1659 # make the axis X= (0,5), Y=(0,10)
1660 # (default with None is X= (-1,1), Y= (-1,1))
1661 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1662
1663 def OnPlotDraw6(self, event):
1664 #Bar Graph Example
1665 self.resetDefaults()
1666 #self.client.SetEnableLegend(True) #turn on Legend
1667 #self.client.SetEnableGrid(True) #turn on Grid
1668 self.client.SetXSpec('none') #turns off x-axis scale
1669 self.client.SetYSpec('auto')
1670 self.client.Draw(_draw6Objects(), xAxis= (0,7))
1671
1672 def OnPlotRedraw(self,event):
1673 self.client.Redraw()
1674
1675 def OnPlotClear(self,event):
1676 self.client.Clear()
1677
1678 def OnPlotScale(self, event):
1679 if self.client.last_draw != None:
1680 graphics, xAxis, yAxis= self.client.last_draw
1681 self.client.Draw(graphics,(1,3.05),(0,1))
1682
1683 def OnEnableZoom(self, event):
1684 self.client.SetEnableZoom(event.IsChecked())
1685
1686 def OnEnableGrid(self, event):
1687 self.client.SetEnableGrid(event.IsChecked())
1688
1689 def OnEnableLegend(self, event):
1690 self.client.SetEnableLegend(event.IsChecked())
1691
1692 def OnEnablePointLabel(self, event):
1693 self.client.SetEnablePointLabel(event.IsChecked())
1694
1695 def OnScrUp(self, event):
1696 self.client.ScrollUp(1)
1697
1698 def OnScrRt(self,event):
1699 self.client.ScrollRight(2)
1700
1701 def OnReset(self,event):
1702 self.client.Reset()
1703
1704 def OnHelpAbout(self, event):
1705 from wx.lib.dialogs import ScrolledMessageDialog
1706 about = ScrolledMessageDialog(self, __doc__, "About...")
1707 about.ShowModal()
1708
1709 def resetDefaults(self):
1710 """Just to reset the fonts back to the PlotCanvas defaults"""
1711 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1712 self.client.SetFontSizeAxis(10)
1713 self.client.SetFontSizeLegend(7)
1714 self.client.SetXSpec('auto')
1715 self.client.SetYSpec('auto')
1716
1717
1718 def __test():
1719
1720 class MyApp(wx.App):
1721 def OnInit(self):
1722 wx.InitAllImageHandlers()
1723 frame = TestFrame(None, -1, "PlotCanvas")
1724 #frame.Show(True)
1725 self.SetTopWindow(frame)
1726 return True
1727
1728
1729 app = MyApp(0)
1730 app.MainLoop()
1731
1732 if __name__ == '__main__':
1733 __test()