X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/ecf0b9f98289581eb5c9ae0ef860db61d88ff040..b2bfee6caa9cc781c94da51587341ead28138869:/wxPython/wx/lib/plot.py diff --git a/wxPython/wx/lib/plot.py b/wxPython/wx/lib/plot.py index 1462407433..0e5d01f1ec 100644 --- a/wxPython/wx/lib/plot.py +++ b/wxPython/wx/lib/plot.py @@ -34,14 +34,6 @@ # - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve) # can be in either user coords or screen coords. # -# Nov 2004 Oliver Schoenborn (oliver.schoenborn@utoronto.ca) with -# valuable input from and testing by Gary) -# - Factored out "draw command" so extensions easier to implement: clean -# separation b/w user's "draw" (Draw) and internal "draw" (_draw) -# - Added ability to define your own ticks at PlotCanvas.Draw -# - Added better bar charts: PolyBar class and getBarTicksGen() -# - Put legend writing for a Poly* into Poly* class where it belongs -# - If no legend specified or exists, no gap created # """ @@ -100,20 +92,23 @@ import string as _string import time as _time import wx -# Needs Numeric or numarray +# Needs Numeric or numarray or NumPy try: - import Numeric as _Numeric + import numpy.oldnumeric as _Numeric except: try: import numarray as _Numeric #if numarray is used it is renamed Numeric except: - msg= """ - This module requires the Numeric 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, "Numeric or numarray not found. \n" + msg + try: + import Numeric as _Numeric + except: + msg= """ + This module requires the Numeric/numarray or NumPy module, + which could not be imported. It probably is not installed + (it's not part of the standard Python distribution). See the + Numeric Python site (http://numpy.scipy.org) for information on + downloading source or binaries.""" + raise ImportError, "Numeric,numarray or NumPy not found. \n" + msg @@ -126,7 +121,8 @@ class PolyPoints: """ def __init__(self, points, attr): - self.points = _Numeric.array(points) + self._points = _Numeric.array(points).astype(_Numeric.Float64) + self._logscale = (False, False) self.currentScale= (1,1) self.currentShift= (0,0) self.scaled = self.points @@ -137,12 +133,34 @@ class PolyPoints: raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys() self.attributes[name] = value + def setLogScale(self, logscale): + self._logscale = logscale + + def __getattr__(self, name): + if name == 'points': + if len(self._points)>0: + data = _Numeric.array(self._points,copy=True) + if self._logscale[0]: + data = self.log10(data, 0) + if self._logscale[1]: + data = self.log10(data, 1) + return data + else: + return self._points + else: + raise AttributeError, name + + def log10(self, data, ind): + data = _Numeric.compress(data[:,ind]>0,data,0) + data[:,ind] = _Numeric.log10(data[:,ind]) + return data + 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= _Numeric.array([-1,-1]) - maxXY= _Numeric.array([ 1, 1]) + minXY= _Numeric.array([-1.0,-1.0]) + maxXY= _Numeric.array([ 1.0, 1.0]) else: minXY= _Numeric.minimum.reduce(self.points) maxXY= _Numeric.maximum.reduce(self.points) @@ -159,20 +177,6 @@ class PolyPoints: self.currentShift= shift # else unchanged use the current scaling - def drawLegend(self, dc, printerScale, symWidth, hgap, textHeight, x0, y0): - legend = self.getLegend() - if legend != '': - self._drawLegendSym(dc, printerScale, symWidth, x0, y0) - dc.DrawText(legend, x0+symWidth+hgap, y0-textHeight/2) - return True - - return False - - def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0): - # default: - pnt= (x0+symWidth/2., y0) - self.draw(dc, printerScale, coord= _Numeric.array([pnt])) - def getLegend(self): return self.attributes['legend'] @@ -223,6 +227,8 @@ class PolyLine(PolyPoints): colour = self.attributes['colour'] width = self.attributes['width'] * printerScale style= self.attributes['style'] + if not isinstance(colour, wx.Colour): + colour = wx.NamedColour(colour) pen = wx.Pen(colour, width, style) pen.SetCap(wx.CAP_BUTT) dc.SetPen(pen) @@ -231,11 +237,6 @@ class PolyLine(PolyPoints): else: dc.DrawLines(coord) # draw legend line - def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0): - pnt1= (x0, y0) - pnt2= (x0+symWidth/1.5, y0) - self.draw(dc, printerScale, coord= _Numeric.array([pnt1,pnt2])) - def getSymExtent(self, printerScale): """Width and Height of Marker""" h= self.attributes['width'] * printerScale @@ -264,7 +265,7 @@ class PolyMarker(PolyPoints): 'colour'= 'black', - wx.Pen Colour any wx.NamedColour 'width'= 1, - Pen width 'size'= 2, - Marker size - 'fillcolour'= same as colour, - wx.Brush Colour + '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 @@ -289,6 +290,11 @@ class PolyMarker(PolyPoints): fillstyle = self.attributes['fillstyle'] marker = self.attributes['marker'] + if colour and not isinstance(colour, wx.Colour): + colour = wx.NamedColour(colour) + if fillcolour and not isinstance(fillcolour, wx.Colour): + fillcolour = wx.NamedColour(fillcolour) + dc.SetPen(wx.Pen(colour, width)) if fillcolour: dc.SetBrush(wx.Brush(fillcolour,fillstyle)) @@ -351,148 +357,6 @@ class PolyMarker(PolyPoints): lines= _Numeric.concatenate((coords,coords),axis=1)+f dc.DrawLineList(lines.astype(_Numeric.Int32)) -class PolyBar(PolyPoints): - """Class to define one or more bars for bar chart. All bars in a - PolyBar share same style etc. - - All methods except __init__ are private. - """ - - _attributes = {'colour': 'black', - 'width': 1, - 'size': 2, - 'legend': '', - 'fillcolour': None, - 'fillstyle': wx.SOLID} - - def __init__(self, points, **attr): - """Creates several bars. - points - sequence (array, tuple or list) of (x,y) points - indicating *top left* corner of bar. Can also be - just one (x,y) tuple if labels attribute is just - a string (not a list) - **attr - key word attributes - Defaults: - 'colour'= wx.BLACK, - wx.Pen Colour - 'width'= 1, - Pen width - 'size'= 2, - Bar size - 'fillcolour'= same as colour, - wx.Brush Colour - 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill) - 'legend'= '' - string used for PolyBar legend - 'labels'= '' - string if only one point, or list of labels, one per point - Note that if no legend is given the label is used. - """ - barPoints = [(0,0)] # needed so height of bar can be determined - self.labels = [] - # add labels and points to above data members: - try: - labels = attr['labels'] - del attr['labels'] - except: - labels = None - if labels is None: # figure out if we have 1 point or many - try: - points[0][0] # ok: we have a seq of points - barPoints.extend(points) - except: # failed, so have just one point: - barPoints.append(points) - elif isinstance(labels, list) or isinstance(labels, tuple): - # labels is a sequence so points must be too, with same length: - self.labels.extend(labels) - barPoints.extend(points) - if len(labels) != len(points): - msg = "%d bar labels missing" % (len(points)-len(labels)) - raise ValueError, msg - else: # label given, but only one, so must be only one point: - barPoints.append(points) - self.labels.append(labels) - - PolyPoints.__init__(self, barPoints, 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'] - - dc.SetPen(wx.Pen(colour,int(width))) - if fillcolour: - dc.SetBrush(wx.Brush(fillcolour,fillstyle)) - else: - dc.SetBrush(wx.Brush(colour, fillstyle)) - if coord == None: - self._drawbar(dc, self.scaled, size) - else: - self._drawLegendMarker(dc, coord, size) #draw legend marker - - def _drawLegendMarker(self, dc, coord, size): - fact= 10 - wh= 2*fact - rect= _Numeric.zeros((len(coord),4),_Numeric.Float)+[0.0,0.0,wh,wh] - rect[:,0:2]= coord-[fact,fact] - dc.DrawRectangleList(rect.astype(_Numeric.Int32)) - - def offset(self, heights, howMany = 1, shift=0.25): - """Return a list of points, where x's are those of this bar, - shifted by shift*howMany (in caller's units, not pixel units), and - heights are given in heights.""" - points = [(point[0][0]+shift*howMany,point[1]) for point - in zip(self.points[1:], heights)] - return points - - def getSymExtent(self, printerScale): - """Width and Height of bar symbol""" - s= 5*self.attributes['size'] * printerScale - return (s,s) - - def getLegend(self): - """This returns a comma-separated list of bar labels (one string)""" - try: legendItem = self.attributes['legend'] - except: - legendItem = ", ".join(self.labels) - return legendItem - - def _drawbar(self, dc, coords, size=1): - width = 5.0*size - y0 = coords[0][1] - bars = [] - for coord in coords[1:]: - x = coord[0] # - width/2 not as good - y = coord[1] - height = int(y0) - int(y) - bars.append((int(x),int(y),int(width),int(height))) - #print x,y,width, height - dc.DrawRectangleList(bars) - - def getTicks(self): - """Get the list of (tick pos, label) pairs for this PolyBar. - It is unlikely you need this, but it is used by - getBarTicksGen.""" - ticks = [] - # remember to skip first point: - for point, label in zip(self.points[1:], self.labels): - ticks.append((point[0], label)) - return ticks - -def getBarTicksGen(*polyBars): - """Get a function that can be given as xTicks argument when - calling PolyCanvas.Draw() when plotting one or more PolyBar. - The returned function allows the bar chart to have the bars - labelled at the x axis. """ - ticks = [] - for polyBar in polyBars: - ticks.extend(polyBar.getTicks()) - - def tickGenerator(lower,upper,gg,ff): - tickPairs = [] - for tick in ticks: - if lower <= tick[0] < upper: - tickPairs.append(tick) - return tickPairs - - return tickGenerator - - class PlotGraphics: """Container to hold PolyXXX objects and graph labels - All methods except __init__ are private. @@ -512,6 +376,14 @@ class PlotGraphics: self.xLabel= xLabel self.yLabel= yLabel + def setLogScale(self, logscale): + if type(logscale) != tuple: + raise TypeError, 'logscale must be a tuple of bools, e.g. (False, False)' + if len(self.objects) == 0: + return + for o in self.objects: + o.setLogScale(logscale) + def boundingBox(self): p1, p2 = self.objects[0].boundingBox() for o in self.objects[1:]: @@ -584,30 +456,64 @@ class PlotGraphics: #------------------------------------------------------------------------------- # 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 +class PlotCanvas(wx.Panel): + """ + Subclass of a wx.Panel which holds two scrollbars and the actual + plotting canvas (self.canvas). It allows for 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= ""): - """Constucts a window, which can be a child of a frame, dialog or + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, name="plotCanvas"): + """Constructs a panel, which can be a child of a frame or any other non-control window""" - wx.Window.__init__(self, parent, id, pos, size, style, name) + wx.Panel.__init__(self, parent, id, pos, size, style, name) + + sizer = wx.FlexGridSizer(2,2,0,0) + self.canvas = wx.Window(self, -1) + self.sb_vert = wx.ScrollBar(self, -1, style=wx.SB_VERTICAL) + self.sb_vert.SetScrollbar(0,1000,1000,1000) + self.sb_hor = wx.ScrollBar(self, -1, style=wx.SB_HORIZONTAL) + self.sb_hor.SetScrollbar(0,1000,1000,1000) + + sizer.Add(self.canvas, 1, wx.EXPAND) + sizer.Add(self.sb_vert, 0, wx.EXPAND) + sizer.Add(self.sb_hor, 0, wx.EXPAND) + sizer.Add((0,0)) + + sizer.AddGrowableRow(0, 1) + sizer.AddGrowableCol(0, 1) + + self.sb_vert.Show(False) + self.sb_hor.Show(False) + + self.SetSizer(sizer) + self.Fit() + 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) + self.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) + self.canvas.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp) + self.canvas.Bind(wx.EVT_MOTION, self.OnMotion) + self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick) + self.canvas.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown) + + # scrollbar events + self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnScroll) + self.Bind(wx.EVT_SCROLL_PAGEUP, self.OnScroll) + self.Bind(wx.EVT_SCROLL_PAGEDOWN, self.OnScroll) + self.Bind(wx.EVT_SCROLL_LINEUP, self.OnScroll) + self.Bind(wx.EVT_SCROLL_LINEDOWN, self.OnScroll) # set curser as cross-hairs - self.SetCursor(wx.CROSS_CURSOR) - + self.canvas.SetCursor(wx.CROSS_CURSOR) + self.HandCursor = wx.CursorFromImage(getHandImage()) + self.GrabHandCursor = wx.CursorFromImage(getGrabHandImage()) + self.MagCursor = wx.CursorFromImage(getMagPlusImage()) + # Things for printing self.print_data = wx.PrintData() self.print_data.SetPaperId(wx.PAPER_LETTER) @@ -619,6 +525,19 @@ class PlotCanvas(wx.Window): self.printerScale = 1 self.parent= parent + # scrollbar variables + self._sb_ignore = False + self._adjustingSB = False + self._sb_xfullrange = 0 + self._sb_yfullrange = 0 + self._sb_xunit = 0 + self._sb_yunit = 0 + + self._dragEnabled = False + self._screenCoordinates = _Numeric.array([0.0, 0.0]) + + self._logscale = (False, False) + # Zooming variables self._zoomInFactor = 0.5 self._zoomOutFactor = 2 @@ -628,7 +547,7 @@ class PlotCanvas(wx.Window): self._hasDragged= False # Drawing Variables - self._drawCmd = None + self.last_draw = None self._pointScale= 1 self._pointShift= 0 self._xSpec= 'auto' @@ -646,15 +565,28 @@ class PlotCanvas(wx.Window): self._pointLabelEnabled= False self.last_PointLabel= None self._pointLabelFunc= None - self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) + self.canvas.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) - self.Bind(wx.EVT_PAINT, self.OnPaint) - self.Bind(wx.EVT_SIZE, self.OnSize) + self.canvas.Bind(wx.EVT_PAINT, self.OnPaint) + self.canvas.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. - if wx.Platform != "__WXMAC__": - self.OnSize(None) # sets the initial size based on client size + self.OnSize(None) # sets the initial size based on client size + + self._gridColour = wx.NamedColour('black') + + def SetCursor(self, cursor): + self.canvas.SetCursor(cursor) + + def GetGridColour(self): + return self._gridColour + + def SetGridColour(self, colour): + if isinstance(colour, wx.Colour): + self._gridColour = colour + else: + self._gridColour = wx.NamedColour(colour) # SaveFile @@ -725,7 +657,7 @@ class PlotCanvas(wx.Window): self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight()) self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft()) self.pageSetupData.SetPrintData(data.GetPrintData()) - self.print_data=data.GetPrintData() # updates print_data + self.print_data=wx.PrintData(data.GetPrintData()) # updates print_data finally: dlg.Destroy() @@ -733,13 +665,12 @@ class PlotCanvas(wx.Window): """Print current plot.""" if paper != None: self.print_data.SetPaperId(paper) - pdd = wx.PrintDialogData() - pdd.SetPrintData(self.print_data) + pdd = wx.PrintDialogData(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() + self.print_data = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) out.Destroy() def PrintPreview(self): @@ -763,6 +694,20 @@ class PlotCanvas(wx.Window): frame.Centre(wx.BOTH) frame.Show(True) + def setLogScale(self, logscale): + if type(logscale) != tuple: + raise TypeError, 'logscale must be a tuple of bools, e.g. (False, False)' + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + graphics.setLogScale(logscale) + self.last_draw = (graphics, None, None) + self.SetXSpec('min') + self.SetYSpec('min') + self._logscale = logscale + + def getLogScale(self): + return self._logscale + def SetFontSizeAxis(self, point= 10): """Set the tick and axis label font size (default is 10 point)""" self._fontSizeAxis= point @@ -787,10 +732,45 @@ class PlotCanvas(wx.Window): """Get current Legend font size in points""" return self._fontSizeLegend + def SetShowScrollbars(self, value): + """Set True to show scrollbars""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + if value == self.GetShowScrollbars(): + return + self.sb_vert.Show(value) + self.sb_hor.Show(value) + wx.CallAfter(self.Layout) + + def GetShowScrollbars(self): + """Set True to show scrollbars""" + return self.sb_vert.IsShown() + + def SetEnableDrag(self, value): + """Set True to enable drag.""" + if value not in [True,False]: + raise TypeError, "Value should be True or False" + if value: + if self.GetEnableZoom(): + self.SetEnableZoom(False) + self.SetCursor(self.HandCursor) + else: + self.SetCursor(wx.CROSS_CURSOR) + self._dragEnabled = value + + def GetEnableDrag(self): + return self._dragEnabled + def SetEnableZoom(self, value): """Set True to enable zooming.""" if value not in [True,False]: raise TypeError, "Value should be True or False" + if value: + if self.GetEnableDrag(): + self.SetEnableDrag(False) + self.SetCursor(self.MagCursor) + else: + self.SetCursor(wx.CROSS_CURSOR) self._zoomEnabled= value def GetEnableZoom(self): @@ -799,8 +779,8 @@ class PlotCanvas(wx.Window): def SetEnableGrid(self, value): """Set True to enable grid.""" - if value not in [True,False]: - raise TypeError, "Value should be True or False" + if value not in [True,False,'Horizontal','Vertical']: + raise TypeError, "Value should be True, False, Horizontal or Vertical" self._gridEnabled= value self.Redraw() @@ -844,25 +824,35 @@ class PlotCanvas(wx.Window): def Reset(self): """Unzoom the plot.""" self.last_PointLabel = None #reset pointLabel - if self.BeenDrawn(): - self._drawCmd.resetAxes(self._xSpec, self._ySpec) - self._draw() + 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.BeenDrawn(): - self._drawCmd.scrollAxisX(units, self._xSpec) - self._draw() + 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.BeenDrawn(): - self._drawCmd.scrollAxisY(units, self._ySpec) - self._draw() + 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): + """Wrapper around _getXY, which handles log scales""" + x,y = self._getXY(event) + if self.getLogScale()[0]: + x = _Numeric.power(10,x) + if self.getLogScale()[1]: + y = _Numeric.power(10,y) + return x,y - def GetXY(self,event): + def _getXY(self,event): """Takes a mouse event and returns the XY user axis values.""" x,y= self.PositionScreenToUser(event.GetPosition()) return x,y @@ -906,68 +896,88 @@ class PlotCanvas(wx.Window): return self._ySpec def GetXMaxRange(self): + xAxis = self._getXMaxRange() + if self.getLogScale()[0]: + xAxis = _Numeric.power(10,xAxis) + return xAxis + + def _getXMaxRange(self): """Returns (minX, maxX) x-axis range for displayed graph""" - return self._drawCmd.getXMaxRange() + 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): + yAxis = self._getYMaxRange() + if self.getLogScale()[1]: + yAxis = _Numeric.power(10,yAxis) + return yAxis + + def _getYMaxRange(self): """Returns (minY, maxY) y-axis range for displayed graph""" - return self._drawCmd.getYMaxRange() + 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): + xAxis = self._getXCurrentRange() + if self.getLogScale()[0]: + xAxis = _Numeric.power(10,xAxis) + return xAxis + + def _getXCurrentRange(self): """Returns (minX, maxX) x-axis for currently displayed portion of graph""" - return self._drawCmd.xAxis + return self.last_draw[1] def GetYCurrentRange(self): + yAxis = self._getYCurrentRange() + if self.getLogScale()[1]: + yAxis = _Numeric.power(10,yAxis) + return yAxis + + def _getYCurrentRange(self): """Returns (minY, maxY) y-axis for currently displayed portion of graph""" - return self._drawCmd.yAxis + return self.last_draw[2] + + def Draw(self, graphics, xAxis = None, yAxis = None, dc = None): + """Wrapper around _Draw, which handles log axes""" + + graphics.setLogScale(self.getLogScale()) - def BeenDrawn(self): - """Return true if Draw() has been called once, false otherwise.""" - return self._drawCmd is not None - - def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, - xTicks = None, yTicks = 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. - xTicks and yTicks - can be either - - a list of floats where the ticks should be, only visible - ticks will be used; - - a function that generates a list of (tick, label) pairs; - The function must be of form func(lower, upper, grid, - format), and the pairs must contain only ticks between lower and - upper. Function can use grid and format that are computed by - PlotCanvas, if desired, where grid is the spacing between ticks, - and format is the format string for how tick labels - will appear. See tickGeneratorDefault and - tickGeneratorFromList for examples of such functions. - """ # check Axis is either tuple or none if type(xAxis) not in [type(None),tuple]: - raise TypeError, "xAxis should be None or (minX,maxX)" + raise TypeError, "xAxis should be None or (minX,maxX)"+str(type(xAxis)) if type(yAxis) not in [type(None),tuple]: - raise TypeError, "yAxis should be None or (minY,maxY)" + raise TypeError, "yAxis should be None or (minY,maxY)"+str(type(xAxis)) - self._drawCmd = DrawCmd(graphics, xAxis, yAxis, - self._xSpec, self._ySpec, - xTicks, yTicks) - self._draw(dc) - - def _draw(self, dc=None): - """Implement the draw command, with dc if given, using any new - settings (legend toggled, axis specs, zoom, etc). """ - assert self._drawCmd is not None - # check case for axis = (a,b) where a==b caused by improper zooms - if self._drawCmd.isEmpty(): + if xAxis != None: + if xAxis[0] == xAxis[1]: return - + if self.getLogScale()[0]: + xAxis = _Numeric.log10(xAxis) + if yAxis != None: + if yAxis[0] == yAxis[1]: + return + if self.getLogScale()[1]: + yAxis = _Numeric.log10(yAxis) + self._Draw(graphics, xAxis, yAxis, dc) + + def _Draw(self, graphics, xAxis = None, yAxis = None, dc = 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 + """ + if dc == None: # sets new dc and clears it - dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer) + dc = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer) dc.Clear() dc.BeginDrawing() @@ -976,30 +986,49 @@ class PlotCanvas(wx.Window): # 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= _Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin) + p2= _Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax) + + self.last_draw = (graphics, _Numeric.array(xAxis), _Numeric.array(yAxis)) # saves most recient values + # Get ticks and textExtents for axis if required if self._xSpec is not 'none': - xticks = self._drawCmd.getTicksX() + xticks = self._xticks(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._drawCmd.getTicksY() - yTextExtentBottom= dc.GetTextExtent(yticks[0][1]) - yTextExtentTop = dc.GetTextExtent(yticks[-1][1]) - yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]), - max(yTextExtentBottom[1],yTextExtentTop[1])) + yticks = self._yticks(yAxis[0], yAxis[1]) + if self.getLogScale()[1]: + yTextExtent = dc.GetTextExtent('-2e-2') + else: + 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 - graphics = self._drawCmd.graphics titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics) # TextExtents for Legend - legendBoxWH, legendSymExt, legendHGap, legendTextExt \ - = self._legendWH(dc, graphics) + 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 @@ -1025,11 +1054,7 @@ class PlotCanvas(wx.Window): # drawing legend makers and text if self._legendEnabled: - self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, - legendSymExt, legendHGap, legendTextExt) - - #sizes axis to axis type, create lower left and upper right corners of plot - p1, p2 = self._drawCmd.getBoundingBox() + self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt) # allow for scaling and shifting plotted points scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1)) @@ -1051,18 +1076,21 @@ class PlotCanvas(wx.Window): # remove the clipping region dc.DestroyClippingRegion() dc.EndDrawing() + + self._adjustScrollbars() - def Redraw(self, dc= None): + def Redraw(self, dc=None): """Redraw the existing plot.""" - if self.BeenDrawn(): - self._draw(dc) + 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 = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer) dc.Clear() - self._drawCmd = None + self.last_draw = None def Zoom(self, Center, Ratio): """ Zoom on the plot @@ -1070,19 +1098,15 @@ class PlotCanvas(wx.Window): Zooms by the Ratio = (Xratio, Yratio) given """ self.last_PointLabel = None #reset maker - if self.BeenDrawn(): - self._drawCmd.zoom(Center, Ratio) - self._draw() + 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 ChangeAxes(self, xAxis, yAxis): - """Change axes specified at last Draw() command. If - Draw() never called yet, RuntimeError raised. """ - if not self.BeenDrawn(): - raise RuntimeError, "Must call Draw() at least once" - self._drawCmd.changeAxisX(xAxis, self._xSpec) - self._drawCmd.changeAxisY(yAxis, self._ySpec) - self._draw() - def GetClosestPoints(self, pntXY, pointScaled= True): """Returns list with [curveNumber, legend, index of closest point, pointXY, scaledXY, distance] @@ -1093,11 +1117,19 @@ class PlotCanvas(wx.Window): if pointScaled == True based on screen coords if pointScaled == False based on user coords """ - if self.BeenDrawn(): - return self._drawCmd.getClosestPoints(pntXY, pointScaled) - else: + 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 @@ -1134,7 +1166,7 @@ class PlotCanvas(wx.Window): """ if self.last_PointLabel != None: #compare pointXY - if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]: + if _Numeric.sometrue(mDataDict["pointXY"] != self.last_PointLabel["pointXY"]): #closest changed self._drawPointLabel(self.last_PointLabel) #erase old self._drawPointLabel(mDataDict) #plot new @@ -1146,44 +1178,62 @@ class PlotCanvas(wx.Window): # event handlers ********************************** def OnMotion(self, event): - if not self.BeenDrawn(): return 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._zoomCorner2[0], self._zoomCorner2[1] = self._getXY(event) self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new - + elif self._dragEnabled and event.LeftIsDown(): + coordinates = event.GetPosition() + newpos, oldpos = map(_Numeric.array, map(self.PositionScreenToUser, [coordinates, self._screenCoordinates])) + dist = newpos-oldpos + self._screenCoordinates = coordinates + + if self.last_draw is not None: + graphics, xAxis, yAxis= self.last_draw + yAxis -= dist[1] + xAxis -= dist[0] + self._Draw(graphics,xAxis,yAxis) + def OnMouseLeftDown(self,event): - if self.BeenDrawn(): - self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event) + self._zoomCorner1[0], self._zoomCorner1[1]= self._getXY(event) + self._screenCoordinates = _Numeric.array(event.GetPosition()) + if self._dragEnabled: + self.SetCursor(self.GrabHandCursor) + self.canvas.CaptureMouse() def OnMouseLeftUp(self, event): - if not self.BeenDrawn(): return 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._zoomCorner2[0], self._zoomCorner2[1]= self._getXY(event) self._hasDragged = False # reset flag minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2) maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2) self.last_PointLabel = None #reset pointLabel - self._drawCmd.changeAxisX((minX,maxX)) - self._drawCmd.changeAxisY((minY,maxY)) - self._draw() + 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) + # X,Y = self._getXY(event) # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) ) + if self._dragEnabled: + self.SetCursor(self.HandCursor) + if self.canvas.HasCapture(): + self.canvas.ReleaseMouse() def OnMouseDoubleClick(self,event): if self._zoomEnabled: - self.Reset() + # Give a little time for the click to be totally finished + # before (possibly) removing the scrollbars and trigering + # size events, etc. + wx.FutureCall(200,self.Reset) def OnMouseRightDown(self,event): if self._zoomEnabled: - X,Y = self.GetXY(event) + X,Y = self._getXY(event) self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) ) def OnPaint(self, event): @@ -1191,25 +1241,28 @@ class PlotCanvas(wx.Window): if self.last_PointLabel != None: self._drawPointLabel(self.last_PointLabel) #erase old self.last_PointLabel = None - dc = wx.BufferedPaintDC(self, self._Buffer) + dc = wx.BufferedPaintDC(self.canvas, 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() - + Size = self.canvas.GetClientSize() + Size.width = max(1, Size.width) + Size.height = max(1, Size.height) + # 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]) + self._Buffer = wx.EmptyBitmap(Size.width, Size.height) self._setSize() self.last_PointLabel = None #reset pointLabel - if self.BeenDrawn(): - self._draw() - else: + 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""" @@ -1217,12 +1270,26 @@ class PlotCanvas(wx.Window): self._drawPointLabel(self.last_PointLabel) #erase old self.last_PointLabel = None + def OnScroll(self, evt): + if not self._adjustingSB: + self._sb_ignore = True + sbpos = evt.GetPosition() + if evt.GetOrientation() == wx.VERTICAL: + fullrange,pagesize = self.sb_vert.GetRange(),self.sb_vert.GetPageSize() + sbpos = fullrange-pagesize-sbpos + dist = sbpos*self._sb_yunit-(self._getYCurrentRange()[0]-self._sb_yfullrange[0]) + self.ScrollUp(dist) + + if evt.GetOrientation() == wx.HORIZONTAL: + dist = sbpos*self._sb_xunit-(self._getXCurrentRange()[0]-self._sb_xfullrange[0]) + self.ScrollRight(dist) + # Private Methods ************************************************** def _setSize(self, width=None, height=None): """DC width and height.""" if width == None: - (self.width,self.height) = self.GetClientSize() + (self.width,self.height) = self.canvas.GetClientSize() else: self.width, self.height= width,height self.plotbox_size = 0.97*_Numeric.array([self.width, self.height]) @@ -1237,8 +1304,9 @@ class PlotCanvas(wx.Window): def _printDraw(self, printDC): """Used for printing.""" - if self.BeenDrawn(): - self._draw(printDC) + 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""" @@ -1252,26 +1320,35 @@ class PlotCanvas(wx.Window): self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function dcs.EndDrawing() - dc = wx.ClientDC( self ) + dc = wx.ClientDC( self.canvas ) #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, hgap, legendTextExt): + 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)) - legendIdx = 0 - for gr in graphics: - s = legendIdx * lineHeight - drew = gr.drawLegend(dc, self.printerScale, - legendSymExt[0], hgap, legendTextExt[1], - trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.) - if drew: - legendIdx += 1 + 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= _Numeric.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= _Numeric.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): @@ -1290,31 +1367,29 @@ class PlotCanvas(wx.Window): """Returns the size in screen units for legend box""" if self._legendEnabled != True: legendBoxWH= symExt= txtExt= (0,0) - hgap= 0 else: - #find max symbol size and gap b/w symbol and text + # find max symbol size symExt= graphics.getSymExtent(self.printerScale) - hgap = symExt[0]*0.1 # 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= _Numeric.maximum(txtExt,dc.GetTextExtent(txt)) - maxW= symExt[0]+hgap+txtExt[0] + 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, hgap, txtExt) + 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 = wx.ClientDC( self.canvas ) dc.BeginDrawing() dc.SetPen(wx.Pen(wx.BLACK)) dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) ) @@ -1386,13 +1461,20 @@ class PlotCanvas(wx.Window): def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks): penWidth= self.printerScale # increases thickness for printing only - dc.SetPen(wx.Pen(wx.BLACK, penWidth)) + dc.SetPen(wx.Pen(self._gridColour, 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 + if self._gridEnabled == 'Horizontal': + yTickLength= width/2.0 +1 + xTickLength= 3 * self.printerScale + elif self._gridEnabled == 'Vertical': + yTickLength= 3 * self.printerScale + xTickLength= height/2.0 +1 + else: + yTickLength= width/2.0 +1 + xTickLength= height/2.0 +1 else: yTickLength= 3 * self.printerScale # lengthens lines for printing xTickLength= 3 * self.printerScale @@ -1427,147 +1509,50 @@ class PlotCanvas(wx.Window): pt[1]-0.5*h) text = 0 # axis values not drawn on right side - -class DrawCmd: - """Represent a "draw" command, i.e. the one given in call to - PlotCanvas.Draw(). The axis specs are not saved to preserve - backward compatibility: they could be specified before the - first Draw() command.""" - def __init__(self, graphics, xAxis, yAxis, xSpec, ySpec, xTicks, yTicks): - """Same args as PlotCanvas.Draw(), plus axis specs.""" - self.graphics = graphics - self.xTickGen = xTicks - self.yTickGen = yTicks - self.xAxisInit, self.yAxisInit = xAxis, yAxis - - self.xAxis = None - self.yAxis = None - self.changeAxisX(xAxis, xSpec) - self.changeAxisY(yAxis, ySpec) - assert self.xAxis is not None - assert self.yAxis is not None - - def isEmpty(self): - """Return true if either axis has 0 size.""" - if self.xAxis[0] == self.xAxis[1]: - return True - if self.yAxis[0] == self.yAxis[1]: - return True - - return False - - def resetAxes(self, xSpec, ySpec): - """Reset the axes to what they were initially, using axes specs given.""" - self.changeAxisX(self.xAxisInit, xSpec) - self.changeAxisY(self.yAxisInit, ySpec) - - def getBoundingBox(self): - """Returns p1, p2 (two pairs)""" - p1 = _Numeric.array((self.xAxis[0], self.yAxis[0])) - p2 = _Numeric.array((self.xAxis[1], self.yAxis[1])) - return p1, p2 + def _xticks(self, *args): + if self._logscale[0]: + return self._logticks(*args) + else: + return self._ticks(*args) - def getClosestPoints(self, pntXY, pointScaled=True): - ll = [] - for curveNum,obj in enumerate(self.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) - ll.append(cn) - return ll - - def scrollAxisX(self, units, xSpec): - """Scroll y axis by units, using ySpec for axis spec.""" - self.changeAxisX((self.xAxis[0]+units, self.xAxis[1]+units), xSpec) - - def scrollAxisY(self, units, ySpec): - """Scroll y axis by units, using ySpec for axis spec.""" - self.changeAxisY((self.yAxis[0]+units, self.yAxis[1]+units), ySpec) - - def changeAxisX(self, xAxis=None, xSpec=None): - """Change the x axis to new given, or if None use ySpec to get it. """ - assert type(xAxis) in [type(None),tuple] - if xAxis is None: - p1, p2 = self.graphics.boundingBox() #min, max points of graphics - self.xAxis = self._axisInterval(xSpec, p1[0], p2[0]) #in user units - else: - self.xAxis = xAxis - - def changeAxisY(self, yAxis=None, ySpec=None): - """Change the y axis to new given, or if None use ySpec to get it. """ - assert type(yAxis) in [type(None),tuple] - if yAxis is None: - p1, p2 = self.graphics.boundingBox() #min, max points of graphics - self.yAxis = self._axisInterval(ySpec, p1[1], p2[1]) - else: - self.yAxis = yAxis - - def zoom(self, center, ratio): - """Zoom to center and ratio.""" - x,y = Center - w = (self.xAxis[1] - self.xAxis[0]) * Ratio[0] - h = (self.yAxis[1] - self.yAxis[0]) * Ratio[1] - self.xAxis = ( x - w/2, x + w/2 ) - self.yAxis = ( y - h/2, y + h/2 ) - - def getXMaxRange(self): - p1, p2 = self.graphics.boundingBox() #min, max points of graphics - return self._axisInterval(self._xSpec, p1[0], p2[0]) #in user units - - def getYMaxRange(self): - p1, p2 = self.graphics.boundingBox() #min, max points of graphics - return self._axisInterval(self._ySpec, p1[1], p2[1]) #in user units + def _yticks(self, *args): + if self._logscale[1]: + return self._logticks(*args) + else: + return self._ticks(*args) - def getTicksX(self): - """Get the ticks along y axis. Actually pairs of (t, str).""" - xticks = self._ticks(self.xAxis[0], self.xAxis[1], self.xTickGen) - if xticks == []: # try without the generator - xticks = self._ticks(self.xAxis[0], self.xAxis[1]) - return xticks - - def getTicksY(self): - """Get the ticks along y axis. Actually pairs of (t, str).""" - yticks = self._ticks(self.yAxis[0], self.yAxis[1], self.yTickGen) - if yticks == []: # try without the generator - yticks = self._ticks(self.yAxis[0], self.yAxis[1]) - return yticks - - 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.: - return lower-0.5, upper+0.5 - log = _Numeric.log10(range) - power = _Numeric.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 + def _logticks(self, lower, upper): + #lower,upper = map(_Numeric.log10,[lower,upper]) + #print 'logticks',lower,upper + ticks = [] + mag = _Numeric.power(10,_Numeric.floor(lower)) + if upper-lower > 6: + t = _Numeric.power(10,_Numeric.ceil(lower)) + base = _Numeric.power(10,_Numeric.floor((upper-lower)/6)) + def inc(t): + return t*base-t else: - raise ValueError, str(spec) + ': illegal axis specification' - - def _ticks(self, lower, upper, generator=None): - """Get ticks between lower and upper, using generator if given. """ + t = _Numeric.ceil(_Numeric.power(10,lower)/mag)*mag + def inc(t): + return 10**int(_Numeric.floor(_Numeric.log10(t)+1e-16)) + majortick = int(_Numeric.log10(mag)) + while t <= pow(10,upper): + if majortick != int(_Numeric.floor(_Numeric.log10(t)+1e-16)): + majortick = int(_Numeric.floor(_Numeric.log10(t)+1e-16)) + ticklabel = '1e%d'%majortick + else: + if upper-lower < 2: + minortick = int(t/pow(10,majortick)+.5) + ticklabel = '%de%d'%(minortick,majortick) + else: + ticklabel = '' + ticks.append((_Numeric.log10(t), ticklabel)) + t += inc(t) + if len(ticks) == 0: + ticks = [(0,'')] + return ticks + + def _ticks(self, lower, upper): ideal = (upper-lower)/7. log = _Numeric.log10(ideal) power = _Numeric.floor(log) @@ -1588,34 +1573,70 @@ class DrawCmd: else: digits = -int(power) format = '%'+`digits+2`+'.'+`digits`+'f' + ticks = [] + t = -grid*_Numeric.floor(-lower/grid) + while t <= upper: + ticks.append( (t, format % (t,)) ) + t = t + grid + return ticks + + _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))] + + + def _adjustScrollbars(self): + if self._sb_ignore: + self._sb_ignore = False + return + + self._adjustingSB = True + needScrollbars = False + + # horizontal scrollbar + r_current = self._getXCurrentRange() + r_max = list(self._getXMaxRange()) + sbfullrange = float(self.sb_hor.GetRange()) + + r_max[0] = min(r_max[0],r_current[0]) + r_max[1] = max(r_max[1],r_current[1]) + + self._sb_xfullrange = r_max + + unit = (r_max[1]-r_max[0])/float(self.sb_hor.GetRange()) + pos = int((r_current[0]-r_max[0])/unit) - if generator is None: - return tickGeneratorDefault(lower, upper, grid, format) - elif isinstance(generator, list): - return tickGeneratorFromList(generator, lower, upper, format) + if pos >= 0: + pagesize = int((r_current[1]-r_current[0])/unit) + + self.sb_hor.SetScrollbar(pos, pagesize, sbfullrange, pagesize) + self._sb_xunit = unit + needScrollbars = needScrollbars or (pagesize != sbfullrange) else: - return generator(lower, upper, grid, format) + self.sb_hor.SetScrollbar(0, 1000, 1000, 1000) - _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))] + # vertical scrollbar + r_current = self._getYCurrentRange() + r_max = list(self._getYMaxRange()) + sbfullrange = float(self.sb_vert.GetRange()) + r_max[0] = min(r_max[0],r_current[0]) + r_max[1] = max(r_max[1],r_current[1]) + + self._sb_yfullrange = r_max + + unit = (r_max[1]-r_max[0])/sbfullrange + pos = int((r_current[0]-r_max[0])/unit) + + if pos >= 0: + pagesize = int((r_current[1]-r_current[0])/unit) + pos = (sbfullrange-1-pos-pagesize) + self.sb_vert.SetScrollbar(pos, pagesize, sbfullrange, pagesize) + self._sb_yunit = unit + needScrollbars = needScrollbars or (pagesize != sbfullrange) + else: + self.sb_vert.SetScrollbar(0, 1000, 1000, 1000) -def tickGeneratorDefault(lower, upper, grid, format): - """Default tick generator used by PlotCanvas.Draw() if not specified.""" - ticks = [] - t = -grid*_Numeric.floor(-lower/grid) - while t <= upper: - ticks.append( (t, format % (t,)) ) - t = t + grid - return ticks - -def tickGeneratorFromList(ticks, lower, upper, format): - """Tick generator used by PlotCanvas.Draw() if - a list is given for xTicks or yTicks. """ - tickPairs = [] - for tick in ticks: - if lower <= tick <= upper: - tickPairs.append((tick, format % tick)) - return tickPairs + self.SetShowScrollbars(needScrollbars) + self._adjustingSB = False #------------------------------------------------------------------------------- # Used to layout the printer page @@ -1695,6 +1716,83 @@ class PlotPrintout(wx.Printout): return True +#---------------------------------------------------------------------- +from wx import ImageFromStream, BitmapFromImage +import cStringIO, zlib + + +def getMagPlusData(): + return zlib.decompress( +'x\xda\x01*\x01\xd5\xfe\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\ +\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\x00\x00\x00\x04sBIT\x08\x08\ +\x08\x08|\x08d\x88\x00\x00\x00\xe1IDATx\x9c\xb5U\xd1\x0e\xc4 \x08\xa3n\xff\ +\xff\xc5\xdb\xb8\xa7\xee<\x04\x86gFb\xb2\x88\xb6\x14\x90\x01m\x937m\x8f\x1c\ +\xd7yh\xe4k\xdb\x8e*\x01<\x05\x04\x07F\x1cU\x9d"\x19\x14\\\xe7\xa1\x1e\xf07"\ +\x90H+$?\x04\x16\x9c\xd1z\x04\x00J$m\x06\xdc\xee\x03Hku\x13\xd8C\x16\x84+"O\ +\x1b\xa2\x07\xca"\xb7\xc6sY\xbdD\x926\xf5.\xce\x06!\xd2)x\xcb^\'\x08S\xe4\ +\xe5x&5\xb4[A\xb5h\xb4j=\x9a\xc8\xf8\xecm\xd4\\\x9e\xdf\xbb?\x10\xf0P\x06\ +\x12\xed?=\xb6a\xd8=\xcd\xa2\xc8T\xd5U2t\x11\x95d\xa3"\x9aQ\x9e\x12\xb7M\x19\ +I\x9f\xff\x1e\xd8\xa63#q\xff\x07U\x8b\xd2\xd9\xa7k\xe9\xa1U\x94,\xbf\xe4\x88\ +\xe4\xf6\xaf\x12x$}\x8a\xc2Q\xf1\'\x89\xf2\x9b\xfbKE\xae\xd8\x07+\xd2\xa7c\ +\xdf\x0e\xc3D\x00\x00\x00\x00IEND\xaeB`\x82\xe2ovy' ) + +def getMagPlusBitmap(): + return BitmapFromImage(getMagPlusImage()) + +def getMagPlusImage(): + stream = cStringIO.StringIO(getMagPlusData()) + return ImageFromStream(stream) + +#---------------------------------------------------------------------- +def getGrabHandData(): + return zlib.decompress( +'x\xda\x01Z\x01\xa5\xfe\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\ +\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\x00\x00\x00\x04sBIT\x08\x08\ +\x08\x08|\x08d\x88\x00\x00\x01\x11IDATx\x9c\xb5U\xd1\x12\x830\x08Kh\xff\xff\ +\x8b7\xb3\x97\xd1C\xa4Zw\x93;\x1fJ1\t\x98VJ\x92\xb5N<\x14\x04 I\x00\x80H\xb4\ +\xbd_\x8a9_{\\\x89\xf2z\x02\x18/J\x82\xb5\xce\xed\xfd\x12\xc9\x91\x03\x00_\ +\xc7\xda\x8al\x00{\xfdW\xfex\xf2zeO\x92h\xed\x80\x05@\xa45D\xc5\xb3\x98u\x12\ +\xf7\xab.\xa9\xd0k\x1eK\x95\xbb\x1a]&0\x92\xf0\'\xc6]gI\xda\tsr\xab\x8aI\x1e\ +\\\xe3\xa4\x0e\xb4*`7"\x07\x8f\xaa"x\x05\xe0\xdfo6B\xf3\x17\xe3\x98r\xf1\xaf\ +\x07\xd1Z\'%\x95\x0erW\xac\x8c\xe3\xe0\xfd\xd8AN\xae\xb8\xa3R\x9as>\x11\x8bl\ +yD\xab\x1f\xf3\xec\x1cY\x06\x89$\xbf\x80\xfb\x14\\dw\x90x\x12\xa3+\xeeD\x16%\ +I\xe3\x1c\xb8\xc7c\'\xd5Y8S\x9f\xc3Zg\xcf\x89\xe8\xaao\'\xbbk{U\xfd\xc0\xacX\ +\xab\xbb\xe8\xae\xfa)AEr\x15g\x86(\t\xfe\x19\xa4\xb5\xe9f\xfem\xde\xdd\xbf$\ +\xf8G<>\xa2\xc7\t>\tE\xfc\x8a\xf6\x8dqc\x00\x00\x00\x00IEND\xaeB`\x82\xdb\ +\xd0\x8f\n' ) + +def getGrabHandBitmap(): + return BitmapFromImage(getGrabHandImage()) + +def getGrabHandImage(): + stream = cStringIO.StringIO(getGrabHandData()) + return ImageFromStream(stream) + +#---------------------------------------------------------------------- +def getHandData(): + return zlib.decompress( +'x\xda\x01Y\x01\xa6\xfe\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x18\ +\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0w=\xf8\x00\x00\x00\x04sBIT\x08\x08\ +\x08\x08|\x08d\x88\x00\x00\x01\x10IDATx\x9c\xad\x96\xe1\x02\xc2 \x08\x849\ +\xf5\xfd\x9fx\xdb\xf5\'\x8c!\xa8\xab\xee\x975\xe5\x83\x0b\\@\xa9\xb2\xab\xeb\ +<\xa8\xebR\x1bv\xce\xb4\'\xc1\x81OL\x92\xdc\x81\x0c\x00\x1b\x88\xa4\x94\xda\ +\xe0\x83\x8b\x88\x00\x10\x92\xcb\x8a\xca,K\x1fT\xa1\x1e\x04\xe0f_\n\x88\x02\ +\xf1:\xc3\x83>\x81\x0c\x92\x02v\xe5+\xba\xce\x83\xb7f\xb8\xd1\x9c\x8fz8\xb2*\ +\x93\xb7l\xa8\xe0\x9b\xa06\xb8]_\xe7\xc1\x01\x10U\xe1m\x98\xc9\xefm"ck\xea\ +\x1a\x80\xa0Th\xb9\xfd\x877{V*Qk\xda,\xb4\x8b\xf4;[\xa1\xcf6\xaa4\x9cd\x85X\ +\xb0\r\\j\x83\x9dd\x92\xc3 \xf6\xbd\xab\x0c2\x05\xc0p\x9a\xa7]\xf4\x14\x18]3\ +7\x80}h?\xff\xa2\xa2\xe5e\x90\xact\xaf\xe8B\x14y[4\x83|\x13\xdc\x9e\xeb\x16e\ +\x90\xa7\xf2I\rw\x91\x87d\xd7p\x96\xbd\xd70\x07\xda\xe3v\x9a\xf5\xc5\xb2\xb2\ ++\xb24\xbc\xaew\xedZe\x9f\x02"\xc8J\xdb\x83\xf6oa\xf5\xb7\xa5\xbf8\x12\xffW\ +\xcf_\xbd;\xe4\x8c\x03\x10\xdb^\x00\x00\x00\x00IEND\xaeB`\x82\xd1>\x97B' ) + +def getHandBitmap(): + return BitmapFromImage(getHandImage()) + +def getHandImage(): + stream = cStringIO.StringIO(getHandData()) + return ImageFromStream(stream) + #--------------------------------------------------------------------------- @@ -1790,29 +1888,17 @@ def _draw6Objects(): return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b], "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students") - def _draw7Objects(): - # four bars of various styles colors and sizes - bar1 = PolyBar((0,1), colour='green', size=4, labels='Green bar') - bar2 = PolyBar((2,1), colour='red', size=7, labels='Red bar', - fillstyle=wx.CROSSDIAG_HATCH) - bar3 = PolyBar([(1,3),(3,4)], colour='blue', - labels=['Blue bar 1', 'Blue bar 2'], - fillstyle=wx.TRANSPARENT) - bars = [bar1, bar2, bar3] - return PlotGraphics(bars, - "Graph Title", "Bar names", "Bar height"), getBarTicksGen(*bars) - -def _draw8Objects(): - # four bars in two groups, overlayed to a line plot - x1,x2 = 0.0,0.5 - bar1 = PolyBar([(x1,1.),(x1+2,2.)], colour='green', size=4, - legend="1998", fillstyle=wx.CROSSDIAG_HATCH) - bar2 = PolyBar(bar1.offset([1.2,2.5]), colour='red', size=7, - legend="2000", labels=['cars','trucks']) - line1 = PolyLine([(x1,1.5), (x2,1.2), (x1+2,1), (x2+2,2)], colour='blue') - return PlotGraphics([bar1, bar2, line1], - "Graph Title", "Bar names", "Bar height"), getBarTicksGen(bar1, bar2) + # Empty graph with axis defined but no points/lines + x = _Numeric.arange(1,1000,1) + y1 = 4.5*x**2 + y2 = 2.2*x**3 + points1 = _Numeric.transpose([x,y1]) + points2 = _Numeric.transpose([x,y2]) + line1 = PolyLine(points1, legend='quadratic', colour='blue', width=1) + line2 = PolyLine(points2, legend='cubic', colour='red', width=1) + return PlotGraphics([line1,line2], "double log plot", "Value X", "Value Y") + class TestFrame(wx.Frame): def __init__(self, parent, id, title): @@ -1854,9 +1940,6 @@ class TestFrame(wx.Frame): self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260) menu.Append(261, 'Draw7', 'Draw plots7') self.Bind(wx.EVT_MENU,self.OnPlotDraw7, id=261) - menu.Append(262, 'Draw8', 'Draw plots8') - self.Bind(wx.EVT_MENU,self.OnPlotDraw8, id=262) - menu.Append(211, '&Redraw', 'Redraw plots') self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211) @@ -1868,6 +1951,8 @@ class TestFrame(wx.Frame): 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(217, 'Enable &Drag', 'Activates dragging mode', kind=wx.ITEM_CHECK) + self.Bind(wx.EVT_MENU,self.OnEnableDrag, id=217) 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) @@ -1896,9 +1981,9 @@ class TestFrame(wx.Frame): #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) + self.client.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) # Show closest point when enabled - self.client.Bind(wx.EVT_MOTION, self.OnMotion) + self.client.canvas.Bind(wx.EVT_MOTION, self.OnMotion) self.Show(True) @@ -1927,7 +2012,7 @@ class TestFrame(wx.Frame): # ----------- def OnMouseLeftDown(self,event): - s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event) + s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client._getXY(event) self.SetStatusText(s) event.Skip() #allows plotCanvas OnMouseLeftDown to be called @@ -1936,7 +2021,7 @@ class TestFrame(wx.Frame): 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) + 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) @@ -2007,20 +2092,10 @@ class TestFrame(wx.Frame): self.client.Draw(_draw6Objects(), xAxis= (0,7)) def OnPlotDraw7(self, event): - #Bar Graph and custom ticks Example, using PolyBar + #log scale example self.resetDefaults() - bars, tickGenerator = _draw7Objects() - #note that yTicks is not necessary here, used only - #to show off custom ticks - self.client.Draw(bars, xAxis=(0,4),yAxis = (0,5), - xTicks=tickGenerator, yTicks=[0,1,3]) - - def OnPlotDraw8(self, event): - #Bar Graph and custom ticks Example, using PolyBar - self.resetDefaults() - plot, tickGenerator = _draw8Objects() - self.client.Draw(plot, xAxis=(0,4),yAxis = (0,5), xTicks=tickGenerator) - + self.client.setLogScale((True,True)) + self.client.Draw(_draw7Objects()) def OnPlotRedraw(self,event): self.client.Redraw() @@ -2029,16 +2104,21 @@ class TestFrame(wx.Frame): self.client.Clear() def OnPlotScale(self, event): - if self.client.BeenDrawn(): - self.client.ChangeAxes((1,3.05),(0,1)) - + 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()) + self.mainmenu.Check(217, not event.IsChecked()) def OnEnableGrid(self, event): self.client.SetEnableGrid(event.IsChecked()) + def OnEnableDrag(self, event): + self.client.SetEnableDrag(event.IsChecked()) + self.mainmenu.Check(214, not event.IsChecked()) + def OnEnableLegend(self, event): self.client.SetEnableLegend(event.IsChecked()) @@ -2064,9 +2144,11 @@ class TestFrame(wx.Frame): self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL)) self.client.SetFontSizeAxis(10) self.client.SetFontSizeLegend(7) + self.client.setLogScale((False,False)) self.client.SetXSpec('auto') self.client.SetYSpec('auto') + def __test():