1 #-----------------------------------------------------------------------------
3 # Purpose: Line, Bar and Scatter Graphs
5 # Author: Gordon Williams
9 # Copyright: (c) 2002,2007
10 # Licence: Use as you wish.
11 #-----------------------------------------------------------------------------
12 # 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
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.
19 # 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
21 # o wxScrolledMessageDialog -> ScrolledMessageDialog
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
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.
37 # May 27, 2007 Johnathan Corgan (jcorgan@corganenterprises.com)
38 # - Converted from numarray to numpy
41 This is a simple light weight plotting module that can be used with
42 Boa or easily integrated into your own wxPython application. The
43 emphasis is on small size and fast plotting for large data sets. It
44 has a reasonable number of features to do line and scatter graphs
45 easily as well as simple bar graphs. It is not as sophisticated or
46 as powerful as SciPy Plt or Chaco. Both of these are great packages
47 but consume huge amounts of computer resources for simple plots.
48 They can be found at http://scipy.com
50 This file contains two parts; first the re-usable library stuff, then,
51 after a "if __name__=='__main__'" test, a simple frame and a few default
52 plots for examples and testing.
55 Written by K.Hinsen, R. Srinivasan;
56 Ported to wxPython Harm van der Heijden, feb 1999
58 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
60 -Zooming using mouse 'rubber band'
63 -Printing, preview, and page set up (margins)
64 -Axis and title labels
65 -Cursor xy axis values
66 -Doc strings and lots of comments
67 -Optimizations for large number of points
70 Did a lot of work here to speed markers up. Only a factor of 4
71 improvement though. Lines are much faster than markers, especially
72 filled markers. Stay away from circles and triangles unless you
73 only have a few thousand points.
75 Times for 25,000 points
82 triangle, triangle_down - 0.90
84 Thanks to Chris Barker for getting this version working on Linux.
86 Zooming controls with mouse (when enabled):
87 Left mouse drag - Zoom box.
88 Left mouse double click - reset zoom.
89 Right mouse click - zoom out centred on click location.
92 import string as _string
96 # Needs numpy or numarray
98 import numpy as _numpy
101 import numarray as _numpy #if numarray is used it is renamed numpy
104 This module requires the numpy or numarray module,
105 which could not be imported. It probably is not installed
106 (it's not part of the standard Python distribution). See the
107 Python site (http://www.python.org) for information on
108 downloading source or binaries."""
109 raise ImportError, "numpy or numarray not found. \n" + msg
114 # Plotting classes...
117 """Base Class for lines and markers
118 - All methods are private.
121 def __init__(self, points, attr):
122 self.points = _numpy.array(points)
123 self.currentScale= (1,1)
124 self.currentShift= (0,0)
125 self.scaled = self.points
127 self.attributes.update(self._attributes)
128 for name, value in attr.items():
129 if name not in self._attributes.keys():
130 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
131 self.attributes[name] = value
133 def boundingBox(self):
134 if len(self.points) == 0:
136 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
137 minXY= _numpy.array([-1,-1])
138 maxXY= _numpy.array([ 1, 1])
140 minXY= _numpy.minimum.reduce(self.points)
141 maxXY= _numpy.maximum.reduce(self.points)
144 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
145 if len(self.points) == 0:
148 if (scale is not self.currentScale) or (shift is not self.currentShift):
149 # update point scaling
150 self.scaled = scale*self.points+shift
151 self.currentScale= scale
152 self.currentShift= shift
153 # else unchanged use the current scaling
156 return self.attributes['legend']
158 def getClosestPoint(self, pntXY, pointScaled= True):
159 """Returns the index of closest point on the curve, pointXY, scaledXY, distance
161 if pointScaled == True based on screen coords
162 if pointScaled == False based on user coords
164 if pointScaled == True:
167 pxy = self.currentScale * _numpy.array(pntXY)+ self.currentShift
171 pxy = _numpy.array(pntXY)
172 #determine distance for each point
173 d= _numpy.sqrt(_numpy.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
174 pntIndex = _numpy.argmin(d)
176 return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
179 class PolyLine(PolyPoints):
180 """Class to define line type and style
181 - All methods except __init__ are private.
184 _attributes = {'colour': 'black',
189 def __init__(self, points, **attr):
190 """Creates PolyLine object
191 points - sequence (array, tuple or list) of (x,y) points making up line
192 **attr - key word attributes
194 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
195 'width'= 1, - Pen width
196 'style'= wx.SOLID, - wx.Pen style
197 'legend'= '' - Line Legend to display
199 PolyPoints.__init__(self, points, attr)
201 def draw(self, dc, printerScale, coord= None):
202 colour = self.attributes['colour']
203 width = self.attributes['width'] * printerScale
204 style= self.attributes['style']
205 pen = wx.Pen(wx.NamedColour(colour), width, style)
206 pen.SetCap(wx.CAP_BUTT)
209 dc.DrawLines(self.scaled)
211 dc.DrawLines(coord) # draw legend line
213 def getSymExtent(self, printerScale):
214 """Width and Height of Marker"""
215 h= self.attributes['width'] * printerScale
220 class PolyMarker(PolyPoints):
221 """Class to define marker type and style
222 - All methods except __init__ are private.
225 _attributes = {'colour': 'black',
229 'fillstyle': wx.SOLID,
233 def __init__(self, points, **attr):
234 """Creates PolyMarker object
235 points - sequence (array, tuple or list) of (x,y) points
236 **attr - key word attributes
238 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
239 'width'= 1, - Pen width
240 'size'= 2, - Marker size
241 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
242 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
243 'marker'= 'circle' - Marker shape
244 'legend'= '' - Marker Legend to display
256 PolyPoints.__init__(self, points, attr)
258 def draw(self, dc, printerScale, coord= None):
259 colour = self.attributes['colour']
260 width = self.attributes['width'] * printerScale
261 size = self.attributes['size'] * printerScale
262 fillcolour = self.attributes['fillcolour']
263 fillstyle = self.attributes['fillstyle']
264 marker = self.attributes['marker']
266 dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
268 dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
270 dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
272 self._drawmarkers(dc, self.scaled, marker, size)
274 self._drawmarkers(dc, coord, marker, size) # draw legend marker
276 def getSymExtent(self, printerScale):
277 """Width and Height of Marker"""
278 s= 5*self.attributes['size'] * printerScale
281 def _drawmarkers(self, dc, coords, marker,size=1):
282 f = eval('self._' +marker)
285 def _circle(self, dc, coords, size=1):
288 rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
289 rect[:,0:2]= coords-[fact,fact]
290 dc.DrawEllipseList(rect.astype(_numpy.int32))
292 def _dot(self, dc, coords, size=1):
293 dc.DrawPointList(coords)
295 def _square(self, dc, coords, size=1):
298 rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
299 rect[:,0:2]= coords-[fact,fact]
300 dc.DrawRectangleList(rect.astype(_numpy.int32))
302 def _triangle(self, dc, coords, size=1):
303 shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
304 poly= _numpy.repeat(coords,3)
305 poly.shape= (len(coords),3,2)
307 dc.DrawPolygonList(poly.astype(_numpy.int32))
309 def _triangle_down(self, dc, coords, size=1):
310 shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
311 poly= _numpy.repeat(coords,3)
312 poly.shape= (len(coords),3,2)
314 dc.DrawPolygonList(poly.astype(_numpy.int32))
316 def _cross(self, dc, coords, size=1):
318 for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
319 lines= _numpy.concatenate((coords,coords),axis=1)+f
320 dc.DrawLineList(lines.astype(_numpy.int32))
322 def _plus(self, dc, coords, size=1):
324 for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
325 lines= _numpy.concatenate((coords,coords),axis=1)+f
326 dc.DrawLineList(lines.astype(_numpy.int32))
329 """Container to hold PolyXXX objects and graph labels
330 - All methods except __init__ are private.
333 def __init__(self, objects, title='', xLabel='', yLabel= ''):
334 """Creates PlotGraphics object
335 objects - list of PolyXXX objects to make graph
336 title - title shown at top of graph
337 xLabel - label shown on x-axis
338 yLabel - label shown on y-axis
340 if type(objects) not in [list,tuple]:
341 raise TypeError, "objects argument should be list or tuple"
342 self.objects = objects
347 def boundingBox(self):
348 p1, p2 = self.objects[0].boundingBox()
349 for o in self.objects[1:]:
350 p1o, p2o = o.boundingBox()
351 p1 = _numpy.minimum(p1, p1o)
352 p2 = _numpy.maximum(p2, p2o)
355 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
356 for o in self.objects:
357 o.scaleAndShift(scale, shift)
359 def setPrinterScale(self, scale):
360 """Thickens up lines and markers only for printing"""
361 self.printerScale= scale
363 def setXLabel(self, xLabel= ''):
364 """Set the X axis label on the graph"""
367 def setYLabel(self, yLabel= ''):
368 """Set the Y axis label on the graph"""
371 def setTitle(self, title= ''):
372 """Set the title at the top of graph"""
376 """Get x axis label string"""
380 """Get y axis label string"""
383 def getTitle(self, title= ''):
384 """Get the title at the top of graph"""
388 for o in self.objects:
389 #t=_time.clock() # profile info
390 o.draw(dc, self.printerScale)
392 #print o, "time=", dt
394 def getSymExtent(self, printerScale):
395 """Get max width and height of lines and markers symbols for legend"""
396 symExt = self.objects[0].getSymExtent(printerScale)
397 for o in self.objects[1:]:
398 oSymExt = o.getSymExtent(printerScale)
399 symExt = _numpy.maximum(symExt, oSymExt)
402 def getLegendNames(self):
403 """Returns list of legend names"""
404 lst = [None]*len(self)
405 for i in range(len(self)):
406 lst[i]= self.objects[i].getLegend()
410 return len(self.objects)
412 def __getitem__(self, item):
413 return self.objects[item]
416 #-------------------------------------------------------------------------------
417 # Main window that you will want to import into your application.
419 class PlotCanvas(wx.Window):
420 """Subclass of a wx.Window to allow simple general plotting
421 of data with zoom, labels, and automatic axis scaling."""
423 def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
424 size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
425 """Constucts a window, which can be a child of a frame, dialog or
426 any other non-control window"""
428 wx.Window.__init__(self, parent, id, pos, size, style, name)
431 self.SetBackgroundColour("white")
433 # Create some mouse events for zooming
434 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
435 self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
436 self.Bind(wx.EVT_MOTION, self.OnMotion)
437 self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
438 self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
440 # set curser as cross-hairs
441 self.SetCursor(wx.CROSS_CURSOR)
443 # Things for printing
444 self.print_data = wx.PrintData()
445 self.print_data.SetPaperId(wx.PAPER_LETTER)
446 self.print_data.SetOrientation(wx.LANDSCAPE)
447 self.pageSetupData= wx.PageSetupDialogData()
448 self.pageSetupData.SetMarginBottomRight((25,25))
449 self.pageSetupData.SetMarginTopLeft((25,25))
450 self.pageSetupData.SetPrintData(self.print_data)
451 self.printerScale = 1
455 self._zoomInFactor = 0.5
456 self._zoomOutFactor = 2
457 self._zoomCorner1= _numpy.array([0.0, 0.0]) # left mouse down corner
458 self._zoomCorner2= _numpy.array([0.0, 0.0]) # left mouse up corner
459 self._zoomEnabled= False
460 self._hasDragged= False
463 self.last_draw = None
468 self._gridEnabled= False
469 self._legendEnabled= False
470 self._xUseScopeTicks= False
474 self._fontSizeAxis= 10
475 self._fontSizeTitle= 15
476 self._fontSizeLegend= 7
479 self._pointLabelEnabled= False
480 self.last_PointLabel= None
481 self._pointLabelFunc= None
482 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
484 self.Bind(wx.EVT_PAINT, self.OnPaint)
485 self.Bind(wx.EVT_SIZE, self.OnSize)
486 # OnSize called to make sure the buffer is initialized.
487 # This might result in OnSize getting called twice on some
488 # platforms at initialization, but little harm done.
489 self.OnSize(None) # sets the initial size based on client size
490 # UNCONDITIONAL, needed to create self._Buffer
493 def SaveFile(self, fileName= ''):
494 """Saves the file to the type specified in the extension. If no file
495 name is specified a dialog box is provided. Returns True if sucessful,
498 .bmp Save a Windows bitmap file.
499 .xbm Save an X bitmap file.
500 .xpm Save an XPM bitmap file.
501 .png Save a Portable Network Graphics file.
502 .jpg Save a Joint Photographic Experts Group file.
504 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
505 dlg1 = wx.FileDialog(
507 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
508 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
509 wx.SAVE|wx.OVERWRITE_PROMPT
513 if dlg1.ShowModal() == wx.ID_OK:
514 fileName = dlg1.GetPath()
515 # Check for proper exension
516 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
517 dlg2 = wx.MessageDialog(self, 'File name extension\n'
519 'bmp, xbm, xpm, png, or jpg',
520 'File Name Error', wx.OK | wx.ICON_ERROR)
526 break # now save file
527 else: # exit without saving
532 # File name has required extension
533 fType = _string.lower(fileName[-3:])
535 tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
537 tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
539 tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
541 tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
543 tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
545 res= self._Buffer.SaveFile(fileName, tp)
549 """Brings up the page setup dialog"""
550 data = self.pageSetupData
551 data.SetPrintData(self.print_data)
552 dlg = wx.PageSetupDialog(self.parent, data)
554 if dlg.ShowModal() == wx.ID_OK:
555 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
556 # updates page parameters from dialog
557 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
558 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
559 self.pageSetupData.SetPrintData(data.GetPrintData())
560 self.print_data=data.GetPrintData() # updates print_data
564 def Printout(self, paper=None):
565 """Print current plot."""
567 self.print_data.SetPaperId(paper)
568 pdd = wx.PrintDialogData()
569 pdd.SetPrintData(self.print_data)
570 printer = wx.Printer(pdd)
571 out = PlotPrintout(self)
572 print_ok = printer.Print(self.parent, out)
574 self.print_data = printer.GetPrintDialogData().GetPrintData()
577 def PrintPreview(self):
578 """Print-preview current plot."""
579 printout = PlotPrintout(self)
580 printout2 = PlotPrintout(self)
581 self.preview = wx.PrintPreview(printout, printout2, self.print_data)
582 if not self.preview.Ok():
583 wx.MessageDialog(self, "Print Preview failed.\n" \
584 "Check that default printer is configured\n", \
585 "Print error", wx.OK|wx.CENTRE).ShowModal()
586 self.preview.SetZoom(40)
587 # search up tree to find frame instance
589 while not isinstance(frameInst, wx.Frame):
590 frameInst= frameInst.GetParent()
591 frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
593 frame.SetPosition(self.GetPosition())
594 frame.SetSize((600,550))
595 frame.Centre(wx.BOTH)
598 def SetFontSizeAxis(self, point= 10):
599 """Set the tick and axis label font size (default is 10 point)"""
600 self._fontSizeAxis= point
602 def GetFontSizeAxis(self):
603 """Get current tick and axis label font size in points"""
604 return self._fontSizeAxis
606 def SetFontSizeTitle(self, point= 15):
607 """Set Title font size (default is 15 point)"""
608 self._fontSizeTitle= point
610 def GetFontSizeTitle(self):
611 """Get current Title font size in points"""
612 return self._fontSizeTitle
614 def SetFontSizeLegend(self, point= 7):
615 """Set Legend font size (default is 7 point)"""
616 self._fontSizeLegend= point
618 def GetFontSizeLegend(self):
619 """Get current Legend font size in points"""
620 return self._fontSizeLegend
622 def SetEnableZoom(self, value):
623 """Set True to enable zooming."""
624 if value not in [True,False]:
625 raise TypeError, "Value should be True or False"
626 self._zoomEnabled= value
628 def GetEnableZoom(self):
629 """True if zooming enabled."""
630 return self._zoomEnabled
632 def SetEnableGrid(self, value):
633 """Set True to enable grid."""
634 if value not in [True,False]:
635 raise TypeError, "Value should be True or False"
636 self._gridEnabled= value
639 def GetEnableGrid(self):
640 """True if grid enabled."""
641 return self._gridEnabled
643 def SetEnableLegend(self, value):
644 """Set True to enable legend."""
645 if value not in [True,False]:
646 raise TypeError, "Value should be True or False"
647 self._legendEnabled= value
650 def GetEnableLegend(self):
651 """True if Legend enabled."""
652 return self._legendEnabled
654 def SetEnablePointLabel(self, value):
655 """Set True to enable pointLabel."""
656 if value not in [True,False]:
657 raise TypeError, "Value should be True or False"
658 self._pointLabelEnabled= value
659 self.Redraw() #will erase existing pointLabel if present
660 self.last_PointLabel = None
662 def GetEnablePointLabel(self):
663 """True if pointLabel enabled."""
664 return self._pointLabelEnabled
666 def SetPointLabelFunc(self, func):
667 """Sets the function with custom code for pointLabel drawing
668 ******** more info needed ***************
670 self._pointLabelFunc= func
672 def GetPointLabelFunc(self):
673 """Returns pointLabel Drawing Function"""
674 return self._pointLabelFunc
677 """Unzoom the plot."""
678 self.last_PointLabel = None #reset pointLabel
679 if self.last_draw is not None:
680 self.Draw(self.last_draw[0])
682 def ScrollRight(self, units):
683 """Move view right number of axis units."""
684 self.last_PointLabel = None #reset pointLabel
685 if self.last_draw is not None:
686 graphics, xAxis, yAxis= self.last_draw
687 xAxis= (xAxis[0]+units, xAxis[1]+units)
688 self.Draw(graphics,xAxis,yAxis)
690 def ScrollUp(self, units):
691 """Move view up number of axis units."""
692 self.last_PointLabel = None #reset pointLabel
693 if self.last_draw is not None:
694 graphics, xAxis, yAxis= self.last_draw
695 yAxis= (yAxis[0]+units, yAxis[1]+units)
696 self.Draw(graphics,xAxis,yAxis)
698 def GetXY(self,event):
699 """Takes a mouse event and returns the XY user axis values."""
700 x,y= self.PositionScreenToUser(event.GetPosition())
703 def PositionUserToScreen(self, pntXY):
704 """Converts User position to Screen Coordinates"""
705 userPos= _numpy.array(pntXY)
706 x,y= userPos * self._pointScale + self._pointShift
709 def PositionScreenToUser(self, pntXY):
710 """Converts Screen position to User Coordinates"""
711 screenPos= _numpy.array(pntXY)
712 x,y= (screenPos-self._pointShift)/self._pointScale
715 def SetXSpec(self, type= 'auto'):
716 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
718 'none' - shows no axis or tick mark values
719 'min' - shows min bounding box values
720 'auto' - rounds axis range to sensible values
724 def SetYSpec(self, type= 'auto'):
725 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
727 'none' - shows no axis or tick mark values
728 'min' - shows min bounding box values
729 'auto' - rounds axis range to sensible values
734 """Returns current XSpec for axis"""
738 """Returns current YSpec for axis"""
741 def GetXMaxRange(self):
742 """Returns (minX, maxX) x-axis range for displayed graph"""
743 graphics= self.last_draw[0]
744 p1, p2 = graphics.boundingBox() # min, max points of graphics
745 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
748 def GetYMaxRange(self):
749 """Returns (minY, maxY) y-axis range for displayed graph"""
750 graphics= self.last_draw[0]
751 p1, p2 = graphics.boundingBox() # min, max points of graphics
752 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
755 def GetXCurrentRange(self):
756 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
757 return self.last_draw[1]
759 def GetYCurrentRange(self):
760 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
761 return self.last_draw[2]
763 def SetXUseScopeTicks(self, v=False):
764 """Always 10 divisions, no labels"""
765 self._xUseScopeTicks = v
767 def GetXUseScopeTicks(self):
768 return self._xUseScopeTicks
770 def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, step=None):
771 """Draw objects in graphics with specified x and y axis.
772 graphics- instance of PlotGraphics with list of PolyXXX objects
773 xAxis - tuple with (min, max) axis range to view
774 yAxis - same as xAxis
775 dc - drawing context - doesn't have to be specified.
776 If it's not, the offscreen buffer is used
778 # check Axis is either tuple or none
779 if type(xAxis) not in [type(None),tuple]:
780 raise TypeError, "xAxis should be None or (minX,maxX)"
781 if type(yAxis) not in [type(None),tuple]:
782 raise TypeError, "yAxis should be None or (minY,maxY)"
784 # check case for axis = (a,b) where a==b caused by improper zooms
786 if xAxis[0] == xAxis[1]:
789 if yAxis[0] == yAxis[1]:
793 # sets new dc and clears it
794 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
800 # set font size for every thing but title and legend
801 dc.SetFont(self._getFont(self._fontSizeAxis))
803 # sizes axis to axis type, create lower left and upper right corners of plot
804 if xAxis == None or yAxis == None:
805 # One or both axis not specified in Draw
806 p1, p2 = graphics.boundingBox() # min, max points of graphics
808 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
810 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
811 # Adjust bounding box for axis spec
812 p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
813 p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
815 # Both axis specified in Draw
816 p1= _numpy.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
817 p2= _numpy.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
819 self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
821 # Get ticks and textExtents for axis if required
822 if self._xSpec is not 'none':
823 if self._xUseScopeTicks:
824 xticks = self._scope_ticks(xAxis[0], xAxis[1])
826 xticks = self._ticks(xAxis[0], xAxis[1])
827 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
830 xTextExtent= (0,0) # No text for ticks
831 if self._ySpec is not 'none':
832 yticks = self._ticks(yAxis[0], yAxis[1], step)
833 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
834 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
835 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
836 max(yTextExtentBottom[1],yTextExtentTop[1]))
839 yTextExtent= (0,0) # No text for ticks
841 # TextExtents for Title and Axis Labels
842 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
844 # TextExtents for Legend
845 legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
847 # room around graph area
848 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
849 lhsW= yTextExtent[0]+ yLabelWH[1]
850 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
851 topH= yTextExtent[1]/2. + titleWH[1]
852 textSize_scale= _numpy.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
853 textSize_shift= _numpy.array([lhsW, bottomH]) # shift plot area by this amount
855 # drawing title and labels text
856 dc.SetFont(self._getFont(self._fontSizeTitle))
857 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
858 self.plotbox_origin[1]- self.plotbox_size[1])
859 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
860 dc.SetFont(self._getFont(self._fontSizeAxis))
861 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
862 self.plotbox_origin[1]- xLabelWH[1])
863 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
864 yLabelPos= (self.plotbox_origin[0],
865 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
866 if graphics.getYLabel(): # bug fix for Linux
867 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
869 # drawing legend makers and text
870 if self._legendEnabled:
871 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
873 # allow for scaling and shifting plotted points
874 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _numpy.array((1,-1))
875 shift = -p1*scale + self.plotbox_origin + textSize_shift * _numpy.array((1,-1))
876 self._pointScale= scale # make available for mouse events
877 self._pointShift= shift
878 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
880 graphics.scaleAndShift(scale, shift)
881 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
883 # set clipping area so drawing does not occur outside axis box
884 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
885 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
886 # Draw the lines and markers
887 #start = _time.clock()
889 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
890 # remove the clipping region
891 dc.DestroyClippingRegion()
894 def Redraw(self, dc= None):
895 """Redraw the existing plot."""
896 if self.last_draw is not None:
897 graphics, xAxis, yAxis= self.last_draw
898 self.Draw(graphics,xAxis,yAxis,dc)
901 """Erase the window."""
902 self.last_PointLabel = None #reset pointLabel
903 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
905 self.last_draw = None
907 def Zoom(self, Center, Ratio):
909 Centers on the X,Y coords given in Center
910 Zooms by the Ratio = (Xratio, Yratio) given
912 self.last_PointLabel = None #reset maker
914 if self.last_draw != None:
915 (graphics, xAxis, yAxis) = self.last_draw
916 w = (xAxis[1] - xAxis[0]) * Ratio[0]
917 h = (yAxis[1] - yAxis[0]) * Ratio[1]
918 xAxis = ( x - w/2, x + w/2 )
919 yAxis = ( y - h/2, y + h/2 )
920 self.Draw(graphics, xAxis, yAxis)
922 def GetClosestPoints(self, pntXY, pointScaled= True):
924 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
926 Returns [] if no curves are being plotted.
929 if pointScaled == True based on screen coords
930 if pointScaled == False based on user coords
932 if self.last_draw == None:
935 graphics, xAxis, yAxis= self.last_draw
937 for curveNum,obj in enumerate(graphics):
938 #check there are points in the curve
939 if len(obj.points) == 0:
940 continue #go to next obj
941 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
942 cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
946 def GetClosetPoint(self, pntXY, pointScaled= True):
948 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
949 list for only the closest curve.
950 Returns [] if no curves are being plotted.
953 if pointScaled == True based on screen coords
954 if pointScaled == False based on user coords
956 #closest points on screen based on screen scaling (pointScaled= True)
957 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
958 closestPts= self.GetClosestPoints(pntXY, pointScaled)
960 return [] #no graph present
961 #find one with least distance
962 dists = [c[-1] for c in closestPts]
963 mdist = min(dists) #Min dist
964 i = dists.index(mdist) #index for min dist
965 return closestPts[i] #this is the closest point on closest curve
967 def UpdatePointLabel(self, mDataDict):
968 """Updates the pointLabel point on screen with data contained in
971 mDataDict will be passed to your function set by
972 SetPointLabelFunc. It can contain anything you
973 want to display on the screen at the scaledXY point
976 This function can be called from parent window with onClick,
979 if self.last_PointLabel != None:
981 if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
983 self._drawPointLabel(self.last_PointLabel) #erase old
984 self._drawPointLabel(mDataDict) #plot new
986 #just plot new with no erase
987 self._drawPointLabel(mDataDict) #plot new
989 self.last_PointLabel = mDataDict
991 # event handlers **********************************
992 def OnMotion(self, event):
993 if self._zoomEnabled and event.LeftIsDown():
995 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
997 self._hasDragged= True
998 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
999 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
1001 def OnMouseLeftDown(self,event):
1002 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
1004 def OnMouseLeftUp(self, event):
1005 if self._zoomEnabled:
1006 if self._hasDragged == True:
1007 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1008 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
1009 self._hasDragged = False # reset flag
1010 minX, minY= _numpy.minimum( self._zoomCorner1, self._zoomCorner2)
1011 maxX, maxY= _numpy.maximum( self._zoomCorner1, self._zoomCorner2)
1012 self.last_PointLabel = None #reset pointLabel
1013 if self.last_draw != None:
1014 self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
1015 #else: # A box has not been drawn, zoom in on a point
1016 ## this interfered with the double click, so I've disables it.
1017 # X,Y = self.GetXY(event)
1018 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1020 def OnMouseDoubleClick(self,event):
1021 if self._zoomEnabled:
1024 def OnMouseRightDown(self,event):
1025 if self._zoomEnabled:
1026 X,Y = self.GetXY(event)
1027 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1029 def OnPaint(self, event):
1030 # All that is needed here is to draw the buffer to screen
1031 if self.last_PointLabel != None:
1032 self._drawPointLabel(self.last_PointLabel) #erase old
1033 self.last_PointLabel = None
1034 dc = wx.BufferedPaintDC(self, self._Buffer)
1036 def OnSize(self,event):
1037 # The Buffer init is done here, to make sure the buffer is always
1038 # the same size as the Window
1039 Size = self.GetClientSize()
1041 # Make new offscreen bitmap: this bitmap will always have the
1042 # current drawing in it, so it can be used to save the image to
1043 # a file, or whatever.
1044 self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
1047 self.last_PointLabel = None #reset pointLabel
1049 if self.last_draw is None:
1052 graphics, xSpec, ySpec = self.last_draw
1053 self.Draw(graphics,xSpec,ySpec)
1055 def OnLeave(self, event):
1056 """Used to erase pointLabel when mouse outside window"""
1057 if self.last_PointLabel != None:
1058 self._drawPointLabel(self.last_PointLabel) #erase old
1059 self.last_PointLabel = None
1062 # Private Methods **************************************************
1063 def _setSize(self, width=None, height=None):
1064 """DC width and height."""
1066 (self.width,self.height) = self.GetClientSize()
1068 self.width, self.height= width,height
1069 self.plotbox_size = 0.97*_numpy.array([self.width, self.height])
1070 xo = 0.5*(self.width-self.plotbox_size[0])
1071 yo = self.height-0.5*(self.height-self.plotbox_size[1])
1072 self.plotbox_origin = _numpy.array([xo, yo])
1074 def _setPrinterScale(self, scale):
1075 """Used to thicken lines and increase marker size for print out."""
1076 # line thickness on printer is very thin at 600 dot/in. Markers small
1077 self.printerScale= scale
1079 def _printDraw(self, printDC):
1080 """Used for printing."""
1081 if self.last_draw != None:
1082 graphics, xSpec, ySpec= self.last_draw
1083 self.Draw(graphics,xSpec,ySpec,printDC)
1085 def _drawPointLabel(self, mDataDict):
1086 """Draws and erases pointLabels"""
1087 width = self._Buffer.GetWidth()
1088 height = self._Buffer.GetHeight()
1089 tmp_Buffer = wx.EmptyBitmap(width,height)
1091 dcs.SelectObject(tmp_Buffer)
1094 self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
1097 dc = wx.ClientDC( self )
1098 #this will erase if called twice
1099 dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
1102 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
1103 """Draws legend symbols and text"""
1104 # top right hand corner of graph box is ref corner
1105 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1106 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
1107 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1108 dc.SetFont(self._getFont(self._fontSizeLegend))
1109 for i in range(len(graphics)):
1112 if isinstance(o,PolyMarker):
1113 # draw marker with legend
1114 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
1115 o.draw(dc, self.printerScale, coord= _numpy.array([pnt]))
1116 elif isinstance(o,PolyLine):
1117 # draw line with legend
1118 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1119 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
1120 o.draw(dc, self.printerScale, coord= _numpy.array([pnt1,pnt2]))
1122 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1124 pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
1125 dc.DrawText(o.getLegend(),pnt[0],pnt[1])
1126 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1128 def _titleLablesWH(self, dc, graphics):
1129 """Draws Title and labels and returns width and height for each"""
1130 # TextExtents for Title and Axis Labels
1131 dc.SetFont(self._getFont(self._fontSizeTitle))
1132 title= graphics.getTitle()
1133 titleWH= dc.GetTextExtent(title)
1134 dc.SetFont(self._getFont(self._fontSizeAxis))
1135 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1136 xLabelWH= dc.GetTextExtent(xLabel)
1137 yLabelWH= dc.GetTextExtent(yLabel)
1138 return titleWH, xLabelWH, yLabelWH
1140 def _legendWH(self, dc, graphics):
1141 """Returns the size in screen units for legend box"""
1142 if self._legendEnabled != True:
1143 legendBoxWH= symExt= txtExt= (0,0)
1145 # find max symbol size
1146 symExt= graphics.getSymExtent(self.printerScale)
1147 # find max legend text extent
1148 dc.SetFont(self._getFont(self._fontSizeLegend))
1149 txtList= graphics.getLegendNames()
1150 txtExt= dc.GetTextExtent(txtList[0])
1151 for txt in graphics.getLegendNames()[1:]:
1152 txtExt= _numpy.maximum(txtExt,dc.GetTextExtent(txt))
1153 maxW= symExt[0]+txtExt[0]
1154 maxH= max(symExt[1],txtExt[1])
1155 # padding .1 for lhs of legend box and space between lines
1157 maxH= maxH* 1.1 * len(txtList)
1158 dc.SetFont(self._getFont(self._fontSizeAxis))
1159 legendBoxWH= (maxW,maxH)
1160 return (legendBoxWH, symExt, txtExt)
1162 def _drawRubberBand(self, corner1, corner2):
1163 """Draws/erases rect box from corner1 to corner2"""
1164 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1166 dc = wx.ClientDC( self )
1168 dc.SetPen(wx.Pen(wx.BLACK))
1169 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1170 dc.SetLogicalFunction(wx.INVERT)
1171 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
1172 dc.SetLogicalFunction(wx.COPY)
1175 def _getFont(self,size):
1176 """Take font size, adjusts if printing and returns wx.Font"""
1177 s = size*self.printerScale
1179 # Linux speed up to get font from cache rather than X font server
1180 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1181 font = self._fontCache.get (key, None)
1183 return font # yeah! cache hit
1185 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1186 self._fontCache[key] = font
1190 def _point2ClientCoord(self, corner1, corner2):
1191 """Converts user point coords to client screen int coords x,y,width,height"""
1192 c1= _numpy.array(corner1)
1193 c2= _numpy.array(corner2)
1194 # convert to screen coords
1195 pt1= c1*self._pointScale+self._pointShift
1196 pt2= c2*self._pointScale+self._pointShift
1197 # make height and width positive
1198 pul= _numpy.minimum(pt1,pt2) # Upper left corner
1199 plr= _numpy.maximum(pt1,pt2) # Lower right corner
1200 rectWidth, rectHeight= plr-pul
1202 return ptx, pty, rectWidth, rectHeight
1204 def _axisInterval(self, spec, lower, upper):
1205 """Returns sensible axis range for given spec"""
1206 if spec == 'none' or spec == 'min':
1208 return lower-0.5, upper+0.5
1211 elif spec == 'auto':
1214 if abs(range) < 1e-36:
1215 return lower-0.5, upper+0.5
1216 log = _numpy.log10(range)
1217 power = _numpy.floor(log)
1218 fraction = log-power
1219 if fraction <= 0.05:
1222 lower = lower - lower % grid
1225 upper = upper - mod + grid
1227 elif type(spec) == type(()):
1234 raise ValueError, str(spec) + ': illegal axis specification'
1236 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1238 penWidth= self.printerScale # increases thickness for printing only
1239 dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
1241 # set length of tick marks--long ones make grid
1242 if self._gridEnabled:
1243 x,y,width,height= self._point2ClientCoord(p1,p2)
1244 yTickLength= width/2.0 +1
1245 xTickLength= height/2.0 +1
1247 yTickLength= 3 * self.printerScale # lengthens lines for printing
1248 xTickLength= 3 * self.printerScale
1250 if self._xSpec is not 'none':
1251 lower, upper = p1[0],p2[0]
1253 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
1254 a1 = scale*_numpy.array([lower, y])+shift
1255 a2 = scale*_numpy.array([upper, y])+shift
1256 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1257 for x, label in xticks:
1258 pt = scale*_numpy.array([x, y])+shift
1259 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1261 dc.DrawText(label,pt[0],pt[1])
1262 text = 0 # axis values not drawn on top side
1264 if self._ySpec is not 'none':
1265 lower, upper = p1[1],p2[1]
1267 h = dc.GetCharHeight()
1268 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1269 a1 = scale*_numpy.array([x, lower])+shift
1270 a2 = scale*_numpy.array([x, upper])+shift
1271 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1272 for y, label in yticks:
1273 pt = scale*_numpy.array([x, y])+shift
1274 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1276 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1278 text = 0 # axis values not drawn on right side
1280 def _ticks(self, lower, upper, step=None):
1281 ideal = (upper-lower)/7.
1282 log = _numpy.log10(ideal)
1283 power = _numpy.floor(log)
1284 fraction = log-power
1287 for f, lf in self._multiples:
1288 e = _numpy.fabs(fraction-lf)
1292 grid = factor * 10.**power
1293 if power > 4 or power < -4:
1296 digits = max(1, int(power))
1297 format = '%' + `digits`+'.0f'
1299 digits = -int(power)
1300 format = '%'+`digits+2`+'.'+`digits`+'f'
1301 #force grid when step is not None
1302 if step is not None: grid = step
1304 t = -grid*_numpy.floor(-lower/grid)
1306 if t == -0: t = 0 #remove neg zero condition
1307 ticks.append( (t, format % (t,)) )
1311 def _scope_ticks (self, lower, upper):
1312 '''Always 10 divisions, no labels'''
1313 grid = (upper - lower) / 10.0
1317 ticks.append( (t, ""))
1321 _multiples = [(2., _numpy.log10(2.)), (5., _numpy.log10(5.))]
1324 #-------------------------------------------------------------------------------
1325 # Used to layout the printer page
1327 class PlotPrintout(wx.Printout):
1328 """Controls how the plot is made in printing and previewing"""
1329 # Do not change method names in this class,
1330 # we have to override wx.Printout methods here!
1331 def __init__(self, graph):
1332 """graph is instance of plotCanvas to be printed or previewed"""
1333 wx.Printout.__init__(self)
1336 def HasPage(self, page):
1342 def GetPageInfo(self):
1343 return (1, 1, 1, 1) # disable page numbers
1345 def OnPrintPage(self, page):
1346 dc = self.GetDC() # allows using floats for certain functions
1347 ## print "PPI Printer",self.GetPPIPrinter()
1348 ## print "PPI Screen", self.GetPPIScreen()
1349 ## print "DC GetSize", dc.GetSize()
1350 ## print "GetPageSizePixels", self.GetPageSizePixels()
1351 # Note PPIScreen does not give the correct number
1352 # Calulate everything for printer and then scale for preview
1353 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1354 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1355 dcSize= dc.GetSize() # DC size
1356 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1357 clientDcSize= self.graph.GetClientSize()
1359 # find what the margins are (mm)
1360 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1361 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1363 # calculate offset and scale for dc
1364 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1365 pixRight= margRightSize*PPIPrinter[0]/25.4
1366 pixTop= margTopSize*PPIPrinter[1]/25.4
1367 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1369 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1370 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1372 # ratio offset and scale to screen size if preview
1373 if self.IsPreview():
1374 ratioW= float(dcSize[0])/pageSize[0]
1375 ratioH= float(dcSize[1])/pageSize[1]
1381 # rescale plot to page or preview plot area
1382 self.graph._setSize(plotAreaW,plotAreaH)
1384 # Set offset and scale
1385 dc.SetDeviceOrigin(pixLeft,pixTop)
1387 # Thicken up pens and increase marker size for printing
1388 ratioW= float(plotAreaW)/clientDcSize[0]
1389 ratioH= float(plotAreaH)/clientDcSize[1]
1390 aveScale= (ratioW+ratioH)/2
1391 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1393 self.graph._printDraw(dc)
1394 # rescale back to original
1395 self.graph._setSize()
1396 self.graph._setPrinterScale(1)
1397 self.graph.Redraw() #to get point label scale and shift correct
1404 #---------------------------------------------------------------------------
1405 # if running standalone...
1407 # ...a sample implementation using the above
1410 def _draw1Objects():
1411 # 100 points sin function, plotted as green circles
1412 data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1413 data1.shape = (100, 2)
1414 data1[:,1] = _numpy.sin(data1[:,0])
1415 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1417 # 50 points cos function, plotted as red line
1418 data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1419 data1.shape = (50,2)
1420 data1[:,1] = _numpy.cos(data1[:,0])
1421 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1423 # A few more points...
1425 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1426 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1429 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1431 def _draw2Objects():
1432 # 100 points sin function, plotted as green dots
1433 data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1434 data1.shape = (100, 2)
1435 data1[:,1] = _numpy.sin(data1[:,0])
1436 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1438 # 50 points cos function, plotted as red dot-dash
1439 data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1440 data1.shape = (50,2)
1441 data1[:,1] = _numpy.cos(data1[:,0])
1442 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1444 # A few more points...
1446 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1447 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1448 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1451 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1453 def _draw3Objects():
1454 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1455 'cross', 'plus', 'circle']
1457 for i in range(len(markerList)):
1458 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1459 marker=markerList[i]))
1460 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1462 def _draw4Objects():
1464 data1 = _numpy.arange(5e5,1e6,10)
1465 data1.shape = (25000, 2)
1466 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1468 # A few more points...
1469 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1471 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1473 def _draw5Objects():
1474 # Empty graph with axis defined but no points/lines
1476 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1477 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1479 def _draw6Objects():
1481 points1=[(1,0), (1,10)]
1482 line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1483 points1g=[(2,0), (2,4)]
1484 line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1485 points1b=[(3,0), (3,6)]
1486 line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1488 points2=[(4,0), (4,12)]
1489 line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1490 points2g=[(5,0), (5,8)]
1491 line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1492 points2b=[(6,0), (6,4)]
1493 line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1495 return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1496 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1499 class TestFrame(wx.Frame):
1500 def __init__(self, parent, id, title):
1501 wx.Frame.__init__(self, parent, id, title,
1502 wx.DefaultPosition, (600, 400))
1504 # Now Create the menu bar and items
1505 self.mainmenu = wx.MenuBar()
1508 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1509 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1511 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1512 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1514 menu.Append(202, 'Print...', 'Print the current plot')
1515 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1517 menu.Append(203, 'Save Plot...', 'Save current plot')
1518 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1520 menu.Append(205, 'E&xit', 'Enough of this already!')
1521 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1522 self.mainmenu.Append(menu, '&File')
1525 menu.Append(206, 'Draw1', 'Draw plots1')
1526 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1527 menu.Append(207, 'Draw2', 'Draw plots2')
1528 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1529 menu.Append(208, 'Draw3', 'Draw plots3')
1530 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1531 menu.Append(209, 'Draw4', 'Draw plots4')
1532 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1533 menu.Append(210, 'Draw5', 'Draw plots5')
1534 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1535 menu.Append(260, 'Draw6', 'Draw plots6')
1536 self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
1539 menu.Append(211, '&Redraw', 'Redraw plots')
1540 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1541 menu.Append(212, '&Clear', 'Clear canvas')
1542 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1543 menu.Append(213, '&Scale', 'Scale canvas')
1544 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1545 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1546 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1547 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1548 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1549 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1550 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1551 menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1552 self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1554 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1555 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1556 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1557 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1558 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1559 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1561 self.mainmenu.Append(menu, '&Plot')
1564 menu.Append(300, '&About', 'About this thing...')
1565 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1566 self.mainmenu.Append(menu, '&Help')
1568 self.SetMenuBar(self.mainmenu)
1570 # A status bar to tell people what's happening
1571 self.CreateStatusBar(1)
1573 self.client = PlotCanvas(self)
1574 #define the function for drawing pointLabels
1575 self.client.SetPointLabelFunc(self.DrawPointLabel)
1576 # Create mouse event for showing cursor coords in status bar
1577 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1578 # Show closest point when enabled
1579 self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1583 def DrawPointLabel(self, dc, mDataDict):
1584 """This is the fuction that defines how the pointLabels are plotted
1585 dc - DC that will be passed
1586 mDataDict - Dictionary of data that you want to use for the pointLabel
1588 As an example I have decided I want a box at the curve point
1589 with some text information about the curve plotted below.
1590 Any wxDC method can be used.
1593 dc.SetPen(wx.Pen(wx.BLACK))
1594 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1596 sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1597 dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
1598 px,py = mDataDict["pointXY"]
1599 cNum = mDataDict["curveNum"]
1600 pntIn = mDataDict["pIndex"]
1601 legend = mDataDict["legend"]
1602 #make a string to display
1603 s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1604 dc.DrawText(s, sx , sy+1)
1607 def OnMouseLeftDown(self,event):
1608 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1609 self.SetStatusText(s)
1610 event.Skip() #allows plotCanvas OnMouseLeftDown to be called
1612 def OnMotion(self, event):
1613 #show closest point (when enbled)
1614 if self.client.GetEnablePointLabel() == True:
1615 #make up dict with info for the pointLabel
1616 #I've decided to mark the closest point on the closest curve
1617 dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1618 if dlst != []: #returns [] if none
1619 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1620 #make up dictionary to pass to my user function (see DrawPointLabel)
1621 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1622 "pointXY":pointXY, "scaledXY":scaledXY}
1623 #pass dict to update the pointLabel
1624 self.client.UpdatePointLabel(mDataDict)
1625 event.Skip() #go to next handler
1627 def OnFilePageSetup(self, event):
1628 self.client.PageSetup()
1630 def OnFilePrintPreview(self, event):
1631 self.client.PrintPreview()
1633 def OnFilePrint(self, event):
1634 self.client.Printout()
1636 def OnSaveFile(self, event):
1637 self.client.SaveFile()
1639 def OnFileExit(self, event):
1642 def OnPlotDraw1(self, event):
1643 self.resetDefaults()
1644 self.client.Draw(_draw1Objects())
1646 def OnPlotDraw2(self, event):
1647 self.resetDefaults()
1648 self.client.Draw(_draw2Objects())
1650 def OnPlotDraw3(self, event):
1651 self.resetDefaults()
1652 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1653 self.client.SetFontSizeAxis(20)
1654 self.client.SetFontSizeLegend(12)
1655 self.client.SetXSpec('min')
1656 self.client.SetYSpec('none')
1657 self.client.Draw(_draw3Objects())
1659 def OnPlotDraw4(self, event):
1660 self.resetDefaults()
1661 drawObj= _draw4Objects()
1662 self.client.Draw(drawObj)
1664 ## start = _time.clock()
1665 ## for x in range(10):
1666 ## self.client.Draw(drawObj)
1667 ## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1670 def OnPlotDraw5(self, event):
1671 # Empty plot with just axes
1672 self.resetDefaults()
1673 drawObj= _draw5Objects()
1674 # make the axis X= (0,5), Y=(0,10)
1675 # (default with None is X= (-1,1), Y= (-1,1))
1676 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1678 def OnPlotDraw6(self, event):
1680 self.resetDefaults()
1681 #self.client.SetEnableLegend(True) #turn on Legend
1682 #self.client.SetEnableGrid(True) #turn on Grid
1683 self.client.SetXSpec('none') #turns off x-axis scale
1684 self.client.SetYSpec('auto')
1685 self.client.Draw(_draw6Objects(), xAxis= (0,7))
1687 def OnPlotRedraw(self,event):
1688 self.client.Redraw()
1690 def OnPlotClear(self,event):
1693 def OnPlotScale(self, event):
1694 if self.client.last_draw != None:
1695 graphics, xAxis, yAxis= self.client.last_draw
1696 self.client.Draw(graphics,(1,3.05),(0,1))
1698 def OnEnableZoom(self, event):
1699 self.client.SetEnableZoom(event.IsChecked())
1701 def OnEnableGrid(self, event):
1702 self.client.SetEnableGrid(event.IsChecked())
1704 def OnEnableLegend(self, event):
1705 self.client.SetEnableLegend(event.IsChecked())
1707 def OnEnablePointLabel(self, event):
1708 self.client.SetEnablePointLabel(event.IsChecked())
1710 def OnScrUp(self, event):
1711 self.client.ScrollUp(1)
1713 def OnScrRt(self,event):
1714 self.client.ScrollRight(2)
1716 def OnReset(self,event):
1719 def OnHelpAbout(self, event):
1720 from wx.lib.dialogs import ScrolledMessageDialog
1721 about = ScrolledMessageDialog(self, __doc__, "About...")
1724 def resetDefaults(self):
1725 """Just to reset the fonts back to the PlotCanvas defaults"""
1726 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1727 self.client.SetFontSizeAxis(10)
1728 self.client.SetFontSizeLegend(7)
1729 self.client.SetXSpec('auto')
1730 self.client.SetYSpec('auto')
1735 class MyApp(wx.App):
1737 wx.InitAllImageHandlers()
1738 frame = TestFrame(None, -1, "PlotCanvas")
1740 self.SetTopWindow(frame)
1747 if __name__ == '__main__':