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