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
40 # Apr 23, 2010 Martin Dudok van Heel (http://www.olifantasia.com/gnuradio/contact_olifantasia.gif)
41 # - Added Persistence option (emulate after glow of an analog CRT display using IIR)
44 This is a simple light weight plotting module that can be used with
45 Boa or easily integrated into your own wxPython application. The
46 emphasis is on small size and fast plotting for large data sets. It
47 has a reasonable number of features to do line and scatter graphs
48 easily as well as simple bar graphs. It is not as sophisticated or
49 as powerful as SciPy Plt or Chaco. Both of these are great packages
50 but consume huge amounts of computer resources for simple plots.
51 They can be found at http://scipy.com
53 This file contains two parts; first the re-usable library stuff, then,
54 after a "if __name__=='__main__'" test, a simple frame and a few default
55 plots for examples and testing.
58 Written by K.Hinsen, R. Srinivasan;
59 Ported to wxPython Harm van der Heijden, feb 1999
61 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
63 -Zooming using mouse 'rubber band'
66 -Printing, preview, and page set up (margins)
67 -Axis and title labels
68 -Cursor xy axis values
69 -Doc strings and lots of comments
70 -Optimizations for large number of points
73 Did a lot of work here to speed markers up. Only a factor of 4
74 improvement though. Lines are much faster than markers, especially
75 filled markers. Stay away from circles and triangles unless you
76 only have a few thousand points.
78 Times for 25,000 points
85 triangle, triangle_down - 0.90
87 Thanks to Chris Barker for getting this version working on Linux.
89 Zooming controls with mouse (when enabled):
90 Left mouse drag - Zoom box.
91 Left mouse double click - reset zoom.
92 Right mouse click - zoom out centred on click location.
95 import string as _string
99 # Needs numpy or numarray
101 import numpy as _numpy
104 import numarray as _numpy #if numarray is used it is renamed numpy
107 This module requires the numpy or numarray module,
108 which could not be imported. It probably is not installed
109 (it's not part of the standard Python distribution). See the
110 Python site (http://www.python.org) for information on
111 downloading source or binaries."""
112 raise ImportError, "numpy or numarray not found. \n" + msg
117 # Plotting classes...
120 """Base Class for lines and markers
121 - All methods are private.
124 def __init__(self, points, attr):
125 self.points = _numpy.array(points)
126 self.currentScale= (1,1)
127 self.currentShift= (0,0)
128 self.scaled = self.points
130 self.attributes.update(self._attributes)
131 for name, value in attr.items():
132 if name not in self._attributes.keys():
133 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
134 self.attributes[name] = value
136 def boundingBox(self):
137 if len(self.points) == 0:
139 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
140 minXY= _numpy.array([-1,-1])
141 maxXY= _numpy.array([ 1, 1])
143 minXY= _numpy.minimum.reduce(self.points)
144 maxXY= _numpy.maximum.reduce(self.points)
147 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
148 if len(self.points) == 0:
151 if (scale is not self.currentScale) or (shift is not self.currentShift):
152 # update point scaling
153 self.scaled = scale*self.points+shift
154 self.currentScale= scale
155 self.currentShift= shift
156 # else unchanged use the current scaling
159 return self.attributes['legend']
161 def getClosestPoint(self, pntXY, pointScaled= True):
162 """Returns the index of closest point on the curve, pointXY, scaledXY, distance
164 if pointScaled == True based on screen coords
165 if pointScaled == False based on user coords
167 if pointScaled == True:
170 pxy = self.currentScale * _numpy.array(pntXY)+ self.currentShift
174 pxy = _numpy.array(pntXY)
175 #determine distance for each point
176 d= _numpy.sqrt(_numpy.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
177 pntIndex = _numpy.argmin(d)
179 return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
182 class PolyLine(PolyPoints):
183 """Class to define line type and style
184 - All methods except __init__ are private.
187 _attributes = {'colour': 'black',
192 def __init__(self, points, **attr):
193 """Creates PolyLine object
194 points - sequence (array, tuple or list) of (x,y) points making up line
195 **attr - key word attributes
197 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
198 'width'= 1, - Pen width
199 'style'= wx.SOLID, - wx.Pen style
200 'legend'= '' - Line Legend to display
202 PolyPoints.__init__(self, points, attr)
204 def draw(self, dc, printerScale, coord= None):
205 colour = self.attributes['colour']
206 width = self.attributes['width'] * printerScale
207 style= self.attributes['style']
208 pen = wx.Pen(wx.NamedColour(colour), width, style)
209 pen.SetCap(wx.CAP_BUTT)
212 dc.DrawLines(self.scaled)
214 dc.DrawLines(coord) # draw legend line
216 def getSymExtent(self, printerScale):
217 """Width and Height of Marker"""
218 h= self.attributes['width'] * printerScale
223 class PolyMarker(PolyPoints):
224 """Class to define marker type and style
225 - All methods except __init__ are private.
228 _attributes = {'colour': 'black',
232 'fillstyle': wx.SOLID,
236 def __init__(self, points, **attr):
237 """Creates PolyMarker object
238 points - sequence (array, tuple or list) of (x,y) points
239 **attr - key word attributes
241 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
242 'width'= 1, - Pen width
243 'size'= 2, - Marker size
244 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
245 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
246 'marker'= 'circle' - Marker shape
247 'legend'= '' - Marker Legend to display
259 PolyPoints.__init__(self, points, attr)
261 def draw(self, dc, printerScale, coord= None):
262 colour = self.attributes['colour']
263 width = self.attributes['width'] * printerScale
264 size = self.attributes['size'] * printerScale
265 fillcolour = self.attributes['fillcolour']
266 fillstyle = self.attributes['fillstyle']
267 marker = self.attributes['marker']
269 dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
271 dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
273 dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
275 self._drawmarkers(dc, self.scaled, marker, size)
277 self._drawmarkers(dc, coord, marker, size) # draw legend marker
279 def getSymExtent(self, printerScale):
280 """Width and Height of Marker"""
281 s= 5*self.attributes['size'] * printerScale
284 def _drawmarkers(self, dc, coords, marker,size=1):
285 f = eval('self._' +marker)
288 def _circle(self, dc, coords, size=1):
291 rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
292 rect[:,0:2]= coords-[fact,fact]
293 dc.DrawEllipseList(rect.astype(_numpy.int32))
295 def _dot(self, dc, coords, size=1):
296 dc.DrawPointList(coords)
298 def _square(self, dc, coords, size=1):
301 rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
302 rect[:,0:2]= coords-[fact,fact]
303 dc.DrawRectangleList(rect.astype(_numpy.int32))
305 def _triangle(self, dc, coords, size=1):
306 shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
307 poly= _numpy.repeat(coords,3)
308 poly.shape= (len(coords),3,2)
310 dc.DrawPolygonList(poly.astype(_numpy.int32))
312 def _triangle_down(self, dc, coords, size=1):
313 shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
314 poly= _numpy.repeat(coords,3)
315 poly.shape= (len(coords),3,2)
317 dc.DrawPolygonList(poly.astype(_numpy.int32))
319 def _cross(self, dc, coords, size=1):
321 for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
322 lines= _numpy.concatenate((coords,coords),axis=1)+f
323 dc.DrawLineList(lines.astype(_numpy.int32))
325 def _plus(self, dc, coords, size=1):
327 for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
328 lines= _numpy.concatenate((coords,coords),axis=1)+f
329 dc.DrawLineList(lines.astype(_numpy.int32))
332 """Container to hold PolyXXX objects and graph labels
333 - All methods except __init__ are private.
336 def __init__(self, objects, title='', xLabel='', yLabel= ''):
337 """Creates PlotGraphics object
338 objects - list of PolyXXX objects to make graph
339 title - title shown at top of graph
340 xLabel - label shown on x-axis
341 yLabel - label shown on y-axis
343 if type(objects) not in [list,tuple]:
344 raise TypeError, "objects argument should be list or tuple"
345 self.objects = objects
350 def boundingBox(self):
351 p1, p2 = self.objects[0].boundingBox()
352 for o in self.objects[1:]:
353 p1o, p2o = o.boundingBox()
354 p1 = _numpy.minimum(p1, p1o)
355 p2 = _numpy.maximum(p2, p2o)
358 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
359 for o in self.objects:
360 o.scaleAndShift(scale, shift)
362 def setPrinterScale(self, scale):
363 """Thickens up lines and markers only for printing"""
364 self.printerScale= scale
366 def setXLabel(self, xLabel= ''):
367 """Set the X axis label on the graph"""
370 def setYLabel(self, yLabel= ''):
371 """Set the Y axis label on the graph"""
374 def setTitle(self, title= ''):
375 """Set the title at the top of graph"""
379 """Get x axis label string"""
383 """Get y axis label string"""
386 def getTitle(self, title= ''):
387 """Get the title at the top of graph"""
391 for o in self.objects:
392 #t=_time.clock() # profile info
393 o.draw(dc, self.printerScale)
395 #print o, "time=", dt
397 def getSymExtent(self, printerScale):
398 """Get max width and height of lines and markers symbols for legend"""
399 symExt = self.objects[0].getSymExtent(printerScale)
400 for o in self.objects[1:]:
401 oSymExt = o.getSymExtent(printerScale)
402 symExt = _numpy.maximum(symExt, oSymExt)
405 def getLegendNames(self):
406 """Returns list of legend names"""
407 lst = [None]*len(self)
408 for i in range(len(self)):
409 lst[i]= self.objects[i].getLegend()
413 return len(self.objects)
415 def __getitem__(self, item):
416 return self.objects[item]
419 #-------------------------------------------------------------------------------
420 # Main window that you will want to import into your application.
422 class PlotCanvas(wx.Window):
423 """Subclass of a wx.Window to allow simple general plotting
424 of data with zoom, labels, and automatic axis scaling."""
426 def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
427 size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
429 self.use_persistence=False
433 """Constucts a window, which can be a child of a frame, dialog or
434 any other non-control window"""
436 wx.Window.__init__(self, parent, id, pos, size, style, name)
439 self.SetBackgroundColour("white")
441 # Create some mouse events for zooming
442 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
443 self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
444 self.Bind(wx.EVT_MOTION, self.OnMotion)
445 self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
446 self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
448 # set curser as cross-hairs
449 self.SetCursor(wx.CROSS_CURSOR)
451 # Things for printing
452 self.print_data = wx.PrintData()
453 self.print_data.SetPaperId(wx.PAPER_LETTER)
454 self.print_data.SetOrientation(wx.LANDSCAPE)
455 self.pageSetupData= wx.PageSetupDialogData()
456 self.pageSetupData.SetMarginBottomRight((25,25))
457 self.pageSetupData.SetMarginTopLeft((25,25))
458 self.pageSetupData.SetPrintData(self.print_data)
459 self.printerScale = 1
463 self._zoomInFactor = 0.5
464 self._zoomOutFactor = 2
465 self._zoomCorner1= _numpy.array([0.0, 0.0]) # left mouse down corner
466 self._zoomCorner2= _numpy.array([0.0, 0.0]) # left mouse up corner
467 self._zoomEnabled= False
468 self._hasDragged= False
471 self.last_draw = None
476 self._gridEnabled= False
477 self._legendEnabled= False
478 self._xUseScopeTicks= False
482 self._fontSizeAxis= 10
483 self._fontSizeTitle= 15
484 self._fontSizeLegend= 7
487 self._pointLabelEnabled= False
488 self.last_PointLabel= None
489 self._pointLabelFunc= None
490 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
492 self.Bind(wx.EVT_PAINT, self.OnPaint)
493 self.Bind(wx.EVT_SIZE, self.OnSize)
494 # OnSize called to make sure the buffer is initialized.
495 # This might result in OnSize getting called twice on some
496 # platforms at initialization, but little harm done.
497 self.OnSize(None) # sets the initial size based on client size
498 # UNCONDITIONAL, needed to create self._Buffer
501 def set_use_persistence(self, enable):
502 self.use_persistence = enable
504 def set_persist_alpha(self, persist_alpha):
505 self.alpha = persist_alpha
509 def SaveFile(self, fileName= ''):
510 """Saves the file to the type specified in the extension. If no file
511 name is specified a dialog box is provided. Returns True if sucessful,
514 .bmp Save a Windows bitmap file.
515 .xbm Save an X bitmap file.
516 .xpm Save an XPM bitmap file.
517 .png Save a Portable Network Graphics file.
518 .jpg Save a Joint Photographic Experts Group file.
520 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
521 dlg1 = wx.FileDialog(
523 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
524 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
525 wx.SAVE|wx.OVERWRITE_PROMPT
529 if dlg1.ShowModal() == wx.ID_OK:
530 fileName = dlg1.GetPath()
531 # Check for proper exension
532 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
533 dlg2 = wx.MessageDialog(self, 'File name extension\n'
535 'bmp, xbm, xpm, png, or jpg',
536 'File Name Error', wx.OK | wx.ICON_ERROR)
542 break # now save file
543 else: # exit without saving
548 # File name has required extension
549 fType = _string.lower(fileName[-3:])
551 tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
553 tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
555 tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
557 tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
559 tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
561 res= self._Buffer.SaveFile(fileName, tp)
565 """Brings up the page setup dialog"""
566 data = self.pageSetupData
567 data.SetPrintData(self.print_data)
568 dlg = wx.PageSetupDialog(self.parent, data)
570 if dlg.ShowModal() == wx.ID_OK:
571 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
572 # updates page parameters from dialog
573 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
574 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
575 self.pageSetupData.SetPrintData(data.GetPrintData())
576 self.print_data=data.GetPrintData() # updates print_data
580 def Printout(self, paper=None):
581 """Print current plot."""
583 self.print_data.SetPaperId(paper)
584 pdd = wx.PrintDialogData()
585 pdd.SetPrintData(self.print_data)
586 printer = wx.Printer(pdd)
587 out = PlotPrintout(self)
588 print_ok = printer.Print(self.parent, out)
590 self.print_data = printer.GetPrintDialogData().GetPrintData()
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
605 while not isinstance(frameInst, wx.Frame):
606 frameInst= frameInst.GetParent()
607 frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
609 frame.SetPosition(self.GetPosition())
610 frame.SetSize((600,550))
611 frame.Centre(wx.BOTH)
614 def SetFontSizeAxis(self, point= 10):
615 """Set the tick and axis label font size (default is 10 point)"""
616 self._fontSizeAxis= point
618 def GetFontSizeAxis(self):
619 """Get current tick and axis label font size in points"""
620 return self._fontSizeAxis
622 def SetFontSizeTitle(self, point= 15):
623 """Set Title font size (default is 15 point)"""
624 self._fontSizeTitle= point
626 def GetFontSizeTitle(self):
627 """Get current Title font size in points"""
628 return self._fontSizeTitle
630 def SetFontSizeLegend(self, point= 7):
631 """Set Legend font size (default is 7 point)"""
632 self._fontSizeLegend= point
634 def GetFontSizeLegend(self):
635 """Get current Legend font size in points"""
636 return self._fontSizeLegend
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
644 def GetEnableZoom(self):
645 """True if zooming enabled."""
646 return self._zoomEnabled
648 def SetEnableGrid(self, value):
649 """Set True to enable grid."""
650 if value not in [True,False]:
651 raise TypeError, "Value should be True or False"
652 self._gridEnabled= value
655 def GetEnableGrid(self):
656 """True if grid enabled."""
657 return self._gridEnabled
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
666 def GetEnableLegend(self):
667 """True if Legend enabled."""
668 return self._legendEnabled
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
678 def GetEnablePointLabel(self):
679 """True if pointLabel enabled."""
680 return self._pointLabelEnabled
682 def SetPointLabelFunc(self, func):
683 """Sets the function with custom code for pointLabel drawing
684 ******** more info needed ***************
686 self._pointLabelFunc= func
688 def GetPointLabelFunc(self):
689 """Returns pointLabel Drawing Function"""
690 return self._pointLabelFunc
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])
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)
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)
714 def GetXY(self,event):
715 """Takes a mouse event and returns the XY user axis values."""
716 x,y= self.PositionScreenToUser(event.GetPosition())
719 def PositionUserToScreen(self, pntXY):
720 """Converts User position to Screen Coordinates"""
721 userPos= _numpy.array(pntXY)
722 x,y= userPos * self._pointScale + self._pointShift
725 def PositionScreenToUser(self, pntXY):
726 """Converts Screen position to User Coordinates"""
727 screenPos= _numpy.array(pntXY)
728 x,y= (screenPos-self._pointShift)/self._pointScale
731 def SetXSpec(self, type= 'auto'):
732 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
734 'none' - shows no axis or tick mark values
735 'min' - shows min bounding box values
736 'auto' - rounds axis range to sensible values
740 def SetYSpec(self, type= 'auto'):
741 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
743 'none' - shows no axis or tick mark values
744 'min' - shows min bounding box values
745 'auto' - rounds axis range to sensible values
750 """Returns current XSpec for axis"""
754 """Returns current YSpec for axis"""
757 def GetXMaxRange(self):
758 """Returns (minX, maxX) x-axis range for displayed graph"""
759 graphics= self.last_draw[0]
760 p1, p2 = graphics.boundingBox() # min, max points of graphics
761 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
764 def GetYMaxRange(self):
765 """Returns (minY, maxY) y-axis range for displayed graph"""
766 graphics= self.last_draw[0]
767 p1, p2 = graphics.boundingBox() # min, max points of graphics
768 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
771 def GetXCurrentRange(self):
772 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
773 return self.last_draw[1]
775 def GetYCurrentRange(self):
776 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
777 return self.last_draw[2]
779 def SetXUseScopeTicks(self, v=False):
780 """Always 10 divisions, no labels"""
781 self._xUseScopeTicks = v
783 def GetXUseScopeTicks(self):
784 return self._xUseScopeTicks
786 def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, step=None):
787 """Draw objects in graphics with specified x and y axis.
788 graphics- instance of PlotGraphics with list of PolyXXX objects
789 xAxis - tuple with (min, max) axis range to view
790 yAxis - same as xAxis
791 dc - drawing context - doesn't have to be specified.
792 If it's not, the offscreen buffer is used
794 # check Axis is either tuple or none
795 if type(xAxis) not in [type(None),tuple]:
796 raise TypeError, "xAxis should be None or (minX,maxX)"
797 if type(yAxis) not in [type(None),tuple]:
798 raise TypeError, "yAxis should be None or (minY,maxY)"
800 # check case for axis = (a,b) where a==b caused by improper zooms
802 if xAxis[0] == xAxis[1]:
805 if yAxis[0] == yAxis[1]:
809 # sets new dc and clears it
810 if self.use_persistence:
812 dc.SelectObject(self._Buffer)
815 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
823 # set font size for every thing but title and legend
824 dc.SetFont(self._getFont(self._fontSizeAxis))
826 # sizes axis to axis type, create lower left and upper right corners of plot
827 if xAxis == None or yAxis == None:
828 # One or both axis not specified in Draw
829 p1, p2 = graphics.boundingBox() # min, max points of graphics
831 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
833 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
834 # Adjust bounding box for axis spec
835 p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
836 p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
838 # Both axis specified in Draw
839 p1= _numpy.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
840 p2= _numpy.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
842 self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
845 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
846 #dc.SetPen(wx.Pen(wx.BLACK))
847 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) #wx.SOLID wx.TRANSPARENT ) )
848 #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT
849 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
850 #dc.SetBrush(wx.Brush( wx.WHITE, wx.SOLID ) )
851 #dc.SetLogicalFunction(wx.COPY)
853 # Get ticks and textExtents for axis if required
854 if self._xSpec is not 'none':
855 if self._xUseScopeTicks:
856 xticks = self._scope_ticks(xAxis[0], xAxis[1])
858 xticks = self._ticks(xAxis[0], xAxis[1])
859 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
862 xTextExtent= (0,0) # No text for ticks
863 if self._ySpec is not 'none':
864 yticks = self._ticks(yAxis[0], yAxis[1], step)
865 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
866 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
867 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
868 max(yTextExtentBottom[1],yTextExtentTop[1]))
871 yTextExtent= (0,0) # No text for ticks
873 # TextExtents for Title and Axis Labels
874 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
876 # TextExtents for Legend
877 legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
879 # room around graph area
880 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
881 lhsW= yTextExtent[0]+ yLabelWH[1]
882 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
883 topH= yTextExtent[1]/2. + titleWH[1]
884 textSize_scale= _numpy.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
885 textSize_shift= _numpy.array([lhsW, bottomH]) # shift plot area by this amount
887 # drawing title and labels text
888 dc.SetFont(self._getFont(self._fontSizeTitle))
889 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
890 self.plotbox_origin[1]- self.plotbox_size[1])
891 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
892 dc.SetFont(self._getFont(self._fontSizeAxis))
893 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
894 self.plotbox_origin[1]- xLabelWH[1])
895 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
896 yLabelPos= (self.plotbox_origin[0],
897 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
898 if graphics.getYLabel(): # bug fix for Linux
899 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
901 # drawing legend makers and text
902 if self._legendEnabled:
903 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
905 # allow for scaling and shifting plotted points
906 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _numpy.array((1,-1))
907 shift = -p1*scale + self.plotbox_origin + textSize_shift * _numpy.array((1,-1))
908 self._pointScale= scale # make available for mouse events
909 self._pointShift= shift
911 #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT
912 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
913 #dc.SetLogicalFunction(wx.COPY)
915 graphics.scaleAndShift(scale, shift)
916 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
918 # set clipping area so drawing does not occur outside axis box
919 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
920 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
921 # Draw the lines and markers
922 #start = _time.clock()
925 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
926 # remove the clipping region
927 dc.DestroyClippingRegion()
931 if self.use_persistence:
933 self._Buffer.CopyToBuffer(self._Bufferarray) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
935 alpha_int=int(float(self.alpha*256))
937 _numpy.add(self._Bufferarray,0,self._Buffer3array)
938 _numpy.multiply(self._Buffer3array,alpha_int,self._Buffer3array)
939 _numpy.multiply(self._Buffer2array,(256-alpha_int),self._Buffer2array)
940 _numpy.add(self._Buffer3array,self._Buffer2array,self._Buffer2array)
941 _numpy.right_shift(self._Buffer2array,8,self._Buffer2array)
943 self._Buffer2array=(self._Bufferarray.astype(_numpy.uint32) *alpha_int + self._Buffer2array*(256-alpha_int)).__rshift__(8)
945 self._Buffer2array *=(256-alpha_int)
946 self._Buffer2array +=self._Bufferarray.astype(_numpy.uint32)*alpha_int
947 self._Buffer2array /=256
949 ##copy back to image buffer
950 self._Buffer2.CopyFromBuffer(self._Buffer2array.astype(_numpy.uint8)) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
953 #self.decim_counter=self.decim_counter+1
954 if True: #self.decim_counter>self.decimation:
955 #self.decim_counter=0
956 dc2 = wx.ClientDC( self )
958 dc2.DrawBitmap(self._Buffer2, 0, 0, False)
959 #dc2.DrawBitmap(self._Buffer, 0, 0, False)
962 def Redraw(self, dc= None):
963 """Redraw the existing plot."""
964 if self.last_draw is not None:
965 graphics, xAxis, yAxis= self.last_draw
966 self.Draw(graphics,xAxis,yAxis,dc)
969 """Erase the window."""
970 self.last_PointLabel = None #reset pointLabel
971 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
973 self.last_draw = None
975 def Zoom(self, Center, Ratio):
977 Centers on the X,Y coords given in Center
978 Zooms by the Ratio = (Xratio, Yratio) given
980 self.last_PointLabel = None #reset maker
982 if self.last_draw != None:
983 (graphics, xAxis, yAxis) = self.last_draw
984 w = (xAxis[1] - xAxis[0]) * Ratio[0]
985 h = (yAxis[1] - yAxis[0]) * Ratio[1]
986 xAxis = ( x - w/2, x + w/2 )
987 yAxis = ( y - h/2, y + h/2 )
988 self.Draw(graphics, xAxis, yAxis)
990 def GetClosestPoints(self, pntXY, pointScaled= True):
992 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
994 Returns [] if no curves are being plotted.
997 if pointScaled == True based on screen coords
998 if pointScaled == False based on user coords
1000 if self.last_draw == None:
1003 graphics, xAxis, yAxis= self.last_draw
1005 for curveNum,obj in enumerate(graphics):
1006 #check there are points in the curve
1007 if len(obj.points) == 0:
1008 continue #go to next obj
1009 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1010 cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
1014 def GetClosetPoint(self, pntXY, pointScaled= True):
1015 """Returns list with
1016 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1017 list for only the closest curve.
1018 Returns [] if no curves are being plotted.
1021 if pointScaled == True based on screen coords
1022 if pointScaled == False based on user coords
1024 #closest points on screen based on screen scaling (pointScaled= True)
1025 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
1026 closestPts= self.GetClosestPoints(pntXY, pointScaled)
1027 if closestPts == []:
1028 return [] #no graph present
1029 #find one with least distance
1030 dists = [c[-1] for c in closestPts]
1031 mdist = min(dists) #Min dist
1032 i = dists.index(mdist) #index for min dist
1033 return closestPts[i] #this is the closest point on closest curve
1035 def UpdatePointLabel(self, mDataDict):
1036 """Updates the pointLabel point on screen with data contained in
1039 mDataDict will be passed to your function set by
1040 SetPointLabelFunc. It can contain anything you
1041 want to display on the screen at the scaledXY point
1044 This function can be called from parent window with onClick,
1045 onMotion events etc.
1047 if self.last_PointLabel != None:
1049 if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
1051 self._drawPointLabel(self.last_PointLabel) #erase old
1052 self._drawPointLabel(mDataDict) #plot new
1054 #just plot new with no erase
1055 self._drawPointLabel(mDataDict) #plot new
1056 #save for next erase
1057 self.last_PointLabel = mDataDict
1059 # event handlers **********************************
1060 def OnMotion(self, event):
1061 if self._zoomEnabled and event.LeftIsDown():
1062 if self._hasDragged:
1063 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1065 self._hasDragged= True
1066 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
1067 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
1069 def OnMouseLeftDown(self,event):
1070 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
1072 def OnMouseLeftUp(self, event):
1073 if self._zoomEnabled:
1074 if self._hasDragged == True:
1075 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1076 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
1077 self._hasDragged = False # reset flag
1078 minX, minY= _numpy.minimum( self._zoomCorner1, self._zoomCorner2)
1079 maxX, maxY= _numpy.maximum( self._zoomCorner1, self._zoomCorner2)
1080 self.last_PointLabel = None #reset pointLabel
1081 if self.last_draw != None:
1082 self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
1083 #else: # A box has not been drawn, zoom in on a point
1084 ## this interfered with the double click, so I've disables it.
1085 # X,Y = self.GetXY(event)
1086 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1088 def OnMouseDoubleClick(self,event):
1089 if self._zoomEnabled:
1092 def OnMouseRightDown(self,event):
1093 if self._zoomEnabled:
1094 X,Y = self.GetXY(event)
1095 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1097 def OnPaint(self, event):
1098 # All that is needed here is to draw the buffer to screen
1099 if self.last_PointLabel != None:
1100 self._drawPointLabel(self.last_PointLabel) #erase old
1101 self.last_PointLabel = None
1103 #paint current buffer to screen
1104 dc = wx.BufferedPaintDC(self, self._Buffer)
1106 def OnSize(self,event):
1107 # The Buffer init is done here, to make sure the buffer is always
1108 # the same size as the Window
1109 Size = self.GetClientSize()
1111 # Make new offscreen bitmap: this bitmap will always have the
1112 # current drawing in it, so it can be used to save the image to
1113 # a file, or whatever.
1114 self._Buffer = wx.EmptyBitmap(Size[0],Size[1],24)
1117 if True: #self.use_persistence:
1118 #self._Bufferarray = _numpy.zeros((Size[0], Size[1],3), dtype=_numpy.uint8)
1119 self._Bufferarray = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint8)
1121 # Make new second offscreen bitmap: this bitmap will always have the
1122 # last drawing in it, so it can be used to do display time dependent processing
1123 # like averaging (IIR) or show differences between updates
1124 self._Buffer2 = wx.EmptyBitmap(Size[0],Size[1],24)
1125 # now the extra buffers for the IIR processing
1126 # note the different datatype uint32
1127 self._Buffer2array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
1128 self._Buffer3array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
1129 # optional you can set the ufunct buffer size to improve speed
1130 #_numpy.setbufsize(16*((Size[0]* Size[1]*3)/16 +1))
1133 self.last_PointLabel = None #reset pointLabel
1135 if self.last_draw is None:
1138 graphics, xSpec, ySpec = self.last_draw
1139 self.Draw(graphics,xSpec,ySpec)
1141 def OnLeave(self, event):
1142 """Used to erase pointLabel when mouse outside window"""
1143 if self.last_PointLabel != None:
1144 self._drawPointLabel(self.last_PointLabel) #erase old
1145 self.last_PointLabel = None
1148 # Private Methods **************************************************
1149 def _setSize(self, width=None, height=None):
1150 """DC width and height."""
1152 (self.width,self.height) = self.GetClientSize()
1154 self.width, self.height= width,height
1155 self.plotbox_size = 0.97*_numpy.array([self.width, self.height])
1156 xo = 0.5*(self.width-self.plotbox_size[0])
1157 yo = self.height-0.5*(self.height-self.plotbox_size[1])
1158 self.plotbox_origin = _numpy.array([xo, yo])
1160 def _setPrinterScale(self, scale):
1161 """Used to thicken lines and increase marker size for print out."""
1162 # line thickness on printer is very thin at 600 dot/in. Markers small
1163 self.printerScale= scale
1165 def _printDraw(self, printDC):
1166 """Used for printing."""
1167 if self.last_draw != None:
1168 graphics, xSpec, ySpec= self.last_draw
1169 self.Draw(graphics,xSpec,ySpec,printDC)
1171 def _drawPointLabel(self, mDataDict):
1172 """Draws and erases pointLabels"""
1173 width = self._Buffer.GetWidth()
1174 height = self._Buffer.GetHeight()
1175 tmp_Buffer = wx.EmptyBitmap(width,height)
1177 dcs.SelectObject(tmp_Buffer)
1180 self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
1183 dc = wx.ClientDC( self )
1184 #this will erase if called twice
1185 dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
1188 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
1189 """Draws legend symbols and text"""
1190 # top right hand corner of graph box is ref corner
1191 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1192 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
1193 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1194 dc.SetFont(self._getFont(self._fontSizeLegend))
1195 for i in range(len(graphics)):
1198 if isinstance(o,PolyMarker):
1199 # draw marker with legend
1200 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
1201 o.draw(dc, self.printerScale, coord= _numpy.array([pnt]))
1202 elif isinstance(o,PolyLine):
1203 # draw line with legend
1204 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1205 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
1206 o.draw(dc, self.printerScale, coord= _numpy.array([pnt1,pnt2]))
1208 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1210 pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
1211 dc.DrawText(o.getLegend(),pnt[0],pnt[1])
1212 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1214 def _titleLablesWH(self, dc, graphics):
1215 """Draws Title and labels and returns width and height for each"""
1216 # TextExtents for Title and Axis Labels
1217 dc.SetFont(self._getFont(self._fontSizeTitle))
1218 title= graphics.getTitle()
1219 titleWH= dc.GetTextExtent(title)
1220 dc.SetFont(self._getFont(self._fontSizeAxis))
1221 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1222 xLabelWH= dc.GetTextExtent(xLabel)
1223 yLabelWH= dc.GetTextExtent(yLabel)
1224 return titleWH, xLabelWH, yLabelWH
1226 def _legendWH(self, dc, graphics):
1227 """Returns the size in screen units for legend box"""
1228 if self._legendEnabled != True:
1229 legendBoxWH= symExt= txtExt= (0,0)
1231 # find max symbol size
1232 symExt= graphics.getSymExtent(self.printerScale)
1233 # find max legend text extent
1234 dc.SetFont(self._getFont(self._fontSizeLegend))
1235 txtList= graphics.getLegendNames()
1236 txtExt= dc.GetTextExtent(txtList[0])
1237 for txt in graphics.getLegendNames()[1:]:
1238 txtExt= _numpy.maximum(txtExt,dc.GetTextExtent(txt))
1239 maxW= symExt[0]+txtExt[0]
1240 maxH= max(symExt[1],txtExt[1])
1241 # padding .1 for lhs of legend box and space between lines
1243 maxH= maxH* 1.1 * len(txtList)
1244 dc.SetFont(self._getFont(self._fontSizeAxis))
1245 legendBoxWH= (maxW,maxH)
1246 return (legendBoxWH, symExt, txtExt)
1248 def _drawRubberBand(self, corner1, corner2):
1249 """Draws/erases rect box from corner1 to corner2"""
1250 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1252 dc = wx.ClientDC( self )
1254 dc.SetPen(wx.Pen(wx.BLACK))
1255 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1256 dc.SetLogicalFunction(wx.INVERT)
1257 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
1258 dc.SetLogicalFunction(wx.COPY)
1261 def _getFont(self,size):
1262 """Take font size, adjusts if printing and returns wx.Font"""
1263 s = size*self.printerScale
1265 # Linux speed up to get font from cache rather than X font server
1266 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1267 font = self._fontCache.get (key, None)
1269 return font # yeah! cache hit
1271 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1272 self._fontCache[key] = font
1276 def _point2ClientCoord(self, corner1, corner2):
1277 """Converts user point coords to client screen int coords x,y,width,height"""
1278 c1= _numpy.array(corner1)
1279 c2= _numpy.array(corner2)
1280 # convert to screen coords
1281 pt1= c1*self._pointScale+self._pointShift
1282 pt2= c2*self._pointScale+self._pointShift
1283 # make height and width positive
1284 pul= _numpy.minimum(pt1,pt2) # Upper left corner
1285 plr= _numpy.maximum(pt1,pt2) # Lower right corner
1286 rectWidth, rectHeight= plr-pul
1288 return ptx, pty, rectWidth, rectHeight
1290 def _axisInterval(self, spec, lower, upper):
1291 """Returns sensible axis range for given spec"""
1292 if spec == 'none' or spec == 'min':
1294 return lower-0.5, upper+0.5
1297 elif spec == 'auto':
1300 if abs(range) < 1e-36:
1301 return lower-0.5, upper+0.5
1302 log = _numpy.log10(range)
1303 power = _numpy.floor(log)
1304 fraction = log-power
1305 if fraction <= 0.05:
1308 lower = lower - lower % grid
1311 upper = upper - mod + grid
1313 elif type(spec) == type(()):
1320 raise ValueError, str(spec) + ': illegal axis specification'
1322 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1324 penWidth= self.printerScale # increases thickness for printing only
1325 dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
1327 # set length of tick marks--long ones make grid
1328 if self._gridEnabled:
1329 x,y,width,height= self._point2ClientCoord(p1,p2)
1330 yTickLength= width/2.0 +1
1331 xTickLength= height/2.0 +1
1333 yTickLength= 3 * self.printerScale # lengthens lines for printing
1334 xTickLength= 3 * self.printerScale
1336 if self._xSpec is not 'none':
1337 lower, upper = p1[0],p2[0]
1339 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
1340 a1 = scale*_numpy.array([lower, y])+shift
1341 a2 = scale*_numpy.array([upper, y])+shift
1342 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1343 for x, label in xticks:
1344 pt = scale*_numpy.array([x, y])+shift
1345 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1347 dc.DrawText(label,pt[0],pt[1])
1348 text = 0 # axis values not drawn on top side
1350 if self._ySpec is not 'none':
1351 lower, upper = p1[1],p2[1]
1353 h = dc.GetCharHeight()
1354 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1355 a1 = scale*_numpy.array([x, lower])+shift
1356 a2 = scale*_numpy.array([x, upper])+shift
1357 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1358 for y, label in yticks:
1359 pt = scale*_numpy.array([x, y])+shift
1360 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1362 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1364 text = 0 # axis values not drawn on right side
1366 def _ticks(self, lower, upper, step=None):
1367 ideal = (upper-lower)/7.
1368 log = _numpy.log10(ideal)
1369 power = _numpy.floor(log)
1370 fraction = log-power
1373 for f, lf in self._multiples:
1374 e = _numpy.fabs(fraction-lf)
1378 grid = factor * 10.**power
1379 if power > 4 or power < -4:
1382 digits = max(1, int(power))
1383 format = '%' + `digits`+'.0f'
1385 digits = -int(power)
1386 format = '%'+`digits+2`+'.'+`digits`+'f'
1387 #force grid when step is not None
1388 if step is not None: grid = step
1390 t = -grid*_numpy.floor(-lower/grid)
1392 if t == -0: t = 0 #remove neg zero condition
1393 ticks.append( (t, format % (t,)) )
1397 def _scope_ticks (self, lower, upper):
1398 '''Always 10 divisions, no labels'''
1399 grid = (upper - lower) / 10.0
1403 ticks.append( (t, ""))
1407 _multiples = [(2., _numpy.log10(2.)), (5., _numpy.log10(5.))]
1410 #-------------------------------------------------------------------------------
1411 # Used to layout the printer page
1413 class PlotPrintout(wx.Printout):
1414 """Controls how the plot is made in printing and previewing"""
1415 # Do not change method names in this class,
1416 # we have to override wx.Printout methods here!
1417 def __init__(self, graph):
1418 """graph is instance of plotCanvas to be printed or previewed"""
1419 wx.Printout.__init__(self)
1422 def HasPage(self, page):
1428 def GetPageInfo(self):
1429 return (1, 1, 1, 1) # disable page numbers
1431 def OnPrintPage(self, page):
1432 dc = self.GetDC() # allows using floats for certain functions
1433 ## print "PPI Printer",self.GetPPIPrinter()
1434 ## print "PPI Screen", self.GetPPIScreen()
1435 ## print "DC GetSize", dc.GetSize()
1436 ## print "GetPageSizePixels", self.GetPageSizePixels()
1437 # Note PPIScreen does not give the correct number
1438 # Calulate everything for printer and then scale for preview
1439 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1440 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1441 dcSize= dc.GetSize() # DC size
1442 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1443 clientDcSize= self.graph.GetClientSize()
1445 # find what the margins are (mm)
1446 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1447 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1449 # calculate offset and scale for dc
1450 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1451 pixRight= margRightSize*PPIPrinter[0]/25.4
1452 pixTop= margTopSize*PPIPrinter[1]/25.4
1453 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1455 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1456 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1458 # ratio offset and scale to screen size if preview
1459 if self.IsPreview():
1460 ratioW= float(dcSize[0])/pageSize[0]
1461 ratioH= float(dcSize[1])/pageSize[1]
1467 # rescale plot to page or preview plot area
1468 self.graph._setSize(plotAreaW,plotAreaH)
1470 # Set offset and scale
1471 dc.SetDeviceOrigin(pixLeft,pixTop)
1473 # Thicken up pens and increase marker size for printing
1474 ratioW= float(plotAreaW)/clientDcSize[0]
1475 ratioH= float(plotAreaH)/clientDcSize[1]
1476 aveScale= (ratioW+ratioH)/2
1477 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1479 self.graph._printDraw(dc)
1480 # rescale back to original
1481 self.graph._setSize()
1482 self.graph._setPrinterScale(1)
1483 self.graph.Redraw() #to get point label scale and shift correct
1490 #---------------------------------------------------------------------------
1491 # if running standalone...
1493 # ...a sample implementation using the above
1496 def _draw1Objects():
1497 # 100 points sin function, plotted as green circles
1498 data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1499 data1.shape = (100, 2)
1500 data1[:,1] = _numpy.sin(data1[:,0])
1501 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1503 # 50 points cos function, plotted as red line
1504 data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1505 data1.shape = (50,2)
1506 data1[:,1] = _numpy.cos(data1[:,0])
1507 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1509 # A few more points...
1511 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1512 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1515 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1517 def _draw2Objects():
1518 # 100 points sin function, plotted as green dots
1519 data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
1520 data1.shape = (100, 2)
1521 data1[:,1] = _numpy.sin(data1[:,0])
1522 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1524 # 50 points cos function, plotted as red dot-dash
1525 data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
1526 data1.shape = (50,2)
1527 data1[:,1] = _numpy.cos(data1[:,0])
1528 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1530 # A few more points...
1532 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1533 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1534 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1537 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1539 def _draw3Objects():
1540 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1541 'cross', 'plus', 'circle']
1543 for i in range(len(markerList)):
1544 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1545 marker=markerList[i]))
1546 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1548 def _draw4Objects():
1550 data1 = _numpy.arange(5e5,1e6,10)
1551 data1.shape = (25000, 2)
1552 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1554 # A few more points...
1555 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1557 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1559 def _draw5Objects():
1560 # Empty graph with axis defined but no points/lines
1562 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1563 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1565 def _draw6Objects():
1567 points1=[(1,0), (1,10)]
1568 line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1569 points1g=[(2,0), (2,4)]
1570 line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1571 points1b=[(3,0), (3,6)]
1572 line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1574 points2=[(4,0), (4,12)]
1575 line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1576 points2g=[(5,0), (5,8)]
1577 line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1578 points2b=[(6,0), (6,4)]
1579 line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1581 return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1582 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1585 class TestFrame(wx.Frame):
1586 def __init__(self, parent, id, title):
1587 wx.Frame.__init__(self, parent, id, title,
1588 wx.DefaultPosition, (600, 400))
1590 # Now Create the menu bar and items
1591 self.mainmenu = wx.MenuBar()
1594 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1595 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1597 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1598 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1600 menu.Append(202, 'Print...', 'Print the current plot')
1601 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1603 menu.Append(203, 'Save Plot...', 'Save current plot')
1604 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1606 menu.Append(205, 'E&xit', 'Enough of this already!')
1607 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1608 self.mainmenu.Append(menu, '&File')
1611 menu.Append(206, 'Draw1', 'Draw plots1')
1612 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1613 menu.Append(207, 'Draw2', 'Draw plots2')
1614 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1615 menu.Append(208, 'Draw3', 'Draw plots3')
1616 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1617 menu.Append(209, 'Draw4', 'Draw plots4')
1618 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1619 menu.Append(210, 'Draw5', 'Draw plots5')
1620 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1621 menu.Append(260, 'Draw6', 'Draw plots6')
1622 self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
1625 menu.Append(211, '&Redraw', 'Redraw plots')
1626 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1627 menu.Append(212, '&Clear', 'Clear canvas')
1628 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1629 menu.Append(213, '&Scale', 'Scale canvas')
1630 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1631 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1632 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1633 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1634 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1635 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1636 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1637 menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1638 self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1640 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1641 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1642 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1643 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1644 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1645 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1647 self.mainmenu.Append(menu, '&Plot')
1650 menu.Append(300, '&About', 'About this thing...')
1651 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1652 self.mainmenu.Append(menu, '&Help')
1654 self.SetMenuBar(self.mainmenu)
1656 # A status bar to tell people what's happening
1657 self.CreateStatusBar(1)
1659 self.client = PlotCanvas(self)
1660 #define the function for drawing pointLabels
1661 self.client.SetPointLabelFunc(self.DrawPointLabel)
1662 # Create mouse event for showing cursor coords in status bar
1663 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1664 # Show closest point when enabled
1665 self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1669 def DrawPointLabel(self, dc, mDataDict):
1670 """This is the fuction that defines how the pointLabels are plotted
1671 dc - DC that will be passed
1672 mDataDict - Dictionary of data that you want to use for the pointLabel
1674 As an example I have decided I want a box at the curve point
1675 with some text information about the curve plotted below.
1676 Any wxDC method can be used.
1679 dc.SetPen(wx.Pen(wx.BLACK))
1680 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1682 sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1683 dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
1684 px,py = mDataDict["pointXY"]
1685 cNum = mDataDict["curveNum"]
1686 pntIn = mDataDict["pIndex"]
1687 legend = mDataDict["legend"]
1688 #make a string to display
1689 s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1690 dc.DrawText(s, sx , sy+1)
1693 def OnMouseLeftDown(self,event):
1694 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1695 self.SetStatusText(s)
1696 event.Skip() #allows plotCanvas OnMouseLeftDown to be called
1698 def OnMotion(self, event):
1699 #show closest point (when enbled)
1700 if self.client.GetEnablePointLabel() == True:
1701 #make up dict with info for the pointLabel
1702 #I've decided to mark the closest point on the closest curve
1703 dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1704 if dlst != []: #returns [] if none
1705 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1706 #make up dictionary to pass to my user function (see DrawPointLabel)
1707 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1708 "pointXY":pointXY, "scaledXY":scaledXY}
1709 #pass dict to update the pointLabel
1710 self.client.UpdatePointLabel(mDataDict)
1711 event.Skip() #go to next handler
1713 def OnFilePageSetup(self, event):
1714 self.client.PageSetup()
1716 def OnFilePrintPreview(self, event):
1717 self.client.PrintPreview()
1719 def OnFilePrint(self, event):
1720 self.client.Printout()
1722 def OnSaveFile(self, event):
1723 self.client.SaveFile()
1725 def OnFileExit(self, event):
1728 def OnPlotDraw1(self, event):
1729 self.resetDefaults()
1730 self.client.Draw(_draw1Objects())
1732 def OnPlotDraw2(self, event):
1733 self.resetDefaults()
1734 self.client.Draw(_draw2Objects())
1736 def OnPlotDraw3(self, event):
1737 self.resetDefaults()
1738 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1739 self.client.SetFontSizeAxis(20)
1740 self.client.SetFontSizeLegend(12)
1741 self.client.SetXSpec('min')
1742 self.client.SetYSpec('none')
1743 self.client.Draw(_draw3Objects())
1745 def OnPlotDraw4(self, event):
1746 self.resetDefaults()
1747 drawObj= _draw4Objects()
1748 self.client.Draw(drawObj)
1750 ## start = _time.clock()
1751 ## for x in range(10):
1752 ## self.client.Draw(drawObj)
1753 ## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1756 def OnPlotDraw5(self, event):
1757 # Empty plot with just axes
1758 self.resetDefaults()
1759 drawObj= _draw5Objects()
1760 # make the axis X= (0,5), Y=(0,10)
1761 # (default with None is X= (-1,1), Y= (-1,1))
1762 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1764 def OnPlotDraw6(self, event):
1766 self.resetDefaults()
1767 #self.client.SetEnableLegend(True) #turn on Legend
1768 #self.client.SetEnableGrid(True) #turn on Grid
1769 self.client.SetXSpec('none') #turns off x-axis scale
1770 self.client.SetYSpec('auto')
1771 self.client.Draw(_draw6Objects(), xAxis= (0,7))
1773 def OnPlotRedraw(self,event):
1774 self.client.Redraw()
1776 def OnPlotClear(self,event):
1779 def OnPlotScale(self, event):
1780 if self.client.last_draw != None:
1781 graphics, xAxis, yAxis= self.client.last_draw
1782 self.client.Draw(graphics,(1,3.05),(0,1))
1784 def OnEnableZoom(self, event):
1785 self.client.SetEnableZoom(event.IsChecked())
1787 def OnEnableGrid(self, event):
1788 self.client.SetEnableGrid(event.IsChecked())
1790 def OnEnableLegend(self, event):
1791 self.client.SetEnableLegend(event.IsChecked())
1793 def OnEnablePointLabel(self, event):
1794 self.client.SetEnablePointLabel(event.IsChecked())
1796 def OnScrUp(self, event):
1797 self.client.ScrollUp(1)
1799 def OnScrRt(self,event):
1800 self.client.ScrollRight(2)
1802 def OnReset(self,event):
1805 def OnHelpAbout(self, event):
1806 from wx.lib.dialogs import ScrolledMessageDialog
1807 about = ScrolledMessageDialog(self, __doc__, "About...")
1810 def resetDefaults(self):
1811 """Just to reset the fonts back to the PlotCanvas defaults"""
1812 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1813 self.client.SetFontSizeAxis(10)
1814 self.client.SetFontSizeLegend(7)
1815 self.client.SetXSpec('auto')
1816 self.client.SetYSpec('auto')
1821 class MyApp(wx.App):
1823 wx.InitAllImageHandlers()
1824 frame = TestFrame(None, -1, "PlotCanvas")
1826 self.SetTopWindow(frame)
1833 if __name__ == '__main__':