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