1 #-----------------------------------------------------------------------------
3 # Purpose: Line, Bar and Scatter Graphs
5 # Author: Gordon Williams
8 # RCS-ID: $Id: plot.py 1251 2005-08-16 01:38:09Z eb $
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.
40 This is a simple light weight plotting module that can be used with
41 Boa or easily integrated into your own wxPython application. The
42 emphasis is on small size and fast plotting for large data sets. It
43 has a reasonable number of features to do line and scatter graphs
44 easily as well as simple bar graphs. It is not as sophisticated or
45 as powerful as SciPy Plt or Chaco. Both of these are great packages
46 but consume huge amounts of computer resources for simple plots.
47 They can be found at http://scipy.com
49 This file contains two parts; first the re-usable library stuff, then,
50 after a "if __name__=='__main__'" test, a simple frame and a few default
51 plots for examples and testing.
54 Written by K.Hinsen, R. Srinivasan;
55 Ported to wxPython Harm van der Heijden, feb 1999
57 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
59 -Zooming using mouse 'rubber band'
62 -Printing, preview, and page set up (margins)
63 -Axis and title labels
64 -Cursor xy axis values
65 -Doc strings and lots of comments
66 -Optimizations for large number of points
69 Did a lot of work here to speed markers up. Only a factor of 4
70 improvement though. Lines are much faster than markers, especially
71 filled markers. Stay away from circles and triangles unless you
72 only have a few thousand points.
74 Times for 25,000 points
81 triangle, triangle_down - 0.90
83 Thanks to Chris Barker for getting this version working on Linux.
85 Zooming controls with mouse (when enabled):
86 Left mouse drag - Zoom box.
87 Left mouse double click - reset zoom.
88 Right mouse click - zoom out centred on click location.
91 import string as _string
95 # Needs Numeric or numarray
97 import Numeric as _Numeric
100 import numarray as _Numeric #if numarray is used it is renamed Numeric
103 This module requires the Numeric or numarray module,
104 which could not be imported. It probably is not installed
105 (it's not part of the standard Python distribution). See the
106 Python site (http://www.python.org) for information on
107 downloading source or binaries."""
108 raise ImportError, "Numeric or numarray not found. \n" + msg
113 # Plotting classes...
116 """Base Class for lines and markers
117 - All methods are private.
120 def __init__(self, points, attr):
121 self.points = _Numeric.array(points)
122 self.currentScale= (1,1)
123 self.currentShift= (0,0)
124 self.scaled = self.points
126 self.attributes.update(self._attributes)
127 for name, value in attr.items():
128 if name not in self._attributes.keys():
129 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
130 self.attributes[name] = value
132 def boundingBox(self):
133 if len(self.points) == 0:
135 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
136 minXY= _Numeric.array([-1,-1])
137 maxXY= _Numeric.array([ 1, 1])
139 minXY= _Numeric.minimum.reduce(self.points)
140 maxXY= _Numeric.maximum.reduce(self.points)
143 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
144 if len(self.points) == 0:
147 if (scale is not self.currentScale) or (shift is not self.currentShift):
148 # update point scaling
149 self.scaled = scale*self.points+shift
150 self.currentScale= scale
151 self.currentShift= shift
152 # else unchanged use the current scaling
155 return self.attributes['legend']
157 def getClosestPoint(self, pntXY, pointScaled= True):
158 """Returns the index of closest point on the curve, pointXY, scaledXY, distance
160 if pointScaled == True based on screen coords
161 if pointScaled == False based on user coords
163 if pointScaled == True:
166 pxy = self.currentScale * _Numeric.array(pntXY)+ self.currentShift
170 pxy = _Numeric.array(pntXY)
171 #determine distance for each point
172 d= _Numeric.sqrt(_Numeric.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
173 pntIndex = _Numeric.argmin(d)
175 return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
178 class PolyLine(PolyPoints):
179 """Class to define line type and style
180 - All methods except __init__ are private.
183 _attributes = {'colour': 'black',
188 def __init__(self, points, **attr):
189 """Creates PolyLine object
190 points - sequence (array, tuple or list) of (x,y) points making up line
191 **attr - key word attributes
193 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
194 'width'= 1, - Pen width
195 'style'= wx.SOLID, - wx.Pen style
196 'legend'= '' - Line Legend to display
198 PolyPoints.__init__(self, points, attr)
200 def draw(self, dc, printerScale, coord= None):
201 colour = self.attributes['colour']
202 width = self.attributes['width'] * printerScale
203 style= self.attributes['style']
204 pen = wx.Pen(wx.NamedColour(colour), width, style)
205 pen.SetCap(wx.CAP_BUTT)
208 dc.DrawLines(self.scaled)
210 dc.DrawLines(coord) # draw legend line
212 def getSymExtent(self, printerScale):
213 """Width and Height of Marker"""
214 h= self.attributes['width'] * printerScale
219 class PolyMarker(PolyPoints):
220 """Class to define marker type and style
221 - All methods except __init__ are private.
224 _attributes = {'colour': 'black',
228 'fillstyle': wx.SOLID,
232 def __init__(self, points, **attr):
233 """Creates PolyMarker object
234 points - sequence (array, tuple or list) of (x,y) points
235 **attr - key word attributes
237 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
238 'width'= 1, - Pen width
239 'size'= 2, - Marker size
240 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
241 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
242 'marker'= 'circle' - Marker shape
243 'legend'= '' - Marker Legend to display
255 PolyPoints.__init__(self, points, attr)
257 def draw(self, dc, printerScale, coord= None):
258 colour = self.attributes['colour']
259 width = self.attributes['width'] * printerScale
260 size = self.attributes['size'] * printerScale
261 fillcolour = self.attributes['fillcolour']
262 fillstyle = self.attributes['fillstyle']
263 marker = self.attributes['marker']
265 dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
267 dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
269 dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
271 self._drawmarkers(dc, self.scaled, marker, size)
273 self._drawmarkers(dc, coord, marker, size) # draw legend marker
275 def getSymExtent(self, printerScale):
276 """Width and Height of Marker"""
277 s= 5*self.attributes['size'] * printerScale
280 def _drawmarkers(self, dc, coords, marker,size=1):
281 f = eval('self._' +marker)
284 def _circle(self, dc, coords, size=1):
287 rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
288 rect[:,0:2]= coords-[fact,fact]
289 dc.DrawEllipseList(rect.astype(_Numeric.Int32))
291 def _dot(self, dc, coords, size=1):
292 dc.DrawPointList(coords)
294 def _square(self, dc, coords, size=1):
297 rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
298 rect[:,0:2]= coords-[fact,fact]
299 dc.DrawRectangleList(rect.astype(_Numeric.Int32))
301 def _triangle(self, dc, coords, size=1):
302 shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
303 poly= _Numeric.repeat(coords,3)
304 poly.shape= (len(coords),3,2)
306 dc.DrawPolygonList(poly.astype(_Numeric.Int32))
308 def _triangle_down(self, dc, coords, size=1):
309 shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
310 poly= _Numeric.repeat(coords,3)
311 poly.shape= (len(coords),3,2)
313 dc.DrawPolygonList(poly.astype(_Numeric.Int32))
315 def _cross(self, dc, coords, size=1):
317 for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
318 lines= _Numeric.concatenate((coords,coords),axis=1)+f
319 dc.DrawLineList(lines.astype(_Numeric.Int32))
321 def _plus(self, dc, coords, size=1):
323 for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
324 lines= _Numeric.concatenate((coords,coords),axis=1)+f
325 dc.DrawLineList(lines.astype(_Numeric.Int32))
328 """Container to hold PolyXXX objects and graph labels
329 - All methods except __init__ are private.
332 def __init__(self, objects, title='', xLabel='', yLabel= ''):
333 """Creates PlotGraphics object
334 objects - list of PolyXXX objects to make graph
335 title - title shown at top of graph
336 xLabel - label shown on x-axis
337 yLabel - label shown on y-axis
339 if type(objects) not in [list,tuple]:
340 raise TypeError, "objects argument should be list or tuple"
341 self.objects = objects
346 def boundingBox(self):
347 p1, p2 = self.objects[0].boundingBox()
348 for o in self.objects[1:]:
349 p1o, p2o = o.boundingBox()
350 p1 = _Numeric.minimum(p1, p1o)
351 p2 = _Numeric.maximum(p2, p2o)
354 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
355 for o in self.objects:
356 o.scaleAndShift(scale, shift)
358 def setPrinterScale(self, scale):
359 """Thickens up lines and markers only for printing"""
360 self.printerScale= scale
362 def setXLabel(self, xLabel= ''):
363 """Set the X axis label on the graph"""
366 def setYLabel(self, yLabel= ''):
367 """Set the Y axis label on the graph"""
370 def setTitle(self, title= ''):
371 """Set the title at the top of graph"""
375 """Get x axis label string"""
379 """Get y axis label string"""
382 def getTitle(self, title= ''):
383 """Get the title at the top of graph"""
387 for o in self.objects:
388 #t=_time.clock() # profile info
389 o.draw(dc, self.printerScale)
391 #print o, "time=", dt
393 def getSymExtent(self, printerScale):
394 """Get max width and height of lines and markers symbols for legend"""
395 symExt = self.objects[0].getSymExtent(printerScale)
396 for o in self.objects[1:]:
397 oSymExt = o.getSymExtent(printerScale)
398 symExt = _Numeric.maximum(symExt, oSymExt)
401 def getLegendNames(self):
402 """Returns list of legend names"""
403 lst = [None]*len(self)
404 for i in range(len(self)):
405 lst[i]= self.objects[i].getLegend()
409 return len(self.objects)
411 def __getitem__(self, item):
412 return self.objects[item]
415 #-------------------------------------------------------------------------------
416 # Main window that you will want to import into your application.
418 class PlotCanvas(wx.Window):
419 """Subclass of a wx.Window to allow simple general plotting
420 of data with zoom, labels, and automatic axis scaling."""
422 def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
423 size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
424 """Constucts a window, which can be a child of a frame, dialog or
425 any other non-control window"""
427 wx.Window.__init__(self, parent, id, pos, size, style, name)
430 self.SetBackgroundColour("white")
432 # Create some mouse events for zooming
433 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
434 self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
435 self.Bind(wx.EVT_MOTION, self.OnMotion)
436 self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
437 self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
439 # set curser as cross-hairs
440 self.SetCursor(wx.CROSS_CURSOR)
442 # Things for printing
443 self.print_data = wx.PrintData()
444 self.print_data.SetPaperId(wx.PAPER_LETTER)
445 self.print_data.SetOrientation(wx.LANDSCAPE)
446 self.pageSetupData= wx.PageSetupDialogData()
447 self.pageSetupData.SetMarginBottomRight((25,25))
448 self.pageSetupData.SetMarginTopLeft((25,25))
449 self.pageSetupData.SetPrintData(self.print_data)
450 self.printerScale = 1
454 self._zoomInFactor = 0.5
455 self._zoomOutFactor = 2
456 self._zoomCorner1= _Numeric.array([0.0, 0.0]) # left mouse down corner
457 self._zoomCorner2= _Numeric.array([0.0, 0.0]) # left mouse up corner
458 self._zoomEnabled= False
459 self._hasDragged= False
462 self.last_draw = None
467 self._gridEnabled= False
468 self._legendEnabled= False
469 self._xUseScopeTicks= False
473 self._fontSizeAxis= 10
474 self._fontSizeTitle= 15
475 self._fontSizeLegend= 7
478 self._pointLabelEnabled= False
479 self.last_PointLabel= None
480 self._pointLabelFunc= None
481 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
483 self.Bind(wx.EVT_PAINT, self.OnPaint)
484 self.Bind(wx.EVT_SIZE, self.OnSize)
485 # OnSize called to make sure the buffer is initialized.
486 # This might result in OnSize getting called twice on some
487 # platforms at initialization, but little harm done.
488 self.OnSize(None) # sets the initial size based on client size
489 # UNCONDITIONAL, needed to create self._Buffer
492 def SaveFile(self, fileName= ''):
493 """Saves the file to the type specified in the extension. If no file
494 name is specified a dialog box is provided. Returns True if sucessful,
497 .bmp Save a Windows bitmap file.
498 .xbm Save an X bitmap file.
499 .xpm Save an XPM bitmap file.
500 .png Save a Portable Network Graphics file.
501 .jpg Save a Joint Photographic Experts Group file.
503 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
504 dlg1 = wx.FileDialog(
506 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
507 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
508 wx.SAVE|wx.OVERWRITE_PROMPT
512 if dlg1.ShowModal() == wx.ID_OK:
513 fileName = dlg1.GetPath()
514 # Check for proper exension
515 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
516 dlg2 = wx.MessageDialog(self, 'File name extension\n'
518 'bmp, xbm, xpm, png, or jpg',
519 'File Name Error', wx.OK | wx.ICON_ERROR)
525 break # now save file
526 else: # exit without saving
531 # File name has required extension
532 fType = _string.lower(fileName[-3:])
534 tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
536 tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
538 tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
540 tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
542 tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
544 res= self._Buffer.SaveFile(fileName, tp)
548 """Brings up the page setup dialog"""
549 data = self.pageSetupData
550 data.SetPrintData(self.print_data)
551 dlg = wx.PageSetupDialog(self.parent, data)
553 if dlg.ShowModal() == wx.ID_OK:
554 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
555 # updates page parameters from dialog
556 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
557 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
558 self.pageSetupData.SetPrintData(data.GetPrintData())
559 self.print_data=data.GetPrintData() # updates print_data
563 def Printout(self, paper=None):
564 """Print current plot."""
566 self.print_data.SetPaperId(paper)
567 pdd = wx.PrintDialogData()
568 pdd.SetPrintData(self.print_data)
569 printer = wx.Printer(pdd)
570 out = PlotPrintout(self)
571 print_ok = printer.Print(self.parent, out)
573 self.print_data = printer.GetPrintDialogData().GetPrintData()
576 def PrintPreview(self):
577 """Print-preview current plot."""
578 printout = PlotPrintout(self)
579 printout2 = PlotPrintout(self)
580 self.preview = wx.PrintPreview(printout, printout2, self.print_data)
581 if not self.preview.Ok():
582 wx.MessageDialog(self, "Print Preview failed.\n" \
583 "Check that default printer is configured\n", \
584 "Print error", wx.OK|wx.CENTRE).ShowModal()
585 self.preview.SetZoom(40)
586 # search up tree to find frame instance
588 while not isinstance(frameInst, wx.Frame):
589 frameInst= frameInst.GetParent()
590 frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
592 frame.SetPosition(self.GetPosition())
593 frame.SetSize((600,550))
594 frame.Centre(wx.BOTH)
597 def SetFontSizeAxis(self, point= 10):
598 """Set the tick and axis label font size (default is 10 point)"""
599 self._fontSizeAxis= point
601 def GetFontSizeAxis(self):
602 """Get current tick and axis label font size in points"""
603 return self._fontSizeAxis
605 def SetFontSizeTitle(self, point= 15):
606 """Set Title font size (default is 15 point)"""
607 self._fontSizeTitle= point
609 def GetFontSizeTitle(self):
610 """Get current Title font size in points"""
611 return self._fontSizeTitle
613 def SetFontSizeLegend(self, point= 7):
614 """Set Legend font size (default is 7 point)"""
615 self._fontSizeLegend= point
617 def GetFontSizeLegend(self):
618 """Get current Legend font size in points"""
619 return self._fontSizeLegend
621 def SetEnableZoom(self, value):
622 """Set True to enable zooming."""
623 if value not in [True,False]:
624 raise TypeError, "Value should be True or False"
625 self._zoomEnabled= value
627 def GetEnableZoom(self):
628 """True if zooming enabled."""
629 return self._zoomEnabled
631 def SetEnableGrid(self, value):
632 """Set True to enable grid."""
633 if value not in [True,False]:
634 raise TypeError, "Value should be True or False"
635 self._gridEnabled= value
638 def GetEnableGrid(self):
639 """True if grid enabled."""
640 return self._gridEnabled
642 def SetEnableLegend(self, value):
643 """Set True to enable legend."""
644 if value not in [True,False]:
645 raise TypeError, "Value should be True or False"
646 self._legendEnabled= value
649 def GetEnableLegend(self):
650 """True if Legend enabled."""
651 return self._legendEnabled
653 def SetEnablePointLabel(self, value):
654 """Set True to enable pointLabel."""
655 if value not in [True,False]:
656 raise TypeError, "Value should be True or False"
657 self._pointLabelEnabled= value
658 self.Redraw() #will erase existing pointLabel if present
659 self.last_PointLabel = None
661 def GetEnablePointLabel(self):
662 """True if pointLabel enabled."""
663 return self._pointLabelEnabled
665 def SetPointLabelFunc(self, func):
666 """Sets the function with custom code for pointLabel drawing
667 ******** more info needed ***************
669 self._pointLabelFunc= func
671 def GetPointLabelFunc(self):
672 """Returns pointLabel Drawing Function"""
673 return self._pointLabelFunc
676 """Unzoom the plot."""
677 self.last_PointLabel = None #reset pointLabel
678 if self.last_draw is not None:
679 self.Draw(self.last_draw[0])
681 def ScrollRight(self, units):
682 """Move view right number of axis units."""
683 self.last_PointLabel = None #reset pointLabel
684 if self.last_draw is not None:
685 graphics, xAxis, yAxis= self.last_draw
686 xAxis= (xAxis[0]+units, xAxis[1]+units)
687 self.Draw(graphics,xAxis,yAxis)
689 def ScrollUp(self, units):
690 """Move view up number of axis units."""
691 self.last_PointLabel = None #reset pointLabel
692 if self.last_draw is not None:
693 graphics, xAxis, yAxis= self.last_draw
694 yAxis= (yAxis[0]+units, yAxis[1]+units)
695 self.Draw(graphics,xAxis,yAxis)
697 def GetXY(self,event):
698 """Takes a mouse event and returns the XY user axis values."""
699 x,y= self.PositionScreenToUser(event.GetPosition())
702 def PositionUserToScreen(self, pntXY):
703 """Converts User position to Screen Coordinates"""
704 userPos= _Numeric.array(pntXY)
705 x,y= userPos * self._pointScale + self._pointShift
708 def PositionScreenToUser(self, pntXY):
709 """Converts Screen position to User Coordinates"""
710 screenPos= _Numeric.array(pntXY)
711 x,y= (screenPos-self._pointShift)/self._pointScale
714 def SetXSpec(self, type= 'auto'):
715 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
717 'none' - shows no axis or tick mark values
718 'min' - shows min bounding box values
719 'auto' - rounds axis range to sensible values
723 def SetYSpec(self, type= 'auto'):
724 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
726 'none' - shows no axis or tick mark values
727 'min' - shows min bounding box values
728 'auto' - rounds axis range to sensible values
733 """Returns current XSpec for axis"""
737 """Returns current YSpec for axis"""
740 def GetXMaxRange(self):
741 """Returns (minX, maxX) x-axis range for displayed graph"""
742 graphics= self.last_draw[0]
743 p1, p2 = graphics.boundingBox() # min, max points of graphics
744 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
747 def GetYMaxRange(self):
748 """Returns (minY, maxY) y-axis range for displayed graph"""
749 graphics= self.last_draw[0]
750 p1, p2 = graphics.boundingBox() # min, max points of graphics
751 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
754 def GetXCurrentRange(self):
755 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
756 return self.last_draw[1]
758 def GetYCurrentRange(self):
759 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
760 return self.last_draw[2]
762 def SetXUseScopeTicks(self, v=False):
763 """Always 10 divisions, no labels"""
764 self._xUseScopeTicks = v
766 def GetXUseScopeTicks(self):
767 return self._xUseScopeTicks
769 def Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
770 """Draw objects in graphics with specified x and y axis.
771 graphics- instance of PlotGraphics with list of PolyXXX objects
772 xAxis - tuple with (min, max) axis range to view
773 yAxis - same as xAxis
774 dc - drawing context - doesn't have to be specified.
775 If it's not, the offscreen buffer is used
777 # check Axis is either tuple or none
778 if type(xAxis) not in [type(None),tuple]:
779 raise TypeError, "xAxis should be None or (minX,maxX)"
780 if type(yAxis) not in [type(None),tuple]:
781 raise TypeError, "yAxis should be None or (minY,maxY)"
783 # check case for axis = (a,b) where a==b caused by improper zooms
785 if xAxis[0] == xAxis[1]:
788 if yAxis[0] == yAxis[1]:
792 # sets new dc and clears it
793 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
799 # set font size for every thing but title and legend
800 dc.SetFont(self._getFont(self._fontSizeAxis))
802 # sizes axis to axis type, create lower left and upper right corners of plot
803 if xAxis == None or yAxis == None:
804 # One or both axis not specified in Draw
805 p1, p2 = graphics.boundingBox() # min, max points of graphics
807 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
809 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
810 # Adjust bounding box for axis spec
811 p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
812 p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
814 # Both axis specified in Draw
815 p1= _Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
816 p2= _Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
818 self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
820 # Get ticks and textExtents for axis if required
821 if self._xSpec is not 'none':
822 if self._xUseScopeTicks:
823 xticks = self._scope_ticks(xAxis[0], xAxis[1])
825 xticks = self._ticks(xAxis[0], xAxis[1])
826 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
829 xTextExtent= (0,0) # No text for ticks
830 if self._ySpec is not 'none':
831 yticks = self._ticks(yAxis[0], yAxis[1])
832 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
833 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
834 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
835 max(yTextExtentBottom[1],yTextExtentTop[1]))
838 yTextExtent= (0,0) # No text for ticks
840 # TextExtents for Title and Axis Labels
841 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
843 # TextExtents for Legend
844 legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
846 # room around graph area
847 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
848 lhsW= yTextExtent[0]+ yLabelWH[1]
849 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
850 topH= yTextExtent[1]/2. + titleWH[1]
851 textSize_scale= _Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
852 textSize_shift= _Numeric.array([lhsW, bottomH]) # shift plot area by this amount
854 # drawing title and labels text
855 dc.SetFont(self._getFont(self._fontSizeTitle))
856 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
857 self.plotbox_origin[1]- self.plotbox_size[1])
858 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
859 dc.SetFont(self._getFont(self._fontSizeAxis))
860 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
861 self.plotbox_origin[1]- xLabelWH[1])
862 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
863 yLabelPos= (self.plotbox_origin[0],
864 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
865 if graphics.getYLabel(): # bug fix for Linux
866 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
868 # drawing legend makers and text
869 if self._legendEnabled:
870 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
872 # allow for scaling and shifting plotted points
873 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1))
874 shift = -p1*scale + self.plotbox_origin + textSize_shift * _Numeric.array((1,-1))
875 self._pointScale= scale # make available for mouse events
876 self._pointShift= shift
877 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
879 graphics.scaleAndShift(scale, shift)
880 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
882 # set clipping area so drawing does not occur outside axis box
883 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
884 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
885 # Draw the lines and markers
886 #start = _time.clock()
888 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
889 # remove the clipping region
890 dc.DestroyClippingRegion()
893 def Redraw(self, dc= None):
894 """Redraw the existing plot."""
895 if self.last_draw is not None:
896 graphics, xAxis, yAxis= self.last_draw
897 self.Draw(graphics,xAxis,yAxis,dc)
900 """Erase the window."""
901 self.last_PointLabel = None #reset pointLabel
902 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
904 self.last_draw = None
906 def Zoom(self, Center, Ratio):
908 Centers on the X,Y coords given in Center
909 Zooms by the Ratio = (Xratio, Yratio) given
911 self.last_PointLabel = None #reset maker
913 if self.last_draw != None:
914 (graphics, xAxis, yAxis) = self.last_draw
915 w = (xAxis[1] - xAxis[0]) * Ratio[0]
916 h = (yAxis[1] - yAxis[0]) * Ratio[1]
917 xAxis = ( x - w/2, x + w/2 )
918 yAxis = ( y - h/2, y + h/2 )
919 self.Draw(graphics, xAxis, yAxis)
921 def GetClosestPoints(self, pntXY, pointScaled= True):
923 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
925 Returns [] if no curves are being plotted.
928 if pointScaled == True based on screen coords
929 if pointScaled == False based on user coords
931 if self.last_draw == None:
934 graphics, xAxis, yAxis= self.last_draw
936 for curveNum,obj in enumerate(graphics):
937 #check there are points in the curve
938 if len(obj.points) == 0:
939 continue #go to next obj
940 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
941 cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
945 def GetClosetPoint(self, pntXY, pointScaled= True):
947 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
948 list for only the closest curve.
949 Returns [] if no curves are being plotted.
952 if pointScaled == True based on screen coords
953 if pointScaled == False based on user coords
955 #closest points on screen based on screen scaling (pointScaled= True)
956 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
957 closestPts= self.GetClosestPoints(pntXY, pointScaled)
959 return [] #no graph present
960 #find one with least distance
961 dists = [c[-1] for c in closestPts]
962 mdist = min(dists) #Min dist
963 i = dists.index(mdist) #index for min dist
964 return closestPts[i] #this is the closest point on closest curve
966 def UpdatePointLabel(self, mDataDict):
967 """Updates the pointLabel point on screen with data contained in
970 mDataDict will be passed to your function set by
971 SetPointLabelFunc. It can contain anything you
972 want to display on the screen at the scaledXY point
975 This function can be called from parent window with onClick,
978 if self.last_PointLabel != None:
980 if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
982 self._drawPointLabel(self.last_PointLabel) #erase old
983 self._drawPointLabel(mDataDict) #plot new
985 #just plot new with no erase
986 self._drawPointLabel(mDataDict) #plot new
988 self.last_PointLabel = mDataDict
990 # event handlers **********************************
991 def OnMotion(self, event):
992 if self._zoomEnabled and event.LeftIsDown():
994 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
996 self._hasDragged= True
997 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
998 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
1000 def OnMouseLeftDown(self,event):
1001 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
1003 def OnMouseLeftUp(self, event):
1004 if self._zoomEnabled:
1005 if self._hasDragged == True:
1006 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1007 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
1008 self._hasDragged = False # reset flag
1009 minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
1010 maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
1011 self.last_PointLabel = None #reset pointLabel
1012 if self.last_draw != None:
1013 self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
1014 #else: # A box has not been drawn, zoom in on a point
1015 ## this interfered with the double click, so I've disables it.
1016 # X,Y = self.GetXY(event)
1017 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1019 def OnMouseDoubleClick(self,event):
1020 if self._zoomEnabled:
1023 def OnMouseRightDown(self,event):
1024 if self._zoomEnabled:
1025 X,Y = self.GetXY(event)
1026 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1028 def OnPaint(self, event):
1029 # All that is needed here is to draw the buffer to screen
1030 if self.last_PointLabel != None:
1031 self._drawPointLabel(self.last_PointLabel) #erase old
1032 self.last_PointLabel = None
1033 dc = wx.BufferedPaintDC(self, self._Buffer)
1035 def OnSize(self,event):
1036 # The Buffer init is done here, to make sure the buffer is always
1037 # the same size as the Window
1038 Size = self.GetClientSize()
1040 # Make new offscreen bitmap: this bitmap will always have the
1041 # current drawing in it, so it can be used to save the image to
1042 # a file, or whatever.
1043 self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
1046 self.last_PointLabel = None #reset pointLabel
1048 if self.last_draw is None:
1051 graphics, xSpec, ySpec = self.last_draw
1052 self.Draw(graphics,xSpec,ySpec)
1054 def OnLeave(self, event):
1055 """Used to erase pointLabel when mouse outside window"""
1056 if self.last_PointLabel != None:
1057 self._drawPointLabel(self.last_PointLabel) #erase old
1058 self.last_PointLabel = None
1061 # Private Methods **************************************************
1062 def _setSize(self, width=None, height=None):
1063 """DC width and height."""
1065 (self.width,self.height) = self.GetClientSize()
1067 self.width, self.height= width,height
1068 self.plotbox_size = 0.97*_Numeric.array([self.width, self.height])
1069 xo = 0.5*(self.width-self.plotbox_size[0])
1070 yo = self.height-0.5*(self.height-self.plotbox_size[1])
1071 self.plotbox_origin = _Numeric.array([xo, yo])
1073 def _setPrinterScale(self, scale):
1074 """Used to thicken lines and increase marker size for print out."""
1075 # line thickness on printer is very thin at 600 dot/in. Markers small
1076 self.printerScale= scale
1078 def _printDraw(self, printDC):
1079 """Used for printing."""
1080 if self.last_draw != None:
1081 graphics, xSpec, ySpec= self.last_draw
1082 self.Draw(graphics,xSpec,ySpec,printDC)
1084 def _drawPointLabel(self, mDataDict):
1085 """Draws and erases pointLabels"""
1086 width = self._Buffer.GetWidth()
1087 height = self._Buffer.GetHeight()
1088 tmp_Buffer = wx.EmptyBitmap(width,height)
1090 dcs.SelectObject(tmp_Buffer)
1093 self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
1096 dc = wx.ClientDC( self )
1097 #this will erase if called twice
1098 dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
1101 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
1102 """Draws legend symbols and text"""
1103 # top right hand corner of graph box is ref corner
1104 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1105 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
1106 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1107 dc.SetFont(self._getFont(self._fontSizeLegend))
1108 for i in range(len(graphics)):
1111 if isinstance(o,PolyMarker):
1112 # draw marker with legend
1113 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
1114 o.draw(dc, self.printerScale, coord= _Numeric.array([pnt]))
1115 elif isinstance(o,PolyLine):
1116 # draw line with legend
1117 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1118 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
1119 o.draw(dc, self.printerScale, coord= _Numeric.array([pnt1,pnt2]))
1121 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1123 pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
1124 dc.DrawText(o.getLegend(),pnt[0],pnt[1])
1125 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1127 def _titleLablesWH(self, dc, graphics):
1128 """Draws Title and labels and returns width and height for each"""
1129 # TextExtents for Title and Axis Labels
1130 dc.SetFont(self._getFont(self._fontSizeTitle))
1131 title= graphics.getTitle()
1132 titleWH= dc.GetTextExtent(title)
1133 dc.SetFont(self._getFont(self._fontSizeAxis))
1134 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1135 xLabelWH= dc.GetTextExtent(xLabel)
1136 yLabelWH= dc.GetTextExtent(yLabel)
1137 return titleWH, xLabelWH, yLabelWH
1139 def _legendWH(self, dc, graphics):
1140 """Returns the size in screen units for legend box"""
1141 if self._legendEnabled != True:
1142 legendBoxWH= symExt= txtExt= (0,0)
1144 # find max symbol size
1145 symExt= graphics.getSymExtent(self.printerScale)
1146 # find max legend text extent
1147 dc.SetFont(self._getFont(self._fontSizeLegend))
1148 txtList= graphics.getLegendNames()
1149 txtExt= dc.GetTextExtent(txtList[0])
1150 for txt in graphics.getLegendNames()[1:]:
1151 txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt))
1152 maxW= symExt[0]+txtExt[0]
1153 maxH= max(symExt[1],txtExt[1])
1154 # padding .1 for lhs of legend box and space between lines
1156 maxH= maxH* 1.1 * len(txtList)
1157 dc.SetFont(self._getFont(self._fontSizeAxis))
1158 legendBoxWH= (maxW,maxH)
1159 return (legendBoxWH, symExt, txtExt)
1161 def _drawRubberBand(self, corner1, corner2):
1162 """Draws/erases rect box from corner1 to corner2"""
1163 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1165 dc = wx.ClientDC( self )
1167 dc.SetPen(wx.Pen(wx.BLACK))
1168 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1169 dc.SetLogicalFunction(wx.INVERT)
1170 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
1171 dc.SetLogicalFunction(wx.COPY)
1174 def _getFont(self,size):
1175 """Take font size, adjusts if printing and returns wx.Font"""
1176 s = size*self.printerScale
1178 # Linux speed up to get font from cache rather than X font server
1179 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1180 font = self._fontCache.get (key, None)
1182 return font # yeah! cache hit
1184 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1185 self._fontCache[key] = font
1189 def _point2ClientCoord(self, corner1, corner2):
1190 """Converts user point coords to client screen int coords x,y,width,height"""
1191 c1= _Numeric.array(corner1)
1192 c2= _Numeric.array(corner2)
1193 # convert to screen coords
1194 pt1= c1*self._pointScale+self._pointShift
1195 pt2= c2*self._pointScale+self._pointShift
1196 # make height and width positive
1197 pul= _Numeric.minimum(pt1,pt2) # Upper left corner
1198 plr= _Numeric.maximum(pt1,pt2) # Lower right corner
1199 rectWidth, rectHeight= plr-pul
1201 return ptx, pty, rectWidth, rectHeight
1203 def _axisInterval(self, spec, lower, upper):
1204 """Returns sensible axis range for given spec"""
1205 if spec == 'none' or spec == 'min':
1207 return lower-0.5, upper+0.5
1210 elif spec == 'auto':
1213 if abs(range) < 1e-36:
1214 return lower-0.5, upper+0.5
1215 log = _Numeric.log10(range)
1216 power = _Numeric.floor(log)
1217 fraction = log-power
1218 if fraction <= 0.05:
1221 lower = lower - lower % grid
1224 upper = upper - mod + grid
1226 elif type(spec) == type(()):
1233 raise ValueError, str(spec) + ': illegal axis specification'
1235 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1237 penWidth= self.printerScale # increases thickness for printing only
1238 dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
1240 # set length of tick marks--long ones make grid
1241 if self._gridEnabled:
1242 x,y,width,height= self._point2ClientCoord(p1,p2)
1243 yTickLength= width/2.0 +1
1244 xTickLength= height/2.0 +1
1246 yTickLength= 3 * self.printerScale # lengthens lines for printing
1247 xTickLength= 3 * self.printerScale
1249 if self._xSpec is not 'none':
1250 lower, upper = p1[0],p2[0]
1252 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
1253 a1 = scale*_Numeric.array([lower, y])+shift
1254 a2 = scale*_Numeric.array([upper, y])+shift
1255 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1256 for x, label in xticks:
1257 pt = scale*_Numeric.array([x, y])+shift
1258 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1260 dc.DrawText(label,pt[0],pt[1])
1261 text = 0 # axis values not drawn on top side
1263 if self._ySpec is not 'none':
1264 lower, upper = p1[1],p2[1]
1266 h = dc.GetCharHeight()
1267 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1268 a1 = scale*_Numeric.array([x, lower])+shift
1269 a2 = scale*_Numeric.array([x, upper])+shift
1270 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1271 for y, label in yticks:
1272 pt = scale*_Numeric.array([x, y])+shift
1273 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1275 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1277 text = 0 # axis values not drawn on right side
1279 def _ticks(self, lower, upper):
1280 ideal = (upper-lower)/7.
1281 log = _Numeric.log10(ideal)
1282 power = _Numeric.floor(log)
1283 fraction = log-power
1286 for f, lf in self._multiples:
1287 e = _Numeric.fabs(fraction-lf)
1291 grid = factor * 10.**power
1292 if power > 4 or power < -4:
1295 digits = max(1, int(power))
1296 format = '%' + `digits`+'.0f'
1298 digits = -int(power)
1299 format = '%'+`digits+2`+'.'+`digits`+'f'
1301 t = -grid*_Numeric.floor(-lower/grid)
1303 ticks.append( (t, format % (t,)) )
1307 def _scope_ticks (self, lower, upper):
1308 '''Always 10 divisions, no labels'''
1309 grid = (upper - lower) / 10.0
1313 ticks.append( (t, ""))
1317 _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))]
1320 #-------------------------------------------------------------------------------
1321 # Used to layout the printer page
1323 class PlotPrintout(wx.Printout):
1324 """Controls how the plot is made in printing and previewing"""
1325 # Do not change method names in this class,
1326 # we have to override wx.Printout methods here!
1327 def __init__(self, graph):
1328 """graph is instance of plotCanvas to be printed or previewed"""
1329 wx.Printout.__init__(self)
1332 def HasPage(self, page):
1338 def GetPageInfo(self):
1339 return (1, 1, 1, 1) # disable page numbers
1341 def OnPrintPage(self, page):
1342 dc = self.GetDC() # allows using floats for certain functions
1343 ## print "PPI Printer",self.GetPPIPrinter()
1344 ## print "PPI Screen", self.GetPPIScreen()
1345 ## print "DC GetSize", dc.GetSize()
1346 ## print "GetPageSizePixels", self.GetPageSizePixels()
1347 # Note PPIScreen does not give the correct number
1348 # Calulate everything for printer and then scale for preview
1349 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1350 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1351 dcSize= dc.GetSize() # DC size
1352 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1353 clientDcSize= self.graph.GetClientSize()
1355 # find what the margins are (mm)
1356 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1357 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1359 # calculate offset and scale for dc
1360 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1361 pixRight= margRightSize*PPIPrinter[0]/25.4
1362 pixTop= margTopSize*PPIPrinter[1]/25.4
1363 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1365 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1366 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1368 # ratio offset and scale to screen size if preview
1369 if self.IsPreview():
1370 ratioW= float(dcSize[0])/pageSize[0]
1371 ratioH= float(dcSize[1])/pageSize[1]
1377 # rescale plot to page or preview plot area
1378 self.graph._setSize(plotAreaW,plotAreaH)
1380 # Set offset and scale
1381 dc.SetDeviceOrigin(pixLeft,pixTop)
1383 # Thicken up pens and increase marker size for printing
1384 ratioW= float(plotAreaW)/clientDcSize[0]
1385 ratioH= float(plotAreaH)/clientDcSize[1]
1386 aveScale= (ratioW+ratioH)/2
1387 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1389 self.graph._printDraw(dc)
1390 # rescale back to original
1391 self.graph._setSize()
1392 self.graph._setPrinterScale(1)
1393 self.graph.Redraw() #to get point label scale and shift correct
1400 #---------------------------------------------------------------------------
1401 # if running standalone...
1403 # ...a sample implementation using the above
1406 def _draw1Objects():
1407 # 100 points sin function, plotted as green circles
1408 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
1409 data1.shape = (100, 2)
1410 data1[:,1] = _Numeric.sin(data1[:,0])
1411 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1413 # 50 points cos function, plotted as red line
1414 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
1415 data1.shape = (50,2)
1416 data1[:,1] = _Numeric.cos(data1[:,0])
1417 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1419 # A few more points...
1421 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1422 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1425 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1427 def _draw2Objects():
1428 # 100 points sin function, plotted as green dots
1429 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
1430 data1.shape = (100, 2)
1431 data1[:,1] = _Numeric.sin(data1[:,0])
1432 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1434 # 50 points cos function, plotted as red dot-dash
1435 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
1436 data1.shape = (50,2)
1437 data1[:,1] = _Numeric.cos(data1[:,0])
1438 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1440 # A few more points...
1442 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1443 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1444 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1447 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1449 def _draw3Objects():
1450 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1451 'cross', 'plus', 'circle']
1453 for i in range(len(markerList)):
1454 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1455 marker=markerList[i]))
1456 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1458 def _draw4Objects():
1460 data1 = _Numeric.arange(5e5,1e6,10)
1461 data1.shape = (25000, 2)
1462 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1464 # A few more points...
1465 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1467 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1469 def _draw5Objects():
1470 # Empty graph with axis defined but no points/lines
1472 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1473 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1475 def _draw6Objects():
1477 points1=[(1,0), (1,10)]
1478 line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1479 points1g=[(2,0), (2,4)]
1480 line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1481 points1b=[(3,0), (3,6)]
1482 line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1484 points2=[(4,0), (4,12)]
1485 line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1486 points2g=[(5,0), (5,8)]
1487 line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1488 points2b=[(6,0), (6,4)]
1489 line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1491 return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1492 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1495 class TestFrame(wx.Frame):
1496 def __init__(self, parent, id, title):
1497 wx.Frame.__init__(self, parent, id, title,
1498 wx.DefaultPosition, (600, 400))
1500 # Now Create the menu bar and items
1501 self.mainmenu = wx.MenuBar()
1504 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1505 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1507 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1508 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1510 menu.Append(202, 'Print...', 'Print the current plot')
1511 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1513 menu.Append(203, 'Save Plot...', 'Save current plot')
1514 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1516 menu.Append(205, 'E&xit', 'Enough of this already!')
1517 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1518 self.mainmenu.Append(menu, '&File')
1521 menu.Append(206, 'Draw1', 'Draw plots1')
1522 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1523 menu.Append(207, 'Draw2', 'Draw plots2')
1524 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1525 menu.Append(208, 'Draw3', 'Draw plots3')
1526 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1527 menu.Append(209, 'Draw4', 'Draw plots4')
1528 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1529 menu.Append(210, 'Draw5', 'Draw plots5')
1530 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1531 menu.Append(260, 'Draw6', 'Draw plots6')
1532 self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
1535 menu.Append(211, '&Redraw', 'Redraw plots')
1536 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1537 menu.Append(212, '&Clear', 'Clear canvas')
1538 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1539 menu.Append(213, '&Scale', 'Scale canvas')
1540 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1541 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1542 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1543 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1544 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1545 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1546 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1547 menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1548 self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1550 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1551 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1552 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1553 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1554 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1555 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1557 self.mainmenu.Append(menu, '&Plot')
1560 menu.Append(300, '&About', 'About this thing...')
1561 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1562 self.mainmenu.Append(menu, '&Help')
1564 self.SetMenuBar(self.mainmenu)
1566 # A status bar to tell people what's happening
1567 self.CreateStatusBar(1)
1569 self.client = PlotCanvas(self)
1570 #define the function for drawing pointLabels
1571 self.client.SetPointLabelFunc(self.DrawPointLabel)
1572 # Create mouse event for showing cursor coords in status bar
1573 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1574 # Show closest point when enabled
1575 self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1579 def DrawPointLabel(self, dc, mDataDict):
1580 """This is the fuction that defines how the pointLabels are plotted
1581 dc - DC that will be passed
1582 mDataDict - Dictionary of data that you want to use for the pointLabel
1584 As an example I have decided I want a box at the curve point
1585 with some text information about the curve plotted below.
1586 Any wxDC method can be used.
1589 dc.SetPen(wx.Pen(wx.BLACK))
1590 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1592 sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1593 dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
1594 px,py = mDataDict["pointXY"]
1595 cNum = mDataDict["curveNum"]
1596 pntIn = mDataDict["pIndex"]
1597 legend = mDataDict["legend"]
1598 #make a string to display
1599 s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1600 dc.DrawText(s, sx , sy+1)
1603 def OnMouseLeftDown(self,event):
1604 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1605 self.SetStatusText(s)
1606 event.Skip() #allows plotCanvas OnMouseLeftDown to be called
1608 def OnMotion(self, event):
1609 #show closest point (when enbled)
1610 if self.client.GetEnablePointLabel() == True:
1611 #make up dict with info for the pointLabel
1612 #I've decided to mark the closest point on the closest curve
1613 dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1614 if dlst != []: #returns [] if none
1615 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1616 #make up dictionary to pass to my user function (see DrawPointLabel)
1617 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1618 "pointXY":pointXY, "scaledXY":scaledXY}
1619 #pass dict to update the pointLabel
1620 self.client.UpdatePointLabel(mDataDict)
1621 event.Skip() #go to next handler
1623 def OnFilePageSetup(self, event):
1624 self.client.PageSetup()
1626 def OnFilePrintPreview(self, event):
1627 self.client.PrintPreview()
1629 def OnFilePrint(self, event):
1630 self.client.Printout()
1632 def OnSaveFile(self, event):
1633 self.client.SaveFile()
1635 def OnFileExit(self, event):
1638 def OnPlotDraw1(self, event):
1639 self.resetDefaults()
1640 self.client.Draw(_draw1Objects())
1642 def OnPlotDraw2(self, event):
1643 self.resetDefaults()
1644 self.client.Draw(_draw2Objects())
1646 def OnPlotDraw3(self, event):
1647 self.resetDefaults()
1648 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1649 self.client.SetFontSizeAxis(20)
1650 self.client.SetFontSizeLegend(12)
1651 self.client.SetXSpec('min')
1652 self.client.SetYSpec('none')
1653 self.client.Draw(_draw3Objects())
1655 def OnPlotDraw4(self, event):
1656 self.resetDefaults()
1657 drawObj= _draw4Objects()
1658 self.client.Draw(drawObj)
1660 ## start = _time.clock()
1661 ## for x in range(10):
1662 ## self.client.Draw(drawObj)
1663 ## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1666 def OnPlotDraw5(self, event):
1667 # Empty plot with just axes
1668 self.resetDefaults()
1669 drawObj= _draw5Objects()
1670 # make the axis X= (0,5), Y=(0,10)
1671 # (default with None is X= (-1,1), Y= (-1,1))
1672 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1674 def OnPlotDraw6(self, event):
1676 self.resetDefaults()
1677 #self.client.SetEnableLegend(True) #turn on Legend
1678 #self.client.SetEnableGrid(True) #turn on Grid
1679 self.client.SetXSpec('none') #turns off x-axis scale
1680 self.client.SetYSpec('auto')
1681 self.client.Draw(_draw6Objects(), xAxis= (0,7))
1683 def OnPlotRedraw(self,event):
1684 self.client.Redraw()
1686 def OnPlotClear(self,event):
1689 def OnPlotScale(self, event):
1690 if self.client.last_draw != None:
1691 graphics, xAxis, yAxis= self.client.last_draw
1692 self.client.Draw(graphics,(1,3.05),(0,1))
1694 def OnEnableZoom(self, event):
1695 self.client.SetEnableZoom(event.IsChecked())
1697 def OnEnableGrid(self, event):
1698 self.client.SetEnableGrid(event.IsChecked())
1700 def OnEnableLegend(self, event):
1701 self.client.SetEnableLegend(event.IsChecked())
1703 def OnEnablePointLabel(self, event):
1704 self.client.SetEnablePointLabel(event.IsChecked())
1706 def OnScrUp(self, event):
1707 self.client.ScrollUp(1)
1709 def OnScrRt(self,event):
1710 self.client.ScrollRight(2)
1712 def OnReset(self,event):
1715 def OnHelpAbout(self, event):
1716 from wx.lib.dialogs import ScrolledMessageDialog
1717 about = ScrolledMessageDialog(self, __doc__, "About...")
1720 def resetDefaults(self):
1721 """Just to reset the fonts back to the PlotCanvas defaults"""
1722 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1723 self.client.SetFontSizeAxis(10)
1724 self.client.SetFontSizeLegend(7)
1725 self.client.SetXSpec('auto')
1726 self.client.SetYSpec('auto')
1731 class MyApp(wx.App):
1733 wx.InitAllImageHandlers()
1734 frame = TestFrame(None, -1, "PlotCanvas")
1736 self.SetTopWindow(frame)
1743 if __name__ == '__main__':