diff options
Diffstat (limited to 'gr-wxgui/src/python/plot.py')
-rw-r--r-- | gr-wxgui/src/python/plot.py | 1834 |
1 files changed, 1834 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/plot.py b/gr-wxgui/src/python/plot.py new file mode 100644 index 000000000..041a2a7a5 --- /dev/null +++ b/gr-wxgui/src/python/plot.py @@ -0,0 +1,1834 @@ +#----------------------------------------------------------------------------- +# Name: wx.lib.plot.py +# Purpose: Line, Bar and Scatter Graphs +# +# Author: Gordon Williams +# +# Created: 2003/11/03 +# RCS-ID: $Id$ +# Copyright: (c) 2002,2007,2010 +# Licence: Use as you wish. +#----------------------------------------------------------------------------- +# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Renamed to plot.py in the wx.lib directory. +# o Reworked test frame to work with wx demo framework. This saves a bit +# of tedious cut and paste, and the test app is excellent. +# +# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxScrolledMessageDialog -> ScrolledMessageDialog +# +# Oct 6, 2004 Gordon Williams (g_will@cyberus.ca) +# - Added bar graph demo +# - Modified line end shape from round to square. +# - Removed FloatDCWrapper for conversion to ints and ints in arguments +# +# Oct 15, 2004 Gordon Williams (g_will@cyberus.ca) +# - Imported modules given leading underscore to name. +# - Added Cursor Line Tracking and User Point Labels. +# - Demo for Cursor Line Tracking and Point Labels. +# - Size of plot preview frame adjusted to show page better. +# - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas. +# - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve) +# can be in either user coords or screen coords. +# +# May 27, 2007 Johnathan Corgan (jcorgan@corganenterprises.com) +# - Converted from numarray to numpy +# +# Apr 23, 2010 Martin Dudok van Heel (http://www.olifantasia.com/gnuradio/contact_olifantasia.gif) +# - Added Persistence option (emulate after glow of an analog CRT display using IIR) + +""" +This is a simple light weight plotting module that can be used with +Boa or easily integrated into your own wxPython application. The +emphasis is on small size and fast plotting for large data sets. It +has a reasonable number of features to do line and scatter graphs +easily as well as simple bar graphs. It is not as sophisticated or +as powerful as SciPy Plt or Chaco. Both of these are great packages +but consume huge amounts of computer resources for simple plots. +They can be found at http://scipy.com + +This file contains two parts; first the re-usable library stuff, then, +after a "if __name__=='__main__'" test, a simple frame and a few default +plots for examples and testing. + +Based on wxPlotCanvas +Written by K.Hinsen, R. Srinivasan; +Ported to wxPython Harm van der Heijden, feb 1999 + +Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca) + -More style options + -Zooming using mouse 'rubber band' + -Scroll left, right + -Grid(graticule) + -Printing, preview, and page set up (margins) + -Axis and title labels + -Cursor xy axis values + -Doc strings and lots of comments + -Optimizations for large number of points + -Legends + +Did a lot of work here to speed markers up. Only a factor of 4 +improvement though. Lines are much faster than markers, especially +filled markers. Stay away from circles and triangles unless you +only have a few thousand points. + +Times for 25,000 points +Line - 0.078 sec +Markers +Square - 0.22 sec +dot - 0.10 +circle - 0.87 +cross,plus - 0.28 +triangle, triangle_down - 0.90 + +Thanks to Chris Barker for getting this version working on Linux. + +Zooming controls with mouse (when enabled): + Left mouse drag - Zoom box. + Left mouse double click - reset zoom. + Right mouse click - zoom out centred on click location. +""" + +import string as _string +import time as _time +import wx + +# Needs numpy or numarray +try: + import numpy as _numpy +except: + try: + import numarray as _numpy #if numarray is used it is renamed numpy + except: + msg= """ + This module requires the numpy or numarray module, + which could not be imported. It probably is not installed + (it's not part of the standard Python distribution). See the + Python site (http://www.python.org) for information on + downloading source or binaries.""" + raise ImportError, "numpy or numarray not found. \n" + msg + + + +# +# Plotting classes... +# +class PolyPoints: + """Base Class for lines and markers + - All methods are private. + """ + + def __init__(self, points, attr): + self.points = _numpy.array(points) + self.currentScale= (1,1) + self.currentShift= (0,0) + self.scaled = self.points + self.attributes = {} + self.attributes.update(self._attributes) + for name, value in attr.items(): + if name not in self._attributes.keys(): + raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys() + self.attributes[name] = value + + def boundingBox(self): + if len(self.points) == 0: + # no curves to draw + # defaults to (-1,-1) and (1,1) but axis can be set in Draw + minXY= _numpy.array([-1,-1]) + maxXY= _numpy.array([ 1, 1]) + else: + minXY= _numpy.minimum.reduce(self.points) + maxXY= _numpy.maximum.reduce(self.points) + return minXY, maxXY + + def scaleAndShift(self, scale=(1,1), shift=(0,0)): + if len(self.points) == 0: + # no curves to draw + return + if (scale is not self.currentScale) or (shift is not self.currentShift): + # update point scaling + self.scaled = scale*self.points+shift + self.currentScale= scale + self.currentShift= shift + # else unchanged use the current scaling + + def getLegend(self): + return self.attributes['legend'] + + def getClosestPoint(self, pntXY, pointScaled= True): + """Returns the index of closest point on the curve, pointXY, scaledXY, distance + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + if pointScaled == True: + #Using screen coords + p = self.scaled + pxy = self.currentScale * _numpy.array(pntXY)+ self.currentShift + else: + #Using user coords + p = self.points + pxy = _numpy.array(pntXY) + #determine distance for each point + d= _numpy.sqrt(_numpy.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2) + pntIndex = _numpy.argmin(d) + dist = d[pntIndex] + return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist] + + +class PolyLine(PolyPoints): + """Class to define line type and style + - All methods except __init__ are private. + """ + + _attributes = {'colour': 'black', + 'width': 1, + 'style': wx.SOLID, + 'legend': ''} + + def __init__(self, points, **attr): + """Creates PolyLine object + points - sequence (array, tuple or list) of (x,y) points making up line + **attr - key word attributes + Defaults: + 'colour'= 'black', - wx.Pen Colour any wx.NamedColour + 'width'= 1, - Pen width + 'style'= wx.SOLID, - wx.Pen style + 'legend'= '' - Line Legend to display + """ + PolyPoints.__init__(self, points, attr) + + def draw(self, dc, printerScale, coord= None): + colour = self.attributes['colour'] + width = self.attributes['width'] * printerScale + style= self.attributes['style'] + pen = wx.Pen(wx.NamedColour(colour), width, style) + pen.SetCap(wx.CAP_BUTT) + dc.SetPen(pen) + if coord == None: + dc.DrawLines(self.scaled) + else: + dc.DrawLines(coord) # draw legend line + + def getSymExtent(self, printerScale): + """Width and Height of Marker""" + h= self.attributes['width'] * printerScale + w= 5 * h + return (w,h) + + +class PolyMarker(PolyPoints): + """Class to define marker type and style + - All methods except __init__ are private. + """ + + _attributes = {'colour': 'black', + 'width': 1, + 'size': 2, + 'fillcolour': None, + 'fillstyle': wx.SOLID, + 'marker': 'circle', + 'legend': ''} + + def __init__(self, points, **attr): + """Creates PolyMarker object + points - sequence (array, tuple or list) of (x,y) points + **attr - key word attributes + Defaults: + 'colour'= 'black', - wx.Pen Colour any wx.NamedColour + 'width'= 1, - Pen width + 'size'= 2, - Marker size + 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour + 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill) + 'marker'= 'circle' - Marker shape + 'legend'= '' - Marker Legend to display + + Marker Shapes: + - 'circle' + - 'dot' + - 'square' + - 'triangle' + - 'triangle_down' + - 'cross' + - 'plus' + """ + + PolyPoints.__init__(self, points, attr) + + def draw(self, dc, printerScale, coord= None): + colour = self.attributes['colour'] + width = self.attributes['width'] * printerScale + size = self.attributes['size'] * printerScale + fillcolour = self.attributes['fillcolour'] + fillstyle = self.attributes['fillstyle'] + marker = self.attributes['marker'] + + dc.SetPen(wx.Pen(wx.NamedColour(colour), width)) + if fillcolour: + dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle)) + else: + dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle)) + if coord == None: + self._drawmarkers(dc, self.scaled, marker, size) + else: + self._drawmarkers(dc, coord, marker, size) # draw legend marker + + def getSymExtent(self, printerScale): + """Width and Height of Marker""" + s= 5*self.attributes['size'] * printerScale + return (s,s) + + def _drawmarkers(self, dc, coords, marker,size=1): + f = eval('self._' +marker) + f(dc, coords, size) + + def _circle(self, dc, coords, size=1): + fact= 2.5*size + wh= 5.0*size + rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh] + rect[:,0:2]= coords-[fact,fact] + dc.DrawEllipseList(rect.astype(_numpy.int32)) + + def _dot(self, dc, coords, size=1): + dc.DrawPointList(coords) + + def _square(self, dc, coords, size=1): + fact= 2.5*size + wh= 5.0*size + rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh] + rect[:,0:2]= coords-[fact,fact] + dc.DrawRectangleList(rect.astype(_numpy.int32)) + + def _triangle(self, dc, coords, size=1): + shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)] + poly= _numpy.repeat(coords,3) + poly.shape= (len(coords),3,2) + poly += shape + dc.DrawPolygonList(poly.astype(_numpy.int32)) + + def _triangle_down(self, dc, coords, size=1): + shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)] + poly= _numpy.repeat(coords,3) + poly.shape= (len(coords),3,2) + poly += shape + dc.DrawPolygonList(poly.astype(_numpy.int32)) + + def _cross(self, dc, coords, size=1): + fact= 2.5*size + for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]: + lines= _numpy.concatenate((coords,coords),axis=1)+f + dc.DrawLineList(lines.astype(_numpy.int32)) + + def _plus(self, dc, coords, size=1): + fact= 2.5*size + for f in [[-fact,0,fact,0],[0,-fact,0,fact]]: + lines= _numpy.concatenate((coords,coords),axis=1)+f + dc.DrawLineList(lines.astype(_numpy.int32)) + +class PlotGraphics: + """Container to hold PolyXXX objects and graph labels + - All methods except __init__ are private. + """ + + def __init__(self, objects, title='', xLabel='', yLabel= ''): + """Creates PlotGraphics object + objects - list of PolyXXX objects to make graph + title - title shown at top of graph + xLabel - label shown on x-axis + yLabel - label shown on y-axis + """ + if type(objects) not in [list,tuple]: + raise TypeError, "objects argument should be list or tuple" + self.objects = objects + self.title= title + self.xLabel= xLabel + self.yLabel= yLabel + + def boundingBox(self): + p1, p2 = self.objects[0].boundingBox() + for o in self.objects[1:]: + p1o, p2o = o.boundingBox() + p1 = _numpy.minimum(p1, p1o) + p2 = _numpy.maximum(p2, p2o) + return p1, p2 + + def scaleAndShift(self, scale=(1,1), shift=(0,0)): + for o in self.objects: + o.scaleAndShift(scale, shift) + + def setPrinterScale(self, scale): + """Thickens up lines and markers only for printing""" + self.printerScale= scale + + def setXLabel(self, xLabel= ''): + """Set the X axis label on the graph""" + self.xLabel= xLabel + + def setYLabel(self, yLabel= ''): + """Set the Y axis label on the graph""" + self.yLabel= yLabel + + def setTitle(self, title= ''): + """Set the title at the top of graph""" + self.title= title + + def getXLabel(self): + """Get x axis label string""" + return self.xLabel + + def getYLabel(self): + """Get y axis label string""" + return self.yLabel + + def getTitle(self, title= ''): + """Get the title at the top of graph""" + return self.title + + def draw(self, dc): + for o in self.objects: + #t=_time.clock() # profile info + o.draw(dc, self.printerScale) + #dt= _time.clock()-t + #print o, "time=", dt + + def getSymExtent(self, printerScale): + """Get max width and height of lines and markers symbols for legend""" + symExt = self.objects[0].getSymExtent(printerScale) + for o in self.objects[1:]: + oSymExt = o.getSymExtent(printerScale) + symExt = _numpy.maximum(symExt, oSymExt) + return symExt + + def getLegendNames(self): + """Returns list of legend names""" + lst = [None]*len(self) + for i in range(len(self)): + lst[i]= self.objects[i].getLegend() + return lst + + def __len__(self): + return len(self.objects) + + def __getitem__(self, item): + return self.objects[item] + + +#------------------------------------------------------------------------------- +# Main window that you will want to import into your application. + +class PlotCanvas(wx.Window): + """Subclass of a wx.Window to allow simple general plotting + of data with zoom, labels, and automatic axis scaling.""" + + def __init__(self, parent, id = -1, pos=wx.DefaultPosition, + size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""): + + self.use_persistence=False + self.alpha=0.3 + self.decimation=10 + self.decim_counter=0 + """Constucts a window, which can be a child of a frame, dialog or + any other non-control window""" + + wx.Window.__init__(self, parent, id, pos, size, style, name) + self.border = (1,1) + + self.SetBackgroundColour("white") + + # Create some mouse events for zooming + self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown) + + # set curser as cross-hairs + self.SetCursor(wx.CROSS_CURSOR) + + # Things for printing + self.print_data = wx.PrintData() + self.print_data.SetPaperId(wx.PAPER_LETTER) + self.print_data.SetOrientation(wx.LANDSCAPE) + self.pageSetupData= wx.PageSetupDialogData() + self.pageSetupData.SetMarginBottomRight((25,25)) + self.pageSetupData.SetMarginTopLeft((25,25)) + self.pageSetupData.SetPrintData(self.print_data) + self.printerScale = 1 + self.parent= parent + + # Zooming variables + self._zoomInFactor = 0.5 + self._zoomOutFactor = 2 + self._zoomCorner1= _numpy.array([0.0, 0.0]) # left mouse down corner + self._zoomCorner2= _numpy.array([0.0, 0.0]) # left mouse up corner + self._zoomEnabled= False + self._hasDragged= False + + # Drawing Variables + self.last_draw = None + self._pointScale= 1 + self._pointShift= 0 + self._xSpec= 'auto' + self._ySpec= 'auto' + self._gridEnabled= False + self._legendEnabled= False + self._xUseScopeTicks= False + + # Fonts + self._fontCache = {} + self._fontSizeAxis= 10 + self._fontSizeTitle= 15 + self._fontSizeLegend= 7 + + # pointLabels + self._pointLabelEnabled= False + self.last_PointLabel= None + self._pointLabelFunc= None + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) + + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_SIZE, self.OnSize) + # OnSize called to make sure the buffer is initialized. + # This might result in OnSize getting called twice on some + # platforms at initialization, but little harm done. + self.OnSize(None) # sets the initial size based on client size + # UNCONDITIONAL, needed to create self._Buffer + + + def set_use_persistence(self, enable): + self.use_persistence = enable + + def set_persist_alpha(self, persist_alpha): + self.alpha = persist_alpha + + + # SaveFile + def SaveFile(self, fileName= ''): + """Saves the file to the type specified in the extension. If no file + name is specified a dialog box is provided. Returns True if sucessful, + otherwise False. + + .bmp Save a Windows bitmap file. + .xbm Save an X bitmap file. + .xpm Save an XPM bitmap file. + .png Save a Portable Network Graphics file. + .jpg Save a Joint Photographic Experts Group file. + """ + if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']: + dlg1 = wx.FileDialog( + self, + "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "", + "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg", + wx.SAVE|wx.OVERWRITE_PROMPT + ) + try: + while 1: + if dlg1.ShowModal() == wx.ID_OK: + fileName = dlg1.GetPath() + # Check for proper exension + if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']: + dlg2 = wx.MessageDialog(self, 'File name extension\n' + 'must be one of\n' + 'bmp, xbm, xpm, png, or jpg', + 'File Name Error', wx.OK | wx.ICON_ERROR) + try: + dlg2.ShowModal() + finally: + dlg2.Destroy() + else: + break # now save file + else: # exit without saving + return False + finally: + dlg1.Destroy() + + # File name has required extension + fType = _string.lower(fileName[-3:]) + if fType == "bmp": + tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file. + elif fType == "xbm": + tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file. + elif fType == "xpm": + tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file. + elif fType == "jpg": + tp= wx.BITMAP_TYPE_JPEG # Save a JPG file. + else: + tp= wx.BITMAP_TYPE_PNG # Save a PNG file. + # Save Bitmap + res= self._Buffer.SaveFile(fileName, tp) + return res + + def PageSetup(self): + """Brings up the page setup dialog""" + data = self.pageSetupData + data.SetPrintData(self.print_data) + dlg = wx.PageSetupDialog(self.parent, data) + try: + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData + # updates page parameters from dialog + self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight()) + self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft()) + self.pageSetupData.SetPrintData(data.GetPrintData()) + self.print_data=data.GetPrintData() # updates print_data + finally: + dlg.Destroy() + + def Printout(self, paper=None): + """Print current plot.""" + if paper != None: + self.print_data.SetPaperId(paper) + pdd = wx.PrintDialogData() + pdd.SetPrintData(self.print_data) + printer = wx.Printer(pdd) + out = PlotPrintout(self) + print_ok = printer.Print(self.parent, out) + if print_ok: + self.print_data = printer.GetPrintDialogData().GetPrintData() + out.Destroy() + + def PrintPreview(self): + """Print-preview current plot.""" + printout = PlotPrintout(self) + printout2 = PlotPrintout(self) + self.preview = wx.PrintPreview(printout, printout2, self.print_data) + if not self.preview.Ok(): + wx.MessageDialog(self, "Print Preview failed.\n" \ + "Check that default printer is configured\n", \ + "Print error", wx.OK|wx.CENTRE).ShowModal() + self.preview.SetZoom(40) + # search up tree to find frame instance + frameInst= self + while not isinstance(frameInst, wx.Frame): + frameInst= frameInst.GetParent() + frame = wx.PreviewFrame(self.preview, frameInst, "Preview") + frame.Initialize() + frame.SetPosition(self.GetPosition()) + frame.SetSize((600,550)) + frame.Centre(wx.BOTH) + frame.Show(True) + + def SetFontSizeAxis(self, point= 10): + """Set the tick and axis label font size (default is 10 point)""" + self._fontSizeAxis= point + + def GetFontSizeAxis(self): + """Get current tick and axis label font size in points""" + return self._fontSizeAxis + + def SetFontSizeTitle(self, point= 15): + """Set Title font size (default is 15 point)""" + self._fontSizeTitle= point + + def GetFontSizeTitle(self): + """Get current Title font size in points""" + return self._fontSizeTitle + + def SetFontSizeLegend(self, point= 7): + """Set Legend font size (default is 7 point)""" + self._fontSizeLegend= point + + def GetFontSizeLegend(self): + """Get current Legend font size in points""" + return self._fontSizeLegend + + def SetEnableZoom(self, value): + """Set True to enable zooming.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._zoomEnabled= value + + def GetEnableZoom(self): + """True if zooming enabled.""" + return self._zoomEnabled + + def SetEnableGrid(self, value): + """Set True to enable grid.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._gridEnabled= value + self.Redraw() + + def GetEnableGrid(self): + """True if grid enabled.""" + return self._gridEnabled + + def SetEnableLegend(self, value): + """Set True to enable legend.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._legendEnabled= value + self.Redraw() + + def GetEnableLegend(self): + """True if Legend enabled.""" + return self._legendEnabled + + def SetEnablePointLabel(self, value): + """Set True to enable pointLabel.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + self._pointLabelEnabled= value + self.Redraw() #will erase existing pointLabel if present + self.last_PointLabel = None + + def GetEnablePointLabel(self): + """True if pointLabel enabled.""" + return self._pointLabelEnabled + + def SetPointLabelFunc(self, func): + """Sets the function with custom code for pointLabel drawing + ******** more info needed *************** + """ + self._pointLabelFunc= func + + def GetPointLabelFunc(self): + """Returns pointLabel Drawing Function""" + return self._pointLabelFunc + + def Reset(self): + """Unzoom the plot.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + self.Draw(self.last_draw[0]) + + def ScrollRight(self, units): + """Move view right number of axis units.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + xAxis= (xAxis[0]+units, xAxis[1]+units) + self.Draw(graphics,xAxis,yAxis) + + def ScrollUp(self, units): + """Move view up number of axis units.""" + self.last_PointLabel = None #reset pointLabel + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + yAxis= (yAxis[0]+units, yAxis[1]+units) + self.Draw(graphics,xAxis,yAxis) + + def GetXY(self,event): + """Takes a mouse event and returns the XY user axis values.""" + x,y= self.PositionScreenToUser(event.GetPosition()) + return x,y + + def PositionUserToScreen(self, pntXY): + """Converts User position to Screen Coordinates""" + userPos= _numpy.array(pntXY) + x,y= userPos * self._pointScale + self._pointShift + return x,y + + def PositionScreenToUser(self, pntXY): + """Converts Screen position to User Coordinates""" + screenPos= _numpy.array(pntXY) + x,y= (screenPos-self._pointShift)/self._pointScale + return x,y + + def SetXSpec(self, type= 'auto'): + """xSpec- defines x axis type. Can be 'none', 'min' or 'auto' + where: + 'none' - shows no axis or tick mark values + 'min' - shows min bounding box values + 'auto' - rounds axis range to sensible values + """ + self._xSpec= type + + def SetYSpec(self, type= 'auto'): + """ySpec- defines x axis type. Can be 'none', 'min' or 'auto' + where: + 'none' - shows no axis or tick mark values + 'min' - shows min bounding box values + 'auto' - rounds axis range to sensible values + """ + self._ySpec= type + + def GetXSpec(self): + """Returns current XSpec for axis""" + return self._xSpec + + def GetYSpec(self): + """Returns current YSpec for axis""" + return self._ySpec + + def GetXMaxRange(self): + """Returns (minX, maxX) x-axis range for displayed graph""" + graphics= self.last_draw[0] + p1, p2 = graphics.boundingBox() # min, max points of graphics + xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units + return xAxis + + def GetYMaxRange(self): + """Returns (minY, maxY) y-axis range for displayed graph""" + graphics= self.last_draw[0] + p1, p2 = graphics.boundingBox() # min, max points of graphics + yAxis = self._axisInterval(self._ySpec, p1[1], p2[1]) + return yAxis + + def GetXCurrentRange(self): + """Returns (minX, maxX) x-axis for currently displayed portion of graph""" + return self.last_draw[1] + + def GetYCurrentRange(self): + """Returns (minY, maxY) y-axis for currently displayed portion of graph""" + return self.last_draw[2] + + def SetXUseScopeTicks(self, v=False): + """Always 10 divisions, no labels""" + self._xUseScopeTicks = v + + def GetXUseScopeTicks(self): + return self._xUseScopeTicks + + def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, step=None): + """Draw objects in graphics with specified x and y axis. + graphics- instance of PlotGraphics with list of PolyXXX objects + xAxis - tuple with (min, max) axis range to view + yAxis - same as xAxis + dc - drawing context - doesn't have to be specified. + If it's not, the offscreen buffer is used + """ + # check Axis is either tuple or none + if type(xAxis) not in [type(None),tuple]: + raise TypeError, "xAxis should be None or (minX,maxX)" + if type(yAxis) not in [type(None),tuple]: + raise TypeError, "yAxis should be None or (minY,maxY)" + + # check case for axis = (a,b) where a==b caused by improper zooms + if xAxis != None: + if xAxis[0] == xAxis[1]: + return + if yAxis != None: + if yAxis[0] == yAxis[1]: + return + + if dc == None: + # sets new dc and clears it + if self.use_persistence: + dc = wx.MemoryDC() + dc.SelectObject(self._Buffer) + dc.Clear() + else: + dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer) + dc.Clear() + + dc.BeginDrawing() + # dc.Clear() + + + + # set font size for every thing but title and legend + dc.SetFont(self._getFont(self._fontSizeAxis)) + + # sizes axis to axis type, create lower left and upper right corners of plot + if xAxis == None or yAxis == None: + # One or both axis not specified in Draw + p1, p2 = graphics.boundingBox() # min, max points of graphics + if xAxis == None: + xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units + if yAxis == None: + yAxis = self._axisInterval(self._ySpec, p1[1], p2[1]) + # Adjust bounding box for axis spec + p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin) + p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax) + else: + # Both axis specified in Draw + p1= _numpy.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin) + p2= _numpy.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax) + + self.last_draw = (graphics, xAxis, yAxis) # saves most recient values + + if False: + ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2) + #dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) #wx.SOLID wx.TRANSPARENT ) ) + #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT + dc.DrawRectangle( ptx,pty, rectWidth,rectHeight) + #dc.SetBrush(wx.Brush( wx.WHITE, wx.SOLID ) ) + #dc.SetLogicalFunction(wx.COPY) + + # Get ticks and textExtents for axis if required + if self._xSpec is not 'none': + if self._xUseScopeTicks: + xticks = self._scope_ticks(xAxis[0], xAxis[1]) + else: + xticks = self._ticks(xAxis[0], xAxis[1]) + xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis + else: + xticks = None + xTextExtent= (0,0) # No text for ticks + if self._ySpec is not 'none': + yticks = self._ticks(yAxis[0], yAxis[1], step) + yTextExtentBottom= dc.GetTextExtent(yticks[0][1]) + yTextExtentTop = dc.GetTextExtent(yticks[-1][1]) + yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]), + max(yTextExtentBottom[1],yTextExtentTop[1])) + else: + yticks = None + yTextExtent= (0,0) # No text for ticks + + # TextExtents for Title and Axis Labels + titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics) + + # TextExtents for Legend + legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics) + + # room around graph area + rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width + lhsW= yTextExtent[0]+ yLabelWH[1] + bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1] + topH= yTextExtent[1]/2. + titleWH[1] + textSize_scale= _numpy.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size + textSize_shift= _numpy.array([lhsW, bottomH]) # shift plot area by this amount + + # drawing title and labels text + dc.SetFont(self._getFont(self._fontSizeTitle)) + titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2., + self.plotbox_origin[1]- self.plotbox_size[1]) + dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1]) + dc.SetFont(self._getFont(self._fontSizeAxis)) + xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2., + self.plotbox_origin[1]- xLabelWH[1]) + dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1]) + yLabelPos= (self.plotbox_origin[0], + self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.) + if graphics.getYLabel(): # bug fix for Linux + dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90) + + # drawing legend makers and text + if self._legendEnabled: + self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt) + + # allow for scaling and shifting plotted points + scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _numpy.array((1,-1)) + shift = -p1*scale + self.plotbox_origin + textSize_shift * _numpy.array((1,-1)) + self._pointScale= scale # make available for mouse events + self._pointShift= shift + + #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT + self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks) + #dc.SetLogicalFunction(wx.COPY) + + graphics.scaleAndShift(scale, shift) + graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing + + # set clipping area so drawing does not occur outside axis box + ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2) + dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight) + # Draw the lines and markers + #start = _time.clock() + + graphics.draw(dc) + # print "entire graphics drawing took: %f second"%(_time.clock() - start) + # remove the clipping region + dc.DestroyClippingRegion() + dc.EndDrawing() + + + if self.use_persistence: + dc=None + self._Buffer.CopyToBuffer(self._Bufferarray) #, format=wx.BitmapBufferFormat_RGB, stride=-1) + ## do the IIR filter + alpha_int=int(float(self.alpha*256)) + if True: + _numpy.add(self._Bufferarray,0,self._Buffer3array) + _numpy.multiply(self._Buffer3array,alpha_int,self._Buffer3array) + _numpy.multiply(self._Buffer2array,(256-alpha_int),self._Buffer2array) + _numpy.add(self._Buffer3array,self._Buffer2array,self._Buffer2array) + _numpy.right_shift(self._Buffer2array,8,self._Buffer2array) + elif False: + self._Buffer2array=(self._Bufferarray.astype(_numpy.uint32) *alpha_int + self._Buffer2array*(256-alpha_int)).__rshift__(8) + elif False: + self._Buffer2array *=(256-alpha_int) + self._Buffer2array +=self._Bufferarray.astype(_numpy.uint32)*alpha_int + self._Buffer2array /=256 + + ##copy back to image buffer + self._Buffer2.CopyFromBuffer(self._Buffer2array.astype(_numpy.uint8)) #, format=wx.BitmapBufferFormat_RGB, stride=-1) + + #draw to the screen + #self.decim_counter=self.decim_counter+1 + if True: #self.decim_counter>self.decimation: + #self.decim_counter=0 + dc2 = wx.ClientDC( self ) + dc2.BeginDrawing() + dc2.DrawBitmap(self._Buffer2, 0, 0, False) + #dc2.DrawBitmap(self._Buffer, 0, 0, False) + dc2.EndDrawing() + + def Redraw(self, dc= None): + """Redraw the existing plot.""" + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + self.Draw(graphics,xAxis,yAxis,dc) + + def Clear(self): + """Erase the window.""" + self.last_PointLabel = None #reset pointLabel + dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer) + dc.Clear() + self.last_draw = None + + def Zoom(self, Center, Ratio): + """ Zoom on the plot + Centers on the X,Y coords given in Center + Zooms by the Ratio = (Xratio, Yratio) given + """ + self.last_PointLabel = None #reset maker + x,y = Center + if self.last_draw != None: + (graphics, xAxis, yAxis) = self.last_draw + w = (xAxis[1] - xAxis[0]) * Ratio[0] + h = (yAxis[1] - yAxis[0]) * Ratio[1] + xAxis = ( x - w/2, x + w/2 ) + yAxis = ( y - h/2, y + h/2 ) + self.Draw(graphics, xAxis, yAxis) + + def GetClosestPoints(self, pntXY, pointScaled= True): + """Returns list with + [curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + list for each curve. + Returns [] if no curves are being plotted. + + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + if self.last_draw == None: + #no graph available + return [] + graphics, xAxis, yAxis= self.last_draw + l = [] + for curveNum,obj in enumerate(graphics): + #check there are points in the curve + if len(obj.points) == 0: + continue #go to next obj + #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled) + l.append(cn) + return l + + def GetClosetPoint(self, pntXY, pointScaled= True): + """Returns list with + [curveNumber, legend, index of closest point, pointXY, scaledXY, distance] + list for only the closest curve. + Returns [] if no curves are being plotted. + + x, y in user coords + if pointScaled == True based on screen coords + if pointScaled == False based on user coords + """ + #closest points on screen based on screen scaling (pointScaled= True) + #list [curveNumber, index, pointXY, scaledXY, distance] for each curve + closestPts= self.GetClosestPoints(pntXY, pointScaled) + if closestPts == []: + return [] #no graph present + #find one with least distance + dists = [c[-1] for c in closestPts] + mdist = min(dists) #Min dist + i = dists.index(mdist) #index for min dist + return closestPts[i] #this is the closest point on closest curve + + def UpdatePointLabel(self, mDataDict): + """Updates the pointLabel point on screen with data contained in + mDataDict. + + mDataDict will be passed to your function set by + SetPointLabelFunc. It can contain anything you + want to display on the screen at the scaledXY point + you specify. + + This function can be called from parent window with onClick, + onMotion events etc. + """ + if self.last_PointLabel != None: + #compare pointXY + if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]: + #closest changed + self._drawPointLabel(self.last_PointLabel) #erase old + self._drawPointLabel(mDataDict) #plot new + else: + #just plot new with no erase + self._drawPointLabel(mDataDict) #plot new + #save for next erase + self.last_PointLabel = mDataDict + + # event handlers ********************************** + def OnMotion(self, event): + if self._zoomEnabled and event.LeftIsDown(): + if self._hasDragged: + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old + else: + self._hasDragged= True + self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event) + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new + + def OnMouseLeftDown(self,event): + self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event) + + def OnMouseLeftUp(self, event): + if self._zoomEnabled: + if self._hasDragged == True: + self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old + self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event) + self._hasDragged = False # reset flag + minX, minY= _numpy.minimum( self._zoomCorner1, self._zoomCorner2) + maxX, maxY= _numpy.maximum( self._zoomCorner1, self._zoomCorner2) + self.last_PointLabel = None #reset pointLabel + if self.last_draw != None: + self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None) + #else: # A box has not been drawn, zoom in on a point + ## this interfered with the double click, so I've disables it. + # X,Y = self.GetXY(event) + # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) ) + + def OnMouseDoubleClick(self,event): + if self._zoomEnabled: + self.Reset() + + def OnMouseRightDown(self,event): + if self._zoomEnabled: + X,Y = self.GetXY(event) + self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) ) + + def OnPaint(self, event): + # All that is needed here is to draw the buffer to screen + if self.last_PointLabel != None: + self._drawPointLabel(self.last_PointLabel) #erase old + self.last_PointLabel = None + + #paint current buffer to screen + dc = wx.BufferedPaintDC(self, self._Buffer) + + def OnSize(self,event): + # The Buffer init is done here, to make sure the buffer is always + # the same size as the Window + Size = self.GetClientSize() + + # Make new offscreen bitmap: this bitmap will always have the + # current drawing in it, so it can be used to save the image to + # a file, or whatever. + self._Buffer = wx.EmptyBitmap(Size[0],Size[1],24) + + + if True: #self.use_persistence: + #self._Bufferarray = _numpy.zeros((Size[0], Size[1],3), dtype=_numpy.uint8) + self._Bufferarray = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint8) + + # Make new second offscreen bitmap: this bitmap will always have the + # last drawing in it, so it can be used to do display time dependent processing + # like averaging (IIR) or show differences between updates + self._Buffer2 = wx.EmptyBitmap(Size[0],Size[1],24) + # now the extra buffers for the IIR processing + # note the different datatype uint32 + self._Buffer2array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float + self._Buffer3array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float + # optional you can set the ufunct buffer size to improve speed + #_numpy.setbufsize(16*((Size[0]* Size[1]*3)/16 +1)) + self._setSize() + + self.last_PointLabel = None #reset pointLabel + + if self.last_draw is None: + self.Clear() + else: + graphics, xSpec, ySpec = self.last_draw + self.Draw(graphics,xSpec,ySpec) + + def OnLeave(self, event): + """Used to erase pointLabel when mouse outside window""" + if self.last_PointLabel != None: + self._drawPointLabel(self.last_PointLabel) #erase old + self.last_PointLabel = None + + + # Private Methods ************************************************** + def _setSize(self, width=None, height=None): + """DC width and height.""" + if width == None: + (self.width,self.height) = self.GetClientSize() + else: + self.width, self.height= width,height + self.plotbox_size = 0.97*_numpy.array([self.width, self.height]) + xo = 0.5*(self.width-self.plotbox_size[0]) + yo = self.height-0.5*(self.height-self.plotbox_size[1]) + self.plotbox_origin = _numpy.array([xo, yo]) + + def _setPrinterScale(self, scale): + """Used to thicken lines and increase marker size for print out.""" + # line thickness on printer is very thin at 600 dot/in. Markers small + self.printerScale= scale + + def _printDraw(self, printDC): + """Used for printing.""" + if self.last_draw != None: + graphics, xSpec, ySpec= self.last_draw + self.Draw(graphics,xSpec,ySpec,printDC) + + def _drawPointLabel(self, mDataDict): + """Draws and erases pointLabels""" + width = self._Buffer.GetWidth() + height = self._Buffer.GetHeight() + tmp_Buffer = wx.EmptyBitmap(width,height) + dcs = wx.MemoryDC() + dcs.SelectObject(tmp_Buffer) + dcs.Clear() + dcs.BeginDrawing() + self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function + dcs.EndDrawing() + + dc = wx.ClientDC( self ) + #this will erase if called twice + dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst + + + def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt): + """Draws legend symbols and text""" + # top right hand corner of graph box is ref corner + trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1] + legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box + lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines + dc.SetFont(self._getFont(self._fontSizeLegend)) + for i in range(len(graphics)): + o = graphics[i] + s= i*lineHeight + if isinstance(o,PolyMarker): + # draw marker with legend + pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.) + o.draw(dc, self.printerScale, coord= _numpy.array([pnt])) + elif isinstance(o,PolyLine): + # draw line with legend + pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.) + pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.) + o.draw(dc, self.printerScale, coord= _numpy.array([pnt1,pnt2])) + else: + raise TypeError, "object is neither PolyMarker or PolyLine instance" + # draw legend txt + pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2) + dc.DrawText(o.getLegend(),pnt[0],pnt[1]) + dc.SetFont(self._getFont(self._fontSizeAxis)) # reset + + def _titleLablesWH(self, dc, graphics): + """Draws Title and labels and returns width and height for each""" + # TextExtents for Title and Axis Labels + dc.SetFont(self._getFont(self._fontSizeTitle)) + title= graphics.getTitle() + titleWH= dc.GetTextExtent(title) + dc.SetFont(self._getFont(self._fontSizeAxis)) + xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel() + xLabelWH= dc.GetTextExtent(xLabel) + yLabelWH= dc.GetTextExtent(yLabel) + return titleWH, xLabelWH, yLabelWH + + def _legendWH(self, dc, graphics): + """Returns the size in screen units for legend box""" + if self._legendEnabled != True: + legendBoxWH= symExt= txtExt= (0,0) + else: + # find max symbol size + symExt= graphics.getSymExtent(self.printerScale) + # find max legend text extent + dc.SetFont(self._getFont(self._fontSizeLegend)) + txtList= graphics.getLegendNames() + txtExt= dc.GetTextExtent(txtList[0]) + for txt in graphics.getLegendNames()[1:]: + txtExt= _numpy.maximum(txtExt,dc.GetTextExtent(txt)) + maxW= symExt[0]+txtExt[0] + maxH= max(symExt[1],txtExt[1]) + # padding .1 for lhs of legend box and space between lines + maxW= maxW* 1.1 + maxH= maxH* 1.1 * len(txtList) + dc.SetFont(self._getFont(self._fontSizeAxis)) + legendBoxWH= (maxW,maxH) + return (legendBoxWH, symExt, txtExt) + + def _drawRubberBand(self, corner1, corner2): + """Draws/erases rect box from corner1 to corner2""" + ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2) + # draw rectangle + dc = wx.ClientDC( self ) + dc.BeginDrawing() + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) ) + dc.SetLogicalFunction(wx.INVERT) + dc.DrawRectangle( ptx,pty, rectWidth,rectHeight) + dc.SetLogicalFunction(wx.COPY) + dc.EndDrawing() + + def _getFont(self,size): + """Take font size, adjusts if printing and returns wx.Font""" + s = size*self.printerScale + of = self.GetFont() + # Linux speed up to get font from cache rather than X font server + key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ()) + font = self._fontCache.get (key, None) + if font: + return font # yeah! cache hit + else: + font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight()) + self._fontCache[key] = font + return font + + + def _point2ClientCoord(self, corner1, corner2): + """Converts user point coords to client screen int coords x,y,width,height""" + c1= _numpy.array(corner1) + c2= _numpy.array(corner2) + # convert to screen coords + pt1= c1*self._pointScale+self._pointShift + pt2= c2*self._pointScale+self._pointShift + # make height and width positive + pul= _numpy.minimum(pt1,pt2) # Upper left corner + plr= _numpy.maximum(pt1,pt2) # Lower right corner + rectWidth, rectHeight= plr-pul + ptx,pty= pul + return ptx, pty, rectWidth, rectHeight + + def _axisInterval(self, spec, lower, upper): + """Returns sensible axis range for given spec""" + if spec == 'none' or spec == 'min': + if lower == upper: + return lower-0.5, upper+0.5 + else: + return lower, upper + elif spec == 'auto': + range = upper-lower + # if range == 0.: + if abs(range) < 1e-36: + return lower-0.5, upper+0.5 + log = _numpy.log10(range) + power = _numpy.floor(log) + fraction = log-power + if fraction <= 0.05: + power = power-1 + grid = 10.**power + lower = lower - lower % grid + mod = upper % grid + if mod != 0: + upper = upper - mod + grid + return lower, upper + elif type(spec) == type(()): + lower, upper = spec + if lower <= upper: + return lower, upper + else: + return upper, lower + else: + raise ValueError, str(spec) + ': illegal axis specification' + + def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks): + + penWidth= self.printerScale # increases thickness for printing only + dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth)) + + # set length of tick marks--long ones make grid + if self._gridEnabled: + x,y,width,height= self._point2ClientCoord(p1,p2) + yTickLength= width/2.0 +1 + xTickLength= height/2.0 +1 + else: + yTickLength= 3 * self.printerScale # lengthens lines for printing + xTickLength= 3 * self.printerScale + + if self._xSpec is not 'none': + lower, upper = p1[0],p2[0] + text = 1 + for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths + a1 = scale*_numpy.array([lower, y])+shift + a2 = scale*_numpy.array([upper, y])+shift + dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line + for x, label in xticks: + pt = scale*_numpy.array([x, y])+shift + dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units + if text: + dc.DrawText(label,pt[0],pt[1]) + text = 0 # axis values not drawn on top side + + if self._ySpec is not 'none': + lower, upper = p1[1],p2[1] + text = 1 + h = dc.GetCharHeight() + for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]: + a1 = scale*_numpy.array([x, lower])+shift + a2 = scale*_numpy.array([x, upper])+shift + dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) + for y, label in yticks: + pt = scale*_numpy.array([x, y])+shift + dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1]) + if text: + dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0], + pt[1]-0.5*h) + text = 0 # axis values not drawn on right side + + def _ticks(self, lower, upper, step=None): + ideal = (upper-lower)/7. + log = _numpy.log10(ideal) + power = _numpy.floor(log) + fraction = log-power + factor = 1. + error = fraction + for f, lf in self._multiples: + e = _numpy.fabs(fraction-lf) + if e < error: + error = e + factor = f + grid = factor * 10.**power + if power > 4 or power < -4: + format = '%+7.1e' + elif power >= 0: + digits = max(1, int(power)) + format = '%' + `digits`+'.0f' + else: + digits = -int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + #force grid when step is not None + if step is not None: grid = step + ticks = [] + t = -grid*_numpy.floor(-lower/grid) + while t <= upper: + if t == -0: t = 0 #remove neg zero condition + ticks.append( (t, format % (t,)) ) + t = t + grid + return ticks + + def _scope_ticks (self, lower, upper): + '''Always 10 divisions, no labels''' + grid = (upper - lower) / 10.0 + ticks = [] + t = lower + while t <= upper: + ticks.append( (t, "")) + t = t + grid + return ticks + + _multiples = [(2., _numpy.log10(2.)), (5., _numpy.log10(5.))] + + +#------------------------------------------------------------------------------- +# Used to layout the printer page + +class PlotPrintout(wx.Printout): + """Controls how the plot is made in printing and previewing""" + # Do not change method names in this class, + # we have to override wx.Printout methods here! + def __init__(self, graph): + """graph is instance of plotCanvas to be printed or previewed""" + wx.Printout.__init__(self) + self.graph = graph + + def HasPage(self, page): + if page == 1: + return True + else: + return False + + def GetPageInfo(self): + return (1, 1, 1, 1) # disable page numbers + + def OnPrintPage(self, page): + dc = self.GetDC() # allows using floats for certain functions +## print "PPI Printer",self.GetPPIPrinter() +## print "PPI Screen", self.GetPPIScreen() +## print "DC GetSize", dc.GetSize() +## print "GetPageSizePixels", self.GetPageSizePixels() + # Note PPIScreen does not give the correct number + # Calulate everything for printer and then scale for preview + PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h) + #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h) + dcSize= dc.GetSize() # DC size + pageSize= self.GetPageSizePixels() # page size in terms of pixcels + clientDcSize= self.graph.GetClientSize() + + # find what the margins are (mm) + margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft() + margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight() + + # calculate offset and scale for dc + pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in) + pixRight= margRightSize*PPIPrinter[0]/25.4 + pixTop= margTopSize*PPIPrinter[1]/25.4 + pixBottom= margBottomSize*PPIPrinter[1]/25.4 + + plotAreaW= pageSize[0]-(pixLeft+pixRight) + plotAreaH= pageSize[1]-(pixTop+pixBottom) + + # ratio offset and scale to screen size if preview + if self.IsPreview(): + ratioW= float(dcSize[0])/pageSize[0] + ratioH= float(dcSize[1])/pageSize[1] + pixLeft *= ratioW + pixTop *= ratioH + plotAreaW *= ratioW + plotAreaH *= ratioH + + # rescale plot to page or preview plot area + self.graph._setSize(plotAreaW,plotAreaH) + + # Set offset and scale + dc.SetDeviceOrigin(pixLeft,pixTop) + + # Thicken up pens and increase marker size for printing + ratioW= float(plotAreaW)/clientDcSize[0] + ratioH= float(plotAreaH)/clientDcSize[1] + aveScale= (ratioW+ratioH)/2 + self.graph._setPrinterScale(aveScale) # tickens up pens for printing + + self.graph._printDraw(dc) + # rescale back to original + self.graph._setSize() + self.graph._setPrinterScale(1) + self.graph.Redraw() #to get point label scale and shift correct + + return True + + + + +#--------------------------------------------------------------------------- +# if running standalone... +# +# ...a sample implementation using the above +# + +def _draw1Objects(): + # 100 points sin function, plotted as green circles + data1 = 2.*_numpy.pi*_numpy.arange(200)/200. + data1.shape = (100, 2) + data1[:,1] = _numpy.sin(data1[:,0]) + markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1) + + # 50 points cos function, plotted as red line + data1 = 2.*_numpy.pi*_numpy.arange(100)/100. + data1.shape = (50,2) + data1[:,1] = _numpy.cos(data1[:,0]) + lines = PolyLine(data1, legend= 'Red Line', colour='red') + + # A few more points... + pi = _numpy.pi + markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), + (3.*pi/4., -1)], legend='Cross Legend', colour='blue', + marker='cross') + + return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis") + +def _draw2Objects(): + # 100 points sin function, plotted as green dots + data1 = 2.*_numpy.pi*_numpy.arange(200)/200. + data1.shape = (100, 2) + data1[:,1] = _numpy.sin(data1[:,0]) + line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT) + + # 50 points cos function, plotted as red dot-dash + data1 = 2.*_numpy.pi*_numpy.arange(100)/100. + data1.shape = (50,2) + data1[:,1] = _numpy.cos(data1[:,0]) + line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH) + + # A few more points... + pi = _numpy.pi + markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.), + (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6, + fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH, + marker='square') + + return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles") + +def _draw3Objects(): + markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down', + 'cross', 'plus', 'circle'] + m=[] + for i in range(len(markerList)): + m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue', + marker=markerList[i])) + return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis") + +def _draw4Objects(): + # 25,000 point line + data1 = _numpy.arange(5e5,1e6,10) + data1.shape = (25000, 2) + line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5) + + # A few more points... + markers2 = PolyMarker(data1, legend='Square', colour='blue', + marker='square') + return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "") + +def _draw5Objects(): + # Empty graph with axis defined but no points/lines + points=[] + line1 = PolyLine(points, legend='Wide Line', colour='green', width=5) + return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y") + +def _draw6Objects(): + # Bar graph + points1=[(1,0), (1,10)] + line1 = PolyLine(points1, colour='green', legend='Feb.', width=10) + points1g=[(2,0), (2,4)] + line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10) + points1b=[(3,0), (3,6)] + line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10) + + points2=[(4,0), (4,12)] + line2 = PolyLine(points2, colour='Yellow', legend='May', width=10) + points2g=[(5,0), (5,8)] + line2g = PolyLine(points2g, colour='orange', legend='June', width=10) + points2b=[(6,0), (6,4)] + line2b = PolyLine(points2b, colour='brown', legend='July', width=10) + + return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b], + "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students") + + +class TestFrame(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, + wx.DefaultPosition, (600, 400)) + + # Now Create the menu bar and items + self.mainmenu = wx.MenuBar() + + menu = wx.Menu() + menu.Append(200, 'Page Setup...', 'Setup the printer page') + self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200) + + menu.Append(201, 'Print Preview...', 'Show the current plot on page') + self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201) + + menu.Append(202, 'Print...', 'Print the current plot') + self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202) + + menu.Append(203, 'Save Plot...', 'Save current plot') + self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203) + + menu.Append(205, 'E&xit', 'Enough of this already!') + self.Bind(wx.EVT_MENU, self.OnFileExit, id=205) + self.mainmenu.Append(menu, '&File') + + menu = wx.Menu() + menu.Append(206, 'Draw1', 'Draw plots1') + self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206) + menu.Append(207, 'Draw2', 'Draw plots2') + self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207) + menu.Append(208, 'Draw3', 'Draw plots3') + self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208) + menu.Append(209, 'Draw4', 'Draw plots4') + self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209) + menu.Append(210, 'Draw5', 'Draw plots5') + self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210) + menu.Append(260, 'Draw6', 'Draw plots6') + self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260) + + + menu.Append(211, '&Redraw', 'Redraw plots') + self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211) + menu.Append(212, '&Clear', 'Clear canvas') + self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212) + menu.Append(213, '&Scale', 'Scale canvas') + self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213) + menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214) + menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215) + menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220) + menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222) + + menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit') + self.Bind(wx.EVT_MENU,self.OnScrUp, id=225) + menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units') + self.Bind(wx.EVT_MENU,self.OnScrRt, id=230) + menu.Append(235, '&Plot Reset', 'Reset to original plot') + self.Bind(wx.EVT_MENU,self.OnReset, id=235) + + self.mainmenu.Append(menu, '&Plot') + + menu = wx.Menu() + menu.Append(300, '&About', 'About this thing...') + self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300) + self.mainmenu.Append(menu, '&Help') + + self.SetMenuBar(self.mainmenu) + + # A status bar to tell people what's happening + self.CreateStatusBar(1) + + self.client = PlotCanvas(self) + #define the function for drawing pointLabels + self.client.SetPointLabelFunc(self.DrawPointLabel) + # Create mouse event for showing cursor coords in status bar + self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) + # Show closest point when enabled + self.client.Bind(wx.EVT_MOTION, self.OnMotion) + + self.Show(True) + + def DrawPointLabel(self, dc, mDataDict): + """This is the fuction that defines how the pointLabels are plotted + dc - DC that will be passed + mDataDict - Dictionary of data that you want to use for the pointLabel + + As an example I have decided I want a box at the curve point + with some text information about the curve plotted below. + Any wxDC method can be used. + """ + # ---------- + dc.SetPen(wx.Pen(wx.BLACK)) + dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) + + sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point + dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point + px,py = mDataDict["pointXY"] + cNum = mDataDict["curveNum"] + pntIn = mDataDict["pIndex"] + legend = mDataDict["legend"] + #make a string to display + s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn) + dc.DrawText(s, sx , sy+1) + # ----------- + + def OnMouseLeftDown(self,event): + s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event) + self.SetStatusText(s) + event.Skip() #allows plotCanvas OnMouseLeftDown to be called + + def OnMotion(self, event): + #show closest point (when enbled) + if self.client.GetEnablePointLabel() == True: + #make up dict with info for the pointLabel + #I've decided to mark the closest point on the closest curve + dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True) + if dlst != []: #returns [] if none + curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst + #make up dictionary to pass to my user function (see DrawPointLabel) + mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\ + "pointXY":pointXY, "scaledXY":scaledXY} + #pass dict to update the pointLabel + self.client.UpdatePointLabel(mDataDict) + event.Skip() #go to next handler + + def OnFilePageSetup(self, event): + self.client.PageSetup() + + def OnFilePrintPreview(self, event): + self.client.PrintPreview() + + def OnFilePrint(self, event): + self.client.Printout() + + def OnSaveFile(self, event): + self.client.SaveFile() + + def OnFileExit(self, event): + self.Close() + + def OnPlotDraw1(self, event): + self.resetDefaults() + self.client.Draw(_draw1Objects()) + + def OnPlotDraw2(self, event): + self.resetDefaults() + self.client.Draw(_draw2Objects()) + + def OnPlotDraw3(self, event): + self.resetDefaults() + self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL)) + self.client.SetFontSizeAxis(20) + self.client.SetFontSizeLegend(12) + self.client.SetXSpec('min') + self.client.SetYSpec('none') + self.client.Draw(_draw3Objects()) + + def OnPlotDraw4(self, event): + self.resetDefaults() + drawObj= _draw4Objects() + self.client.Draw(drawObj) +## # profile +## start = _time.clock() +## for x in range(10): +## self.client.Draw(drawObj) +## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start) +## # profile end + + def OnPlotDraw5(self, event): + # Empty plot with just axes + self.resetDefaults() + drawObj= _draw5Objects() + # make the axis X= (0,5), Y=(0,10) + # (default with None is X= (-1,1), Y= (-1,1)) + self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10)) + + def OnPlotDraw6(self, event): + #Bar Graph Example + self.resetDefaults() + #self.client.SetEnableLegend(True) #turn on Legend + #self.client.SetEnableGrid(True) #turn on Grid + self.client.SetXSpec('none') #turns off x-axis scale + self.client.SetYSpec('auto') + self.client.Draw(_draw6Objects(), xAxis= (0,7)) + + def OnPlotRedraw(self,event): + self.client.Redraw() + + def OnPlotClear(self,event): + self.client.Clear() + + def OnPlotScale(self, event): + if self.client.last_draw != None: + graphics, xAxis, yAxis= self.client.last_draw + self.client.Draw(graphics,(1,3.05),(0,1)) + + def OnEnableZoom(self, event): + self.client.SetEnableZoom(event.IsChecked()) + + def OnEnableGrid(self, event): + self.client.SetEnableGrid(event.IsChecked()) + + def OnEnableLegend(self, event): + self.client.SetEnableLegend(event.IsChecked()) + + def OnEnablePointLabel(self, event): + self.client.SetEnablePointLabel(event.IsChecked()) + + def OnScrUp(self, event): + self.client.ScrollUp(1) + + def OnScrRt(self,event): + self.client.ScrollRight(2) + + def OnReset(self,event): + self.client.Reset() + + def OnHelpAbout(self, event): + from wx.lib.dialogs import ScrolledMessageDialog + about = ScrolledMessageDialog(self, __doc__, "About...") + about.ShowModal() + + def resetDefaults(self): + """Just to reset the fonts back to the PlotCanvas defaults""" + self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL)) + self.client.SetFontSizeAxis(10) + self.client.SetFontSizeLegend(7) + self.client.SetXSpec('auto') + self.client.SetYSpec('auto') + + +def __test(): + + class MyApp(wx.App): + def OnInit(self): + wx.InitAllImageHandlers() + frame = TestFrame(None, -1, "PlotCanvas") + #frame.Show(True) + self.SetTopWindow(frame) + return True + + + app = MyApp(0) + app.MainLoop() + +if __name__ == '__main__': + __test() |