]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/plot.py
in unescape add normal characters in addition to unescaped characters :)
[wxWidgets.git] / wxPython / wx / lib / plot.py
CommitLineData
9d6685e2
RD
1#-----------------------------------------------------------------------------
2# Name: wx.lib.plot.py
00b9c867 3# Purpose: Line, Bar and Scatter Graphs
9d6685e2
RD
4#
5# Author: Gordon Williams
6#
7# Created: 2003/11/03
8# RCS-ID: $Id$
9# Copyright: (c) 2002
10# Licence: Use as you wish.
11#-----------------------------------------------------------------------------
12# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13#
14# o 2.5 compatability update.
15# o Renamed to plot.py in the wx.lib directory.
16# o Reworked test frame to work with wx demo framework. This saves a bit
17# of tedious cut and paste, and the test app is excellent.
18#
33785d9f
RD
19# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
20#
21# o wxScrolledMessageDialog -> ScrolledMessageDialog
22#
00b9c867
RD
23# Oct 6, 2004 Gordon Williams (g_will@cyberus.ca)
24# - Added bar graph demo
25# - Modified line end shape from round to square.
26# - Removed FloatDCWrapper for conversion to ints and ints in arguments
27#
b31cbeb9
RD
28# Oct 15, 2004 Gordon Williams (g_will@cyberus.ca)
29# - Imported modules given leading underscore to name.
30# - Added Cursor Line Tracking and User Point Labels.
31# - Demo for Cursor Line Tracking and Point Labels.
32# - Size of plot preview frame adjusted to show page better.
33# - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
34# - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
35# can be in either user coords or screen coords.
36#
ecf0b9f9
RD
37# Nov 2004 Oliver Schoenborn (oliver.schoenborn@utoronto.ca) with
38# valuable input from and testing by Gary)
39# - Factored out "draw command" so extensions easier to implement: clean
40# separation b/w user's "draw" (Draw) and internal "draw" (_draw)
41# - Added ability to define your own ticks at PlotCanvas.Draw
42# - Added better bar charts: PolyBar class and getBarTicksGen()
43# - Put legend writing for a Poly* into Poly* class where it belongs
44# - If no legend specified or exists, no gap created
b31cbeb9 45#
9d6685e2
RD
46
47"""
48This is a simple light weight plotting module that can be used with
49Boa or easily integrated into your own wxPython application. The
50emphasis is on small size and fast plotting for large data sets. It
51has a reasonable number of features to do line and scatter graphs
00b9c867
RD
52easily as well as simple bar graphs. It is not as sophisticated or
53as powerful as SciPy Plt or Chaco. Both of these are great packages
54but consume huge amounts of computer resources for simple plots.
55They can be found at http://scipy.com
9d6685e2
RD
56
57This file contains two parts; first the re-usable library stuff, then,
58after a "if __name__=='__main__'" test, a simple frame and a few default
59plots for examples and testing.
60
61Based on wxPlotCanvas
62Written by K.Hinsen, R. Srinivasan;
63Ported to wxPython Harm van der Heijden, feb 1999
64
65Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
66 -More style options
67 -Zooming using mouse "rubber band"
68 -Scroll left, right
69 -Grid(graticule)
70 -Printing, preview, and page set up (margins)
71 -Axis and title labels
72 -Cursor xy axis values
73 -Doc strings and lots of comments
74 -Optimizations for large number of points
75 -Legends
76
77Did a lot of work here to speed markers up. Only a factor of 4
78improvement though. Lines are much faster than markers, especially
79filled markers. Stay away from circles and triangles unless you
80only have a few thousand points.
81
82Times for 25,000 points
83Line - 0.078 sec
84Markers
85Square - 0.22 sec
86dot - 0.10
87circle - 0.87
88cross,plus - 0.28
89triangle, triangle_down - 0.90
90
91Thanks to Chris Barker for getting this version working on Linux.
92
93Zooming controls with mouse (when enabled):
94 Left mouse drag - Zoom box.
95 Left mouse double click - reset zoom.
96 Right mouse click - zoom out centred on click location.
97"""
98
b31cbeb9
RD
99import string as _string
100import time as _time
9d6685e2
RD
101import wx
102
00b9c867 103# Needs Numeric or numarray
9d6685e2 104try:
b31cbeb9 105 import Numeric as _Numeric
9d6685e2
RD
106except:
107 try:
b31cbeb9 108 import numarray as _Numeric #if numarray is used it is renamed Numeric
9d6685e2
RD
109 except:
110 msg= """
111 This module requires the Numeric or numarray module,
112 which could not be imported. It probably is not installed
113 (it's not part of the standard Python distribution). See the
114 Python site (http://www.python.org) for information on
115 downloading source or binaries."""
116 raise ImportError, "Numeric or numarray not found. \n" + msg
117
118
119
120#
121# Plotting classes...
122#
123class PolyPoints:
124 """Base Class for lines and markers
125 - All methods are private.
126 """
127
128 def __init__(self, points, attr):
b31cbeb9 129 self.points = _Numeric.array(points)
9d6685e2
RD
130 self.currentScale= (1,1)
131 self.currentShift= (0,0)
132 self.scaled = self.points
133 self.attributes = {}
134 self.attributes.update(self._attributes)
135 for name, value in attr.items():
136 if name not in self._attributes.keys():
137 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
138 self.attributes[name] = value
139
140 def boundingBox(self):
141 if len(self.points) == 0:
142 # no curves to draw
143 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
b31cbeb9
RD
144 minXY= _Numeric.array([-1,-1])
145 maxXY= _Numeric.array([ 1, 1])
9d6685e2 146 else:
b31cbeb9
RD
147 minXY= _Numeric.minimum.reduce(self.points)
148 maxXY= _Numeric.maximum.reduce(self.points)
9d6685e2
RD
149 return minXY, maxXY
150
151 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
152 if len(self.points) == 0:
153 # no curves to draw
154 return
155 if (scale is not self.currentScale) or (shift is not self.currentShift):
156 # update point scaling
157 self.scaled = scale*self.points+shift
158 self.currentScale= scale
159 self.currentShift= shift
160 # else unchanged use the current scaling
161
ecf0b9f9
RD
162 def drawLegend(self, dc, printerScale, symWidth, hgap, textHeight, x0, y0):
163 legend = self.getLegend()
164 if legend != '':
165 self._drawLegendSym(dc, printerScale, symWidth, x0, y0)
166 dc.DrawText(legend, x0+symWidth+hgap, y0-textHeight/2)
167 return True
168
169 return False
170
171 def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0):
172 # default:
173 pnt= (x0+symWidth/2., y0)
174 self.draw(dc, printerScale, coord= _Numeric.array([pnt]))
175
9d6685e2
RD
176 def getLegend(self):
177 return self.attributes['legend']
178
b31cbeb9
RD
179 def getClosestPoint(self, pntXY, pointScaled= True):
180 """Returns the index of closest point on the curve, pointXY, scaledXY, distance
181 x, y in user coords
182 if pointScaled == True based on screen coords
183 if pointScaled == False based on user coords
184 """
185 if pointScaled == True:
186 #Using screen coords
187 p = self.scaled
188 pxy = self.currentScale * _Numeric.array(pntXY)+ self.currentShift
189 else:
190 #Using user coords
191 p = self.points
192 pxy = _Numeric.array(pntXY)
193 #determine distance for each point
194 d= _Numeric.sqrt(_Numeric.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
195 pntIndex = _Numeric.argmin(d)
196 dist = d[pntIndex]
197 return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
198
199
9d6685e2
RD
200class PolyLine(PolyPoints):
201 """Class to define line type and style
202 - All methods except __init__ are private.
203 """
204
205 _attributes = {'colour': 'black',
206 'width': 1,
207 'style': wx.SOLID,
208 'legend': ''}
209
210 def __init__(self, points, **attr):
211 """Creates PolyLine object
212 points - sequence (array, tuple or list) of (x,y) points making up line
213 **attr - key word attributes
214 Defaults:
215 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
216 'width'= 1, - Pen width
217 'style'= wx.SOLID, - wx.Pen style
218 'legend'= '' - Line Legend to display
219 """
220 PolyPoints.__init__(self, points, attr)
221
222 def draw(self, dc, printerScale, coord= None):
223 colour = self.attributes['colour']
224 width = self.attributes['width'] * printerScale
225 style= self.attributes['style']
ecf0b9f9 226 pen = wx.Pen(colour, width, style)
00b9c867
RD
227 pen.SetCap(wx.CAP_BUTT)
228 dc.SetPen(pen)
9d6685e2
RD
229 if coord == None:
230 dc.DrawLines(self.scaled)
231 else:
232 dc.DrawLines(coord) # draw legend line
233
ecf0b9f9
RD
234 def _drawLegendSym(self, dc, printerScale, symWidth, x0, y0):
235 pnt1= (x0, y0)
236 pnt2= (x0+symWidth/1.5, y0)
237 self.draw(dc, printerScale, coord= _Numeric.array([pnt1,pnt2]))
238
9d6685e2
RD
239 def getSymExtent(self, printerScale):
240 """Width and Height of Marker"""
241 h= self.attributes['width'] * printerScale
242 w= 5 * h
243 return (w,h)
244
245
246class PolyMarker(PolyPoints):
247 """Class to define marker type and style
248 - All methods except __init__ are private.
249 """
250
251 _attributes = {'colour': 'black',
252 'width': 1,
253 'size': 2,
254 'fillcolour': None,
255 'fillstyle': wx.SOLID,
256 'marker': 'circle',
257 'legend': ''}
258
259 def __init__(self, points, **attr):
260 """Creates PolyMarker object
261 points - sequence (array, tuple or list) of (x,y) points
262 **attr - key word attributes
263 Defaults:
264 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
265 'width'= 1, - Pen width
266 'size'= 2, - Marker size
ecf0b9f9 267 'fillcolour'= same as colour, - wx.Brush Colour
9d6685e2
RD
268 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
269 'marker'= 'circle' - Marker shape
270 'legend'= '' - Marker Legend to display
271
272 Marker Shapes:
273 - 'circle'
274 - 'dot'
275 - 'square'
276 - 'triangle'
277 - 'triangle_down'
278 - 'cross'
279 - 'plus'
280 """
281
282 PolyPoints.__init__(self, points, attr)
283
284 def draw(self, dc, printerScale, coord= None):
285 colour = self.attributes['colour']
286 width = self.attributes['width'] * printerScale
287 size = self.attributes['size'] * printerScale
288 fillcolour = self.attributes['fillcolour']
289 fillstyle = self.attributes['fillstyle']
290 marker = self.attributes['marker']
291
ecf0b9f9 292 dc.SetPen(wx.Pen(colour, width))
9d6685e2 293 if fillcolour:
ecf0b9f9 294 dc.SetBrush(wx.Brush(fillcolour,fillstyle))
9d6685e2 295 else:
ecf0b9f9 296 dc.SetBrush(wx.Brush(colour, fillstyle))
9d6685e2
RD
297 if coord == None:
298 self._drawmarkers(dc, self.scaled, marker, size)
299 else:
300 self._drawmarkers(dc, coord, marker, size) # draw legend marker
301
302 def getSymExtent(self, printerScale):
303 """Width and Height of Marker"""
304 s= 5*self.attributes['size'] * printerScale
305 return (s,s)
306
307 def _drawmarkers(self, dc, coords, marker,size=1):
308 f = eval('self._' +marker)
309 f(dc, coords, size)
310
311 def _circle(self, dc, coords, size=1):
312 fact= 2.5*size
313 wh= 5.0*size
b31cbeb9 314 rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
9d6685e2 315 rect[:,0:2]= coords-[fact,fact]
b31cbeb9 316 dc.DrawEllipseList(rect.astype(_Numeric.Int32))
9d6685e2
RD
317
318 def _dot(self, dc, coords, size=1):
319 dc.DrawPointList(coords)
320
321 def _square(self, dc, coords, size=1):
322 fact= 2.5*size
323 wh= 5.0*size
b31cbeb9 324 rect= _Numeric.zeros((len(coords),4),_Numeric.Float)+[0.0,0.0,wh,wh]
9d6685e2 325 rect[:,0:2]= coords-[fact,fact]
b31cbeb9 326 dc.DrawRectangleList(rect.astype(_Numeric.Int32))
9d6685e2
RD
327
328 def _triangle(self, dc, coords, size=1):
329 shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
b31cbeb9 330 poly= _Numeric.repeat(coords,3)
9d6685e2
RD
331 poly.shape= (len(coords),3,2)
332 poly += shape
b31cbeb9 333 dc.DrawPolygonList(poly.astype(_Numeric.Int32))
9d6685e2
RD
334
335 def _triangle_down(self, dc, coords, size=1):
336 shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
b31cbeb9 337 poly= _Numeric.repeat(coords,3)
9d6685e2
RD
338 poly.shape= (len(coords),3,2)
339 poly += shape
b31cbeb9 340 dc.DrawPolygonList(poly.astype(_Numeric.Int32))
9d6685e2
RD
341
342 def _cross(self, dc, coords, size=1):
343 fact= 2.5*size
344 for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
b31cbeb9
RD
345 lines= _Numeric.concatenate((coords,coords),axis=1)+f
346 dc.DrawLineList(lines.astype(_Numeric.Int32))
9d6685e2
RD
347
348 def _plus(self, dc, coords, size=1):
349 fact= 2.5*size
350 for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
b31cbeb9
RD
351 lines= _Numeric.concatenate((coords,coords),axis=1)+f
352 dc.DrawLineList(lines.astype(_Numeric.Int32))
9d6685e2 353
ecf0b9f9
RD
354class PolyBar(PolyPoints):
355 """Class to define one or more bars for bar chart. All bars in a
356 PolyBar share same style etc.
357 - All methods except __init__ are private.
358 """
359
360 _attributes = {'colour': 'black',
361 'width': 1,
362 'size': 2,
363 'legend': '',
364 'fillcolour': None,
365 'fillstyle': wx.SOLID}
366
367 def __init__(self, points, **attr):
368 """Creates several bars.
369 points - sequence (array, tuple or list) of (x,y) points
370 indicating *top left* corner of bar. Can also be
371 just one (x,y) tuple if labels attribute is just
372 a string (not a list)
373 **attr - key word attributes
374 Defaults:
375 'colour'= wx.BLACK, - wx.Pen Colour
376 'width'= 1, - Pen width
377 'size'= 2, - Bar size
378 'fillcolour'= same as colour, - wx.Brush Colour
379 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
380 'legend'= '' - string used for PolyBar legend
381 'labels'= '' - string if only one point, or list of labels, one per point
382 Note that if no legend is given the label is used.
383 """
384 barPoints = [(0,0)] # needed so height of bar can be determined
385 self.labels = []
386 # add labels and points to above data members:
387 try:
388 labels = attr['labels']
389 del attr['labels']
390 except:
391 labels = None
392 if labels is None: # figure out if we have 1 point or many
393 try:
394 points[0][0] # ok: we have a seq of points
395 barPoints.extend(points)
396 except: # failed, so have just one point:
397 barPoints.append(points)
398 elif isinstance(labels, list) or isinstance(labels, tuple):
399 # labels is a sequence so points must be too, with same length:
400 self.labels.extend(labels)
401 barPoints.extend(points)
402 if len(labels) != len(points):
403 msg = "%d bar labels missing" % (len(points)-len(labels))
404 raise ValueError, msg
405 else: # label given, but only one, so must be only one point:
406 barPoints.append(points)
407 self.labels.append(labels)
408
409 PolyPoints.__init__(self, barPoints, attr)
410
411 def draw(self, dc, printerScale, coord= None):
412 colour = self.attributes['colour']
413 width = self.attributes['width'] * printerScale
414 size = self.attributes['size'] * printerScale
415 fillcolour = self.attributes['fillcolour']
416 fillstyle = self.attributes['fillstyle']
417
418 dc.SetPen(wx.Pen(colour,int(width)))
419 if fillcolour:
420 dc.SetBrush(wx.Brush(fillcolour,fillstyle))
421 else:
422 dc.SetBrush(wx.Brush(colour, fillstyle))
423 if coord == None:
424 self._drawbar(dc, self.scaled, size)
425 else:
426 self._drawLegendMarker(dc, coord, size) #draw legend marker
427
428 def _drawLegendMarker(self, dc, coord, size):
429 fact= 10
430 wh= 2*fact
431 rect= _Numeric.zeros((len(coord),4),_Numeric.Float)+[0.0,0.0,wh,wh]
432 rect[:,0:2]= coord-[fact,fact]
433 dc.DrawRectangleList(rect.astype(_Numeric.Int32))
434
435 def offset(self, heights, howMany = 1, shift=0.25):
436 """Return a list of points, where x's are those of this bar,
437 shifted by shift*howMany (in caller's units, not pixel units), and
438 heights are given in heights."""
439 points = [(point[0][0]+shift*howMany,point[1]) for point
440 in zip(self.points[1:], heights)]
441 return points
442
443 def getSymExtent(self, printerScale):
444 """Width and Height of bar symbol"""
445 s= 5*self.attributes['size'] * printerScale
446 return (s,s)
447
448 def getLegend(self):
449 """This returns a comma-separated list of bar labels (one string)"""
450 try: legendItem = self.attributes['legend']
451 except:
452 legendItem = ", ".join(self.labels)
453 return legendItem
454
455 def _drawbar(self, dc, coords, size=1):
456 width = 5.0*size
457 y0 = coords[0][1]
458 bars = []
459 for coord in coords[1:]:
460 x = coord[0] # - width/2 not as good
461 y = coord[1]
462 height = int(y0) - int(y)
463 bars.append((int(x),int(y),int(width),int(height)))
464 #print x,y,width, height
465 dc.DrawRectangleList(bars)
466
467 def getTicks(self):
468 """Get the list of (tick pos, label) pairs for this PolyBar.
469 It is unlikely you need this, but it is used by
470 getBarTicksGen."""
471 ticks = []
472 # remember to skip first point:
473 for point, label in zip(self.points[1:], self.labels):
474 ticks.append((point[0], label))
475 return ticks
476
477def getBarTicksGen(*polyBars):
478 """Get a function that can be given as xTicks argument when
479 calling PolyCanvas.Draw() when plotting one or more PolyBar.
480 The returned function allows the bar chart to have the bars
481 labelled at the x axis. """
482 ticks = []
483 for polyBar in polyBars:
484 ticks.extend(polyBar.getTicks())
485
486 def tickGenerator(lower,upper,gg,ff):
487 tickPairs = []
488 for tick in ticks:
489 if lower <= tick[0] < upper:
490 tickPairs.append(tick)
491 return tickPairs
492
493 return tickGenerator
494
495
9d6685e2
RD
496class PlotGraphics:
497 """Container to hold PolyXXX objects and graph labels
498 - All methods except __init__ are private.
499 """
500
501 def __init__(self, objects, title='', xLabel='', yLabel= ''):
502 """Creates PlotGraphics object
503 objects - list of PolyXXX objects to make graph
504 title - title shown at top of graph
505 xLabel - label shown on x-axis
506 yLabel - label shown on y-axis
507 """
508 if type(objects) not in [list,tuple]:
509 raise TypeError, "objects argument should be list or tuple"
510 self.objects = objects
511 self.title= title
512 self.xLabel= xLabel
513 self.yLabel= yLabel
514
515 def boundingBox(self):
516 p1, p2 = self.objects[0].boundingBox()
517 for o in self.objects[1:]:
518 p1o, p2o = o.boundingBox()
b31cbeb9
RD
519 p1 = _Numeric.minimum(p1, p1o)
520 p2 = _Numeric.maximum(p2, p2o)
9d6685e2
RD
521 return p1, p2
522
523 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
524 for o in self.objects:
525 o.scaleAndShift(scale, shift)
526
527 def setPrinterScale(self, scale):
528 """Thickens up lines and markers only for printing"""
529 self.printerScale= scale
530
531 def setXLabel(self, xLabel= ''):
532 """Set the X axis label on the graph"""
533 self.xLabel= xLabel
534
535 def setYLabel(self, yLabel= ''):
536 """Set the Y axis label on the graph"""
537 self.yLabel= yLabel
538
539 def setTitle(self, title= ''):
540 """Set the title at the top of graph"""
541 self.title= title
542
543 def getXLabel(self):
544 """Get x axis label string"""
545 return self.xLabel
546
547 def getYLabel(self):
548 """Get y axis label string"""
549 return self.yLabel
550
551 def getTitle(self, title= ''):
552 """Get the title at the top of graph"""
553 return self.title
554
555 def draw(self, dc):
556 for o in self.objects:
b31cbeb9 557 #t=_time.clock() # profile info
9d6685e2 558 o.draw(dc, self.printerScale)
b31cbeb9 559 #dt= _time.clock()-t
9d6685e2
RD
560 #print o, "time=", dt
561
562 def getSymExtent(self, printerScale):
563 """Get max width and height of lines and markers symbols for legend"""
564 symExt = self.objects[0].getSymExtent(printerScale)
565 for o in self.objects[1:]:
566 oSymExt = o.getSymExtent(printerScale)
b31cbeb9 567 symExt = _Numeric.maximum(symExt, oSymExt)
9d6685e2
RD
568 return symExt
569
570 def getLegendNames(self):
571 """Returns list of legend names"""
572 lst = [None]*len(self)
573 for i in range(len(self)):
574 lst[i]= self.objects[i].getLegend()
575 return lst
576
577 def __len__(self):
578 return len(self.objects)
579
580 def __getitem__(self, item):
581 return self.objects[item]
582
583
584#-------------------------------------------------------------------------------
585# Main window that you will want to import into your application.
586
587class PlotCanvas(wx.Window):
588 """Subclass of a wx.Window to allow simple general plotting
589 of data with zoom, labels, and automatic axis scaling."""
590
591 def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
592 size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
593 """Constucts a window, which can be a child of a frame, dialog or
594 any other non-control window"""
595
596 wx.Window.__init__(self, parent, id, pos, size, style, name)
597 self.border = (1,1)
598
599 self.SetBackgroundColour("white")
600
9d6685e2
RD
601 # Create some mouse events for zooming
602 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
603 self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
604 self.Bind(wx.EVT_MOTION, self.OnMotion)
605 self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
606 self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
607
608 # set curser as cross-hairs
609 self.SetCursor(wx.CROSS_CURSOR)
610
611 # Things for printing
612 self.print_data = wx.PrintData()
613 self.print_data.SetPaperId(wx.PAPER_LETTER)
614 self.print_data.SetOrientation(wx.LANDSCAPE)
615 self.pageSetupData= wx.PageSetupDialogData()
616 self.pageSetupData.SetMarginBottomRight((25,25))
617 self.pageSetupData.SetMarginTopLeft((25,25))
618 self.pageSetupData.SetPrintData(self.print_data)
619 self.printerScale = 1
620 self.parent= parent
621
622 # Zooming variables
623 self._zoomInFactor = 0.5
624 self._zoomOutFactor = 2
b31cbeb9
RD
625 self._zoomCorner1= _Numeric.array([0.0, 0.0]) # left mouse down corner
626 self._zoomCorner2= _Numeric.array([0.0, 0.0]) # left mouse up corner
9d6685e2
RD
627 self._zoomEnabled= False
628 self._hasDragged= False
629
630 # Drawing Variables
ecf0b9f9 631 self._drawCmd = None
9d6685e2
RD
632 self._pointScale= 1
633 self._pointShift= 0
634 self._xSpec= 'auto'
635 self._ySpec= 'auto'
636 self._gridEnabled= False
637 self._legendEnabled= False
638
639 # Fonts
640 self._fontCache = {}
641 self._fontSizeAxis= 10
642 self._fontSizeTitle= 15
643 self._fontSizeLegend= 7
644
b31cbeb9
RD
645 # pointLabels
646 self._pointLabelEnabled= False
647 self.last_PointLabel= None
648 self._pointLabelFunc= None
649 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
650
da8d6ffa
RD
651 self.Bind(wx.EVT_PAINT, self.OnPaint)
652 self.Bind(wx.EVT_SIZE, self.OnSize)
9d6685e2
RD
653 # OnSize called to make sure the buffer is initialized.
654 # This might result in OnSize getting called twice on some
655 # platforms at initialization, but little harm done.
b31cbeb9
RD
656 if wx.Platform != "__WXMAC__":
657 self.OnSize(None) # sets the initial size based on client size
9d6685e2
RD
658
659
660 # SaveFile
661 def SaveFile(self, fileName= ''):
662 """Saves the file to the type specified in the extension. If no file
663 name is specified a dialog box is provided. Returns True if sucessful,
664 otherwise False.
665
666 .bmp Save a Windows bitmap file.
667 .xbm Save an X bitmap file.
668 .xpm Save an XPM bitmap file.
669 .png Save a Portable Network Graphics file.
670 .jpg Save a Joint Photographic Experts Group file.
671 """
b31cbeb9 672 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
9d6685e2
RD
673 dlg1 = wx.FileDialog(
674 self,
675 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
676 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
677 wx.SAVE|wx.OVERWRITE_PROMPT
678 )
679 try:
680 while 1:
681 if dlg1.ShowModal() == wx.ID_OK:
682 fileName = dlg1.GetPath()
683 # Check for proper exension
b31cbeb9 684 if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
9d6685e2
RD
685 dlg2 = wx.MessageDialog(self, 'File name extension\n'
686 'must be one of\n'
687 'bmp, xbm, xpm, png, or jpg',
688 'File Name Error', wx.OK | wx.ICON_ERROR)
689 try:
690 dlg2.ShowModal()
691 finally:
692 dlg2.Destroy()
693 else:
694 break # now save file
695 else: # exit without saving
696 return False
697 finally:
698 dlg1.Destroy()
699
700 # File name has required extension
b31cbeb9 701 fType = _string.lower(fileName[-3:])
9d6685e2
RD
702 if fType == "bmp":
703 tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
704 elif fType == "xbm":
705 tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
706 elif fType == "xpm":
707 tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
708 elif fType == "jpg":
709 tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
710 else:
711 tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
712 # Save Bitmap
713 res= self._Buffer.SaveFile(fileName, tp)
714 return res
715
716 def PageSetup(self):
717 """Brings up the page setup dialog"""
718 data = self.pageSetupData
719 data.SetPrintData(self.print_data)
720 dlg = wx.PageSetupDialog(self.parent, data)
721 try:
722 if dlg.ShowModal() == wx.ID_OK:
723 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
724 # updates page parameters from dialog
725 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
726 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
727 self.pageSetupData.SetPrintData(data.GetPrintData())
728 self.print_data=data.GetPrintData() # updates print_data
729 finally:
730 dlg.Destroy()
731
732 def Printout(self, paper=None):
733 """Print current plot."""
734 if paper != None:
735 self.print_data.SetPaperId(paper)
736 pdd = wx.PrintDialogData()
737 pdd.SetPrintData(self.print_data)
738 printer = wx.Printer(pdd)
739 out = PlotPrintout(self)
740 print_ok = printer.Print(self.parent, out)
741 if print_ok:
742 self.print_data = printer.GetPrintDialogData().GetPrintData()
743 out.Destroy()
744
745 def PrintPreview(self):
746 """Print-preview current plot."""
747 printout = PlotPrintout(self)
748 printout2 = PlotPrintout(self)
749 self.preview = wx.PrintPreview(printout, printout2, self.print_data)
750 if not self.preview.Ok():
751 wx.MessageDialog(self, "Print Preview failed.\n" \
752 "Check that default printer is configured\n", \
753 "Print error", wx.OK|wx.CENTRE).ShowModal()
b31cbeb9 754 self.preview.SetZoom(40)
9d6685e2
RD
755 # search up tree to find frame instance
756 frameInst= self
757 while not isinstance(frameInst, wx.Frame):
758 frameInst= frameInst.GetParent()
759 frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
760 frame.Initialize()
761 frame.SetPosition(self.GetPosition())
b31cbeb9 762 frame.SetSize((600,550))
9d6685e2
RD
763 frame.Centre(wx.BOTH)
764 frame.Show(True)
765
766 def SetFontSizeAxis(self, point= 10):
767 """Set the tick and axis label font size (default is 10 point)"""
768 self._fontSizeAxis= point
769
770 def GetFontSizeAxis(self):
771 """Get current tick and axis label font size in points"""
772 return self._fontSizeAxis
773
774 def SetFontSizeTitle(self, point= 15):
775 """Set Title font size (default is 15 point)"""
776 self._fontSizeTitle= point
777
778 def GetFontSizeTitle(self):
779 """Get current Title font size in points"""
780 return self._fontSizeTitle
781
782 def SetFontSizeLegend(self, point= 7):
783 """Set Legend font size (default is 7 point)"""
784 self._fontSizeLegend= point
785
786 def GetFontSizeLegend(self):
787 """Get current Legend font size in points"""
788 return self._fontSizeLegend
789
790 def SetEnableZoom(self, value):
791 """Set True to enable zooming."""
792 if value not in [True,False]:
793 raise TypeError, "Value should be True or False"
794 self._zoomEnabled= value
795
796 def GetEnableZoom(self):
797 """True if zooming enabled."""
798 return self._zoomEnabled
799
800 def SetEnableGrid(self, value):
801 """Set True to enable grid."""
802 if value not in [True,False]:
803 raise TypeError, "Value should be True or False"
804 self._gridEnabled= value
805 self.Redraw()
806
807 def GetEnableGrid(self):
808 """True if grid enabled."""
809 return self._gridEnabled
810
811 def SetEnableLegend(self, value):
812 """Set True to enable legend."""
813 if value not in [True,False]:
814 raise TypeError, "Value should be True or False"
815 self._legendEnabled= value
816 self.Redraw()
817
818 def GetEnableLegend(self):
819 """True if Legend enabled."""
820 return self._legendEnabled
821
b31cbeb9
RD
822 def SetEnablePointLabel(self, value):
823 """Set True to enable pointLabel."""
824 if value not in [True,False]:
825 raise TypeError, "Value should be True or False"
826 self._pointLabelEnabled= value
827 self.Redraw() #will erase existing pointLabel if present
828 self.last_PointLabel = None
829
830 def GetEnablePointLabel(self):
831 """True if pointLabel enabled."""
832 return self._pointLabelEnabled
833
834 def SetPointLabelFunc(self, func):
835 """Sets the function with custom code for pointLabel drawing
836 ******** more info needed ***************
837 """
838 self._pointLabelFunc= func
839
840 def GetPointLabelFunc(self):
841 """Returns pointLabel Drawing Function"""
842 return self._pointLabelFunc
843
9d6685e2
RD
844 def Reset(self):
845 """Unzoom the plot."""
b31cbeb9 846 self.last_PointLabel = None #reset pointLabel
ecf0b9f9
RD
847 if self.BeenDrawn():
848 self._drawCmd.resetAxes(self._xSpec, self._ySpec)
849 self._draw()
9d6685e2
RD
850
851 def ScrollRight(self, units):
852 """Move view right number of axis units."""
b31cbeb9 853 self.last_PointLabel = None #reset pointLabel
ecf0b9f9
RD
854 if self.BeenDrawn():
855 self._drawCmd.scrollAxisX(units, self._xSpec)
856 self._draw()
9d6685e2
RD
857
858 def ScrollUp(self, units):
859 """Move view up number of axis units."""
b31cbeb9 860 self.last_PointLabel = None #reset pointLabel
ecf0b9f9
RD
861 if self.BeenDrawn():
862 self._drawCmd.scrollAxisY(units, self._ySpec)
863 self._draw()
9d6685e2
RD
864
865 def GetXY(self,event):
866 """Takes a mouse event and returns the XY user axis values."""
b31cbeb9 867 x,y= self.PositionScreenToUser(event.GetPosition())
9d6685e2
RD
868 return x,y
869
b31cbeb9
RD
870 def PositionUserToScreen(self, pntXY):
871 """Converts User position to Screen Coordinates"""
872 userPos= _Numeric.array(pntXY)
873 x,y= userPos * self._pointScale + self._pointShift
874 return x,y
875
876 def PositionScreenToUser(self, pntXY):
877 """Converts Screen position to User Coordinates"""
878 screenPos= _Numeric.array(pntXY)
879 x,y= (screenPos-self._pointShift)/self._pointScale
880 return x,y
881
9d6685e2
RD
882 def SetXSpec(self, type= 'auto'):
883 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
884 where:
885 'none' - shows no axis or tick mark values
886 'min' - shows min bounding box values
887 'auto' - rounds axis range to sensible values
888 """
889 self._xSpec= type
890
891 def SetYSpec(self, type= 'auto'):
892 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
893 where:
894 'none' - shows no axis or tick mark values
895 'min' - shows min bounding box values
896 'auto' - rounds axis range to sensible values
897 """
898 self._ySpec= type
899
900 def GetXSpec(self):
901 """Returns current XSpec for axis"""
902 return self._xSpec
903
904 def GetYSpec(self):
905 """Returns current YSpec for axis"""
906 return self._ySpec
907
908 def GetXMaxRange(self):
909 """Returns (minX, maxX) x-axis range for displayed graph"""
ecf0b9f9 910 return self._drawCmd.getXMaxRange()
9d6685e2
RD
911
912 def GetYMaxRange(self):
913 """Returns (minY, maxY) y-axis range for displayed graph"""
ecf0b9f9 914 return self._drawCmd.getYMaxRange()
9d6685e2
RD
915
916 def GetXCurrentRange(self):
917 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
ecf0b9f9 918 return self._drawCmd.xAxis
9d6685e2
RD
919
920 def GetYCurrentRange(self):
921 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
ecf0b9f9 922 return self._drawCmd.yAxis
9d6685e2 923
ecf0b9f9
RD
924 def BeenDrawn(self):
925 """Return true if Draw() has been called once, false otherwise."""
926 return self._drawCmd is not None
927
928 def Draw(self, graphics, xAxis = None, yAxis = None, dc = None,
929 xTicks = None, yTicks = None):
9d6685e2
RD
930 """Draw objects in graphics with specified x and y axis.
931 graphics- instance of PlotGraphics with list of PolyXXX objects
932 xAxis - tuple with (min, max) axis range to view
933 yAxis - same as xAxis
934 dc - drawing context - doesn't have to be specified.
ecf0b9f9
RD
935 If it's not, the offscreen buffer is used.
936 xTicks and yTicks - can be either
937 - a list of floats where the ticks should be, only visible
938 ticks will be used;
939 - a function that generates a list of (tick, label) pairs;
940 The function must be of form func(lower, upper, grid,
941 format), and the pairs must contain only ticks between lower and
942 upper. Function can use grid and format that are computed by
943 PlotCanvas, if desired, where grid is the spacing between ticks,
944 and format is the format string for how tick labels
945 will appear. See tickGeneratorDefault and
946 tickGeneratorFromList for examples of such functions.
9d6685e2
RD
947 """
948 # check Axis is either tuple or none
949 if type(xAxis) not in [type(None),tuple]:
950 raise TypeError, "xAxis should be None or (minX,maxX)"
951 if type(yAxis) not in [type(None),tuple]:
952 raise TypeError, "yAxis should be None or (minY,maxY)"
953
ecf0b9f9
RD
954 self._drawCmd = DrawCmd(graphics, xAxis, yAxis,
955 self._xSpec, self._ySpec,
956 xTicks, yTicks)
957 self._draw(dc)
958
959 def _draw(self, dc=None):
960 """Implement the draw command, with dc if given, using any new
961 settings (legend toggled, axis specs, zoom, etc). """
962 assert self._drawCmd is not None
963
9d6685e2 964 # check case for axis = (a,b) where a==b caused by improper zooms
ecf0b9f9 965 if self._drawCmd.isEmpty():
9d6685e2
RD
966 return
967
968 if dc == None:
b31cbeb9 969 # sets new dc and clears it
00b9c867 970 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
9d6685e2
RD
971 dc.Clear()
972
973 dc.BeginDrawing()
974 # dc.Clear()
975
976 # set font size for every thing but title and legend
977 dc.SetFont(self._getFont(self._fontSizeAxis))
978
9d6685e2
RD
979 # Get ticks and textExtents for axis if required
980 if self._xSpec is not 'none':
ecf0b9f9 981 xticks = self._drawCmd.getTicksX()
9d6685e2
RD
982 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
983 else:
984 xticks = None
985 xTextExtent= (0,0) # No text for ticks
986 if self._ySpec is not 'none':
ecf0b9f9 987 yticks = self._drawCmd.getTicksY()
9d6685e2
RD
988 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
989 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
990 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
991 max(yTextExtentBottom[1],yTextExtentTop[1]))
992 else:
993 yticks = None
994 yTextExtent= (0,0) # No text for ticks
995
996 # TextExtents for Title and Axis Labels
ecf0b9f9 997 graphics = self._drawCmd.graphics
9d6685e2
RD
998 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
999
1000 # TextExtents for Legend
ecf0b9f9
RD
1001 legendBoxWH, legendSymExt, legendHGap, legendTextExt \
1002 = self._legendWH(dc, graphics)
9d6685e2
RD
1003
1004 # room around graph area
1005 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
1006 lhsW= yTextExtent[0]+ yLabelWH[1]
1007 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
1008 topH= yTextExtent[1]/2. + titleWH[1]
b31cbeb9
RD
1009 textSize_scale= _Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
1010 textSize_shift= _Numeric.array([lhsW, bottomH]) # shift plot area by this amount
9d6685e2
RD
1011
1012 # drawing title and labels text
1013 dc.SetFont(self._getFont(self._fontSizeTitle))
1014 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
1015 self.plotbox_origin[1]- self.plotbox_size[1])
1016 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
1017 dc.SetFont(self._getFont(self._fontSizeAxis))
1018 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
1019 self.plotbox_origin[1]- xLabelWH[1])
1020 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
1021 yLabelPos= (self.plotbox_origin[0],
1022 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
1023 if graphics.getYLabel(): # bug fix for Linux
1024 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
1025
1026 # drawing legend makers and text
1027 if self._legendEnabled:
ecf0b9f9
RD
1028 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH,
1029 legendSymExt, legendHGap, legendTextExt)
1030
1031 #sizes axis to axis type, create lower left and upper right corners of plot
1032 p1, p2 = self._drawCmd.getBoundingBox()
9d6685e2
RD
1033
1034 # allow for scaling and shifting plotted points
b31cbeb9
RD
1035 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _Numeric.array((1,-1))
1036 shift = -p1*scale + self.plotbox_origin + textSize_shift * _Numeric.array((1,-1))
9d6685e2
RD
1037 self._pointScale= scale # make available for mouse events
1038 self._pointShift= shift
1039 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
1040
1041 graphics.scaleAndShift(scale, shift)
1042 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
1043
1044 # set clipping area so drawing does not occur outside axis box
1045 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
1046 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
1047 # Draw the lines and markers
b31cbeb9 1048 #start = _time.clock()
9d6685e2 1049 graphics.draw(dc)
b31cbeb9 1050 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
9d6685e2
RD
1051 # remove the clipping region
1052 dc.DestroyClippingRegion()
1053 dc.EndDrawing()
1054
1055 def Redraw(self, dc= None):
1056 """Redraw the existing plot."""
ecf0b9f9
RD
1057 if self.BeenDrawn():
1058 self._draw(dc)
9d6685e2
RD
1059
1060 def Clear(self):
1061 """Erase the window."""
b31cbeb9 1062 self.last_PointLabel = None #reset pointLabel
9d6685e2
RD
1063 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
1064 dc.Clear()
ecf0b9f9 1065 self._drawCmd = None
9d6685e2
RD
1066
1067 def Zoom(self, Center, Ratio):
1068 """ Zoom on the plot
1069 Centers on the X,Y coords given in Center
1070 Zooms by the Ratio = (Xratio, Yratio) given
1071 """
b31cbeb9 1072 self.last_PointLabel = None #reset maker
ecf0b9f9
RD
1073 if self.BeenDrawn():
1074 self._drawCmd.zoom(Center, Ratio)
1075 self._draw()
9d6685e2 1076
ecf0b9f9
RD
1077 def ChangeAxes(self, xAxis, yAxis):
1078 """Change axes specified at last Draw() command. If
1079 Draw() never called yet, RuntimeError raised. """
1080 if not self.BeenDrawn():
1081 raise RuntimeError, "Must call Draw() at least once"
1082 self._drawCmd.changeAxisX(xAxis, self._xSpec)
1083 self._drawCmd.changeAxisY(yAxis, self._ySpec)
1084 self._draw()
1085
b31cbeb9
RD
1086 def GetClosestPoints(self, pntXY, pointScaled= True):
1087 """Returns list with
1088 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1089 list for each curve.
1090 Returns [] if no curves are being plotted.
1091
1092 x, y in user coords
1093 if pointScaled == True based on screen coords
1094 if pointScaled == False based on user coords
1095 """
ecf0b9f9
RD
1096 if self.BeenDrawn():
1097 return self._drawCmd.getClosestPoints(pntXY, pointScaled)
1098 else:
b31cbeb9
RD
1099 #no graph available
1100 return []
b31cbeb9
RD
1101
1102 def GetClosetPoint(self, pntXY, pointScaled= True):
1103 """Returns list with
1104 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1105 list for only the closest curve.
1106 Returns [] if no curves are being plotted.
1107
1108 x, y in user coords
1109 if pointScaled == True based on screen coords
1110 if pointScaled == False based on user coords
1111 """
1112 #closest points on screen based on screen scaling (pointScaled= True)
1113 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
1114 closestPts= self.GetClosestPoints(pntXY, pointScaled)
1115 if closestPts == []:
1116 return [] #no graph present
1117 #find one with least distance
1118 dists = [c[-1] for c in closestPts]
1119 mdist = min(dists) #Min dist
1120 i = dists.index(mdist) #index for min dist
1121 return closestPts[i] #this is the closest point on closest curve
1122
1123 def UpdatePointLabel(self, mDataDict):
1124 """Updates the pointLabel point on screen with data contained in
1125 mDataDict.
1126
1127 mDataDict will be passed to your function set by
1128 SetPointLabelFunc. It can contain anything you
1129 want to display on the screen at the scaledXY point
1130 you specify.
1131
1132 This function can be called from parent window with onClick,
1133 onMotion events etc.
1134 """
1135 if self.last_PointLabel != None:
1136 #compare pointXY
1137 if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
1138 #closest changed
1139 self._drawPointLabel(self.last_PointLabel) #erase old
1140 self._drawPointLabel(mDataDict) #plot new
1141 else:
1142 #just plot new with no erase
1143 self._drawPointLabel(mDataDict) #plot new
1144 #save for next erase
1145 self.last_PointLabel = mDataDict
9d6685e2
RD
1146
1147 # event handlers **********************************
1148 def OnMotion(self, event):
ecf0b9f9 1149 if not self.BeenDrawn(): return
9d6685e2
RD
1150 if self._zoomEnabled and event.LeftIsDown():
1151 if self._hasDragged:
1152 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1153 else:
1154 self._hasDragged= True
1155 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
1156 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
1157
1158 def OnMouseLeftDown(self,event):
ecf0b9f9
RD
1159 if self.BeenDrawn():
1160 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
9d6685e2
RD
1161
1162 def OnMouseLeftUp(self, event):
ecf0b9f9 1163 if not self.BeenDrawn(): return
9d6685e2
RD
1164 if self._zoomEnabled:
1165 if self._hasDragged == True:
1166 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
1167 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
1168 self._hasDragged = False # reset flag
b31cbeb9
RD
1169 minX, minY= _Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
1170 maxX, maxY= _Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
1171 self.last_PointLabel = None #reset pointLabel
ecf0b9f9
RD
1172 self._drawCmd.changeAxisX((minX,maxX))
1173 self._drawCmd.changeAxisY((minY,maxY))
1174 self._draw()
9d6685e2
RD
1175 #else: # A box has not been drawn, zoom in on a point
1176 ## this interfered with the double click, so I've disables it.
1177 # X,Y = self.GetXY(event)
1178 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1179
1180 def OnMouseDoubleClick(self,event):
1181 if self._zoomEnabled:
1182 self.Reset()
1183
1184 def OnMouseRightDown(self,event):
1185 if self._zoomEnabled:
1186 X,Y = self.GetXY(event)
1187 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
1188
1189 def OnPaint(self, event):
1190 # All that is needed here is to draw the buffer to screen
b31cbeb9
RD
1191 if self.last_PointLabel != None:
1192 self._drawPointLabel(self.last_PointLabel) #erase old
1193 self.last_PointLabel = None
1194 dc = wx.BufferedPaintDC(self, self._Buffer)
9d6685e2
RD
1195
1196 def OnSize(self,event):
1197 # The Buffer init is done here, to make sure the buffer is always
1198 # the same size as the Window
1199 Size = self.GetClientSize()
1200
1201 # Make new offscreen bitmap: this bitmap will always have the
1202 # current drawing in it, so it can be used to save the image to
1203 # a file, or whatever.
1204 self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
1205 self._setSize()
b31cbeb9
RD
1206
1207 self.last_PointLabel = None #reset pointLabel
1208
ecf0b9f9
RD
1209 if self.BeenDrawn():
1210 self._draw()
9d6685e2 1211 else:
ecf0b9f9 1212 self.Clear()
9d6685e2 1213
b31cbeb9
RD
1214 def OnLeave(self, event):
1215 """Used to erase pointLabel when mouse outside window"""
1216 if self.last_PointLabel != None:
1217 self._drawPointLabel(self.last_PointLabel) #erase old
1218 self.last_PointLabel = None
1219
9d6685e2
RD
1220
1221 # Private Methods **************************************************
1222 def _setSize(self, width=None, height=None):
1223 """DC width and height."""
1224 if width == None:
1225 (self.width,self.height) = self.GetClientSize()
1226 else:
1227 self.width, self.height= width,height
b31cbeb9 1228 self.plotbox_size = 0.97*_Numeric.array([self.width, self.height])
9d6685e2
RD
1229 xo = 0.5*(self.width-self.plotbox_size[0])
1230 yo = self.height-0.5*(self.height-self.plotbox_size[1])
b31cbeb9 1231 self.plotbox_origin = _Numeric.array([xo, yo])
9d6685e2
RD
1232
1233 def _setPrinterScale(self, scale):
1234 """Used to thicken lines and increase marker size for print out."""
1235 # line thickness on printer is very thin at 600 dot/in. Markers small
1236 self.printerScale= scale
1237
1238 def _printDraw(self, printDC):
1239 """Used for printing."""
ecf0b9f9
RD
1240 if self.BeenDrawn():
1241 self._draw(printDC)
9d6685e2 1242
b31cbeb9
RD
1243 def _drawPointLabel(self, mDataDict):
1244 """Draws and erases pointLabels"""
1245 width = self._Buffer.GetWidth()
1246 height = self._Buffer.GetHeight()
1247 tmp_Buffer = wx.EmptyBitmap(width,height)
1248 dcs = wx.MemoryDC()
1249 dcs.SelectObject(tmp_Buffer)
1250 dcs.Clear()
1251 dcs.BeginDrawing()
1252 self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
1253 dcs.EndDrawing()
1254
1255 dc = wx.ClientDC( self )
1256 #this will erase if called twice
1257 dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
1258
1259
ecf0b9f9 1260 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, hgap, legendTextExt):
9d6685e2
RD
1261 """Draws legend symbols and text"""
1262 # top right hand corner of graph box is ref corner
1263 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
1264 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
1265 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
1266 dc.SetFont(self._getFont(self._fontSizeLegend))
ecf0b9f9
RD
1267 legendIdx = 0
1268 for gr in graphics:
1269 s = legendIdx * lineHeight
1270 drew = gr.drawLegend(dc, self.printerScale,
1271 legendSymExt[0], hgap, legendTextExt[1],
1272 trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
1273 if drew:
1274 legendIdx += 1
9d6685e2
RD
1275 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
1276
1277 def _titleLablesWH(self, dc, graphics):
1278 """Draws Title and labels and returns width and height for each"""
1279 # TextExtents for Title and Axis Labels
1280 dc.SetFont(self._getFont(self._fontSizeTitle))
1281 title= graphics.getTitle()
1282 titleWH= dc.GetTextExtent(title)
1283 dc.SetFont(self._getFont(self._fontSizeAxis))
1284 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
1285 xLabelWH= dc.GetTextExtent(xLabel)
1286 yLabelWH= dc.GetTextExtent(yLabel)
1287 return titleWH, xLabelWH, yLabelWH
1288
1289 def _legendWH(self, dc, graphics):
1290 """Returns the size in screen units for legend box"""
1291 if self._legendEnabled != True:
1292 legendBoxWH= symExt= txtExt= (0,0)
ecf0b9f9 1293 hgap= 0
9d6685e2 1294 else:
ecf0b9f9 1295 #find max symbol size and gap b/w symbol and text
9d6685e2 1296 symExt= graphics.getSymExtent(self.printerScale)
ecf0b9f9 1297 hgap = symExt[0]*0.1
9d6685e2
RD
1298 # find max legend text extent
1299 dc.SetFont(self._getFont(self._fontSizeLegend))
1300 txtList= graphics.getLegendNames()
1301 txtExt= dc.GetTextExtent(txtList[0])
1302 for txt in graphics.getLegendNames()[1:]:
b31cbeb9 1303 txtExt= _Numeric.maximum(txtExt,dc.GetTextExtent(txt))
ecf0b9f9 1304 maxW= symExt[0]+hgap+txtExt[0]
9d6685e2
RD
1305 maxH= max(symExt[1],txtExt[1])
1306 # padding .1 for lhs of legend box and space between lines
1307 maxW= maxW* 1.1
1308 maxH= maxH* 1.1 * len(txtList)
1309 dc.SetFont(self._getFont(self._fontSizeAxis))
1310 legendBoxWH= (maxW,maxH)
ecf0b9f9 1311 return (legendBoxWH, symExt, hgap, txtExt)
9d6685e2
RD
1312
1313 def _drawRubberBand(self, corner1, corner2):
1314 """Draws/erases rect box from corner1 to corner2"""
1315 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
1316 # draw rectangle
1317 dc = wx.ClientDC( self )
1318 dc.BeginDrawing()
1319 dc.SetPen(wx.Pen(wx.BLACK))
1320 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
1321 dc.SetLogicalFunction(wx.INVERT)
d7403ad2 1322 dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
9d6685e2
RD
1323 dc.SetLogicalFunction(wx.COPY)
1324 dc.EndDrawing()
1325
1326 def _getFont(self,size):
1327 """Take font size, adjusts if printing and returns wx.Font"""
1328 s = size*self.printerScale
1329 of = self.GetFont()
1330 # Linux speed up to get font from cache rather than X font server
1331 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
1332 font = self._fontCache.get (key, None)
1333 if font:
1334 return font # yeah! cache hit
1335 else:
1336 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1337 self._fontCache[key] = font
1338 return font
1339
1340
1341 def _point2ClientCoord(self, corner1, corner2):
1342 """Converts user point coords to client screen int coords x,y,width,height"""
b31cbeb9
RD
1343 c1= _Numeric.array(corner1)
1344 c2= _Numeric.array(corner2)
9d6685e2
RD
1345 # convert to screen coords
1346 pt1= c1*self._pointScale+self._pointShift
1347 pt2= c2*self._pointScale+self._pointShift
1348 # make height and width positive
b31cbeb9
RD
1349 pul= _Numeric.minimum(pt1,pt2) # Upper left corner
1350 plr= _Numeric.maximum(pt1,pt2) # Lower right corner
9d6685e2
RD
1351 rectWidth, rectHeight= plr-pul
1352 ptx,pty= pul
00b9c867 1353 return ptx, pty, rectWidth, rectHeight
9d6685e2
RD
1354
1355 def _axisInterval(self, spec, lower, upper):
1356 """Returns sensible axis range for given spec"""
1357 if spec == 'none' or spec == 'min':
1358 if lower == upper:
1359 return lower-0.5, upper+0.5
1360 else:
1361 return lower, upper
1362 elif spec == 'auto':
1363 range = upper-lower
1364 if range == 0.:
1365 return lower-0.5, upper+0.5
b31cbeb9
RD
1366 log = _Numeric.log10(range)
1367 power = _Numeric.floor(log)
9d6685e2
RD
1368 fraction = log-power
1369 if fraction <= 0.05:
1370 power = power-1
1371 grid = 10.**power
1372 lower = lower - lower % grid
1373 mod = upper % grid
1374 if mod != 0:
1375 upper = upper - mod + grid
1376 return lower, upper
1377 elif type(spec) == type(()):
1378 lower, upper = spec
1379 if lower <= upper:
1380 return lower, upper
1381 else:
1382 return upper, lower
1383 else:
1384 raise ValueError, str(spec) + ': illegal axis specification'
1385
1386 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1387
1388 penWidth= self.printerScale # increases thickness for printing only
ecf0b9f9 1389 dc.SetPen(wx.Pen(wx.BLACK, penWidth))
9d6685e2
RD
1390
1391 # set length of tick marks--long ones make grid
1392 if self._gridEnabled:
1393 x,y,width,height= self._point2ClientCoord(p1,p2)
1394 yTickLength= width/2.0 +1
1395 xTickLength= height/2.0 +1
1396 else:
1397 yTickLength= 3 * self.printerScale # lengthens lines for printing
1398 xTickLength= 3 * self.printerScale
1399
1400 if self._xSpec is not 'none':
1401 lower, upper = p1[0],p2[0]
1402 text = 1
1403 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
b31cbeb9
RD
1404 a1 = scale*_Numeric.array([lower, y])+shift
1405 a2 = scale*_Numeric.array([upper, y])+shift
9d6685e2
RD
1406 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1407 for x, label in xticks:
b31cbeb9 1408 pt = scale*_Numeric.array([x, y])+shift
9d6685e2
RD
1409 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1410 if text:
1411 dc.DrawText(label,pt[0],pt[1])
1412 text = 0 # axis values not drawn on top side
1413
1414 if self._ySpec is not 'none':
1415 lower, upper = p1[1],p2[1]
1416 text = 1
1417 h = dc.GetCharHeight()
1418 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
b31cbeb9
RD
1419 a1 = scale*_Numeric.array([x, lower])+shift
1420 a2 = scale*_Numeric.array([x, upper])+shift
9d6685e2
RD
1421 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1422 for y, label in yticks:
b31cbeb9 1423 pt = scale*_Numeric.array([x, y])+shift
9d6685e2
RD
1424 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1425 if text:
1426 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1427 pt[1]-0.5*h)
1428 text = 0 # axis values not drawn on right side
1429
ecf0b9f9
RD
1430
1431class DrawCmd:
1432 """Represent a "draw" command, i.e. the one given in call to
1433 PlotCanvas.Draw(). The axis specs are not saved to preserve
1434 backward compatibility: they could be specified before the
1435 first Draw() command."""
1436 def __init__(self, graphics, xAxis, yAxis, xSpec, ySpec, xTicks, yTicks):
1437 """Same args as PlotCanvas.Draw(), plus axis specs."""
1438 self.graphics = graphics
1439 self.xTickGen = xTicks
1440 self.yTickGen = yTicks
1441 self.xAxisInit, self.yAxisInit = xAxis, yAxis
1442
1443 self.xAxis = None
1444 self.yAxis = None
1445 self.changeAxisX(xAxis, xSpec)
1446 self.changeAxisY(yAxis, ySpec)
1447 assert self.xAxis is not None
1448 assert self.yAxis is not None
1449
1450 def isEmpty(self):
1451 """Return true if either axis has 0 size."""
1452 if self.xAxis[0] == self.xAxis[1]:
1453 return True
1454 if self.yAxis[0] == self.yAxis[1]:
1455 return True
1456
1457 return False
1458
1459 def resetAxes(self, xSpec, ySpec):
1460 """Reset the axes to what they were initially, using axes specs given."""
1461 self.changeAxisX(self.xAxisInit, xSpec)
1462 self.changeAxisY(self.yAxisInit, ySpec)
1463
1464 def getBoundingBox(self):
1465 """Returns p1, p2 (two pairs)"""
1466 p1 = _Numeric.array((self.xAxis[0], self.yAxis[0]))
1467 p2 = _Numeric.array((self.xAxis[1], self.yAxis[1]))
1468 return p1, p2
1469
1470 def getClosestPoints(self, pntXY, pointScaled=True):
1471 ll = []
1472 for curveNum,obj in enumerate(self.graphics):
1473 #check there are points in the curve
1474 if len(obj.points) == 0:
1475 continue #go to next obj
1476 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1477 cn = [curveNum, obj.getLegend()]+\
1478 obj.getClosestPoint( pntXY, pointScaled)
1479 ll.append(cn)
1480 return ll
1481
1482 def scrollAxisX(self, units, xSpec):
1483 """Scroll y axis by units, using ySpec for axis spec."""
1484 self.changeAxisX((self.xAxis[0]+units, self.xAxis[1]+units), xSpec)
1485
1486 def scrollAxisY(self, units, ySpec):
1487 """Scroll y axis by units, using ySpec for axis spec."""
1488 self.changeAxisY((self.yAxis[0]+units, self.yAxis[1]+units), ySpec)
1489
1490 def changeAxisX(self, xAxis=None, xSpec=None):
1491 """Change the x axis to new given, or if None use ySpec to get it. """
1492 assert type(xAxis) in [type(None),tuple]
1493 if xAxis is None:
1494 p1, p2 = self.graphics.boundingBox() #min, max points of graphics
1495 self.xAxis = self._axisInterval(xSpec, p1[0], p2[0]) #in user units
1496 else:
1497 self.xAxis = xAxis
1498
1499 def changeAxisY(self, yAxis=None, ySpec=None):
1500 """Change the y axis to new given, or if None use ySpec to get it. """
1501 assert type(yAxis) in [type(None),tuple]
1502 if yAxis is None:
1503 p1, p2 = self.graphics.boundingBox() #min, max points of graphics
1504 self.yAxis = self._axisInterval(ySpec, p1[1], p2[1])
1505 else:
1506 self.yAxis = yAxis
1507
1508 def zoom(self, center, ratio):
1509 """Zoom to center and ratio."""
1510 x,y = Center
1511 w = (self.xAxis[1] - self.xAxis[0]) * Ratio[0]
1512 h = (self.yAxis[1] - self.yAxis[0]) * Ratio[1]
1513 self.xAxis = ( x - w/2, x + w/2 )
1514 self.yAxis = ( y - h/2, y + h/2 )
1515
1516 def getXMaxRange(self):
1517 p1, p2 = self.graphics.boundingBox() #min, max points of graphics
1518 return self._axisInterval(self._xSpec, p1[0], p2[0]) #in user units
1519
1520 def getYMaxRange(self):
1521 p1, p2 = self.graphics.boundingBox() #min, max points of graphics
1522 return self._axisInterval(self._ySpec, p1[1], p2[1]) #in user units
1523
1524 def getTicksX(self):
1525 """Get the ticks along y axis. Actually pairs of (t, str)."""
1526 xticks = self._ticks(self.xAxis[0], self.xAxis[1], self.xTickGen)
1527 if xticks == []: # try without the generator
1528 xticks = self._ticks(self.xAxis[0], self.xAxis[1])
1529 return xticks
1530
1531 def getTicksY(self):
1532 """Get the ticks along y axis. Actually pairs of (t, str)."""
1533 yticks = self._ticks(self.yAxis[0], self.yAxis[1], self.yTickGen)
1534 if yticks == []: # try without the generator
1535 yticks = self._ticks(self.yAxis[0], self.yAxis[1])
1536 return yticks
1537
1538 def _axisInterval(self, spec, lower, upper):
1539 """Returns sensible axis range for given spec."""
1540 if spec == 'none' or spec == 'min':
1541 if lower == upper:
1542 return lower-0.5, upper+0.5
1543 else:
1544 return lower, upper
1545 elif spec == 'auto':
1546 range = upper-lower
1547 if range == 0.:
1548 return lower-0.5, upper+0.5
1549 log = _Numeric.log10(range)
1550 power = _Numeric.floor(log)
1551 fraction = log-power
1552 if fraction <= 0.05:
1553 power = power-1
1554 grid = 10.**power
1555 lower = lower - lower % grid
1556 mod = upper % grid
1557 if mod != 0:
1558 upper = upper - mod + grid
1559 return lower, upper
1560 elif type(spec) == type(()):
1561 lower, upper = spec
1562 if lower <= upper:
1563 return lower, upper
1564 else:
1565 return upper, lower
1566 else:
1567 raise ValueError, str(spec) + ': illegal axis specification'
1568
1569 def _ticks(self, lower, upper, generator=None):
1570 """Get ticks between lower and upper, using generator if given. """
9d6685e2 1571 ideal = (upper-lower)/7.
b31cbeb9
RD
1572 log = _Numeric.log10(ideal)
1573 power = _Numeric.floor(log)
9d6685e2
RD
1574 fraction = log-power
1575 factor = 1.
1576 error = fraction
1577 for f, lf in self._multiples:
b31cbeb9 1578 e = _Numeric.fabs(fraction-lf)
9d6685e2
RD
1579 if e < error:
1580 error = e
1581 factor = f
1582 grid = factor * 10.**power
1583 if power > 4 or power < -4:
1584 format = '%+7.1e'
1585 elif power >= 0:
1586 digits = max(1, int(power))
1587 format = '%' + `digits`+'.0f'
1588 else:
1589 digits = -int(power)
1590 format = '%'+`digits+2`+'.'+`digits`+'f'
ecf0b9f9
RD
1591
1592 if generator is None:
1593 return tickGeneratorDefault(lower, upper, grid, format)
1594 elif isinstance(generator, list):
1595 return tickGeneratorFromList(generator, lower, upper, format)
1596 else:
1597 return generator(lower, upper, grid, format)
9d6685e2 1598
b31cbeb9 1599 _multiples = [(2., _Numeric.log10(2.)), (5., _Numeric.log10(5.))]
9d6685e2
RD
1600
1601
ecf0b9f9
RD
1602def tickGeneratorDefault(lower, upper, grid, format):
1603 """Default tick generator used by PlotCanvas.Draw() if not specified."""
1604 ticks = []
1605 t = -grid*_Numeric.floor(-lower/grid)
1606 while t <= upper:
1607 ticks.append( (t, format % (t,)) )
1608 t = t + grid
1609 return ticks
1610
1611def tickGeneratorFromList(ticks, lower, upper, format):
1612 """Tick generator used by PlotCanvas.Draw() if
1613 a list is given for xTicks or yTicks. """
1614 tickPairs = []
1615 for tick in ticks:
1616 if lower <= tick <= upper:
1617 tickPairs.append((tick, format % tick))
1618 return tickPairs
1619
9d6685e2
RD
1620#-------------------------------------------------------------------------------
1621# Used to layout the printer page
1622
1623class PlotPrintout(wx.Printout):
1624 """Controls how the plot is made in printing and previewing"""
1625 # Do not change method names in this class,
1626 # we have to override wx.Printout methods here!
1627 def __init__(self, graph):
1628 """graph is instance of plotCanvas to be printed or previewed"""
1629 wx.Printout.__init__(self)
1630 self.graph = graph
1631
1632 def HasPage(self, page):
1633 if page == 1:
1634 return True
1635 else:
1636 return False
1637
1638 def GetPageInfo(self):
e5ce86d8 1639 return (1, 1, 1, 1) # disable page numbers
9d6685e2
RD
1640
1641 def OnPrintPage(self, page):
00b9c867 1642 dc = self.GetDC() # allows using floats for certain functions
9d6685e2
RD
1643## print "PPI Printer",self.GetPPIPrinter()
1644## print "PPI Screen", self.GetPPIScreen()
1645## print "DC GetSize", dc.GetSize()
1646## print "GetPageSizePixels", self.GetPageSizePixels()
1647 # Note PPIScreen does not give the correct number
1648 # Calulate everything for printer and then scale for preview
1649 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1650 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1651 dcSize= dc.GetSize() # DC size
1652 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1653 clientDcSize= self.graph.GetClientSize()
1654
1655 # find what the margins are (mm)
1656 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1657 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1658
1659 # calculate offset and scale for dc
1660 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1661 pixRight= margRightSize*PPIPrinter[0]/25.4
1662 pixTop= margTopSize*PPIPrinter[1]/25.4
1663 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1664
1665 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1666 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1667
1668 # ratio offset and scale to screen size if preview
1669 if self.IsPreview():
1670 ratioW= float(dcSize[0])/pageSize[0]
1671 ratioH= float(dcSize[1])/pageSize[1]
1672 pixLeft *= ratioW
1673 pixTop *= ratioH
1674 plotAreaW *= ratioW
1675 plotAreaH *= ratioH
1676
1677 # rescale plot to page or preview plot area
1678 self.graph._setSize(plotAreaW,plotAreaH)
1679
1680 # Set offset and scale
1681 dc.SetDeviceOrigin(pixLeft,pixTop)
1682
1683 # Thicken up pens and increase marker size for printing
1684 ratioW= float(plotAreaW)/clientDcSize[0]
1685 ratioH= float(plotAreaH)/clientDcSize[1]
1686 aveScale= (ratioW+ratioH)/2
1687 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1688
1689 self.graph._printDraw(dc)
1690 # rescale back to original
1691 self.graph._setSize()
1692 self.graph._setPrinterScale(1)
b31cbeb9 1693 self.graph.Redraw() #to get point label scale and shift correct
9d6685e2
RD
1694
1695 return True
1696
9d6685e2
RD
1697
1698
1699
1700#---------------------------------------------------------------------------
1701# if running standalone...
1702#
1703# ...a sample implementation using the above
1704#
1705
1706def _draw1Objects():
1707 # 100 points sin function, plotted as green circles
b31cbeb9 1708 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
9d6685e2 1709 data1.shape = (100, 2)
b31cbeb9 1710 data1[:,1] = _Numeric.sin(data1[:,0])
9d6685e2
RD
1711 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1712
1713 # 50 points cos function, plotted as red line
b31cbeb9 1714 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
9d6685e2 1715 data1.shape = (50,2)
b31cbeb9 1716 data1[:,1] = _Numeric.cos(data1[:,0])
9d6685e2
RD
1717 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1718
1719 # A few more points...
b31cbeb9 1720 pi = _Numeric.pi
9d6685e2
RD
1721 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1722 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1723 marker='cross')
1724
1725 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1726
1727def _draw2Objects():
1728 # 100 points sin function, plotted as green dots
b31cbeb9 1729 data1 = 2.*_Numeric.pi*_Numeric.arange(200)/200.
9d6685e2 1730 data1.shape = (100, 2)
b31cbeb9 1731 data1[:,1] = _Numeric.sin(data1[:,0])
9d6685e2
RD
1732 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1733
1734 # 50 points cos function, plotted as red dot-dash
b31cbeb9 1735 data1 = 2.*_Numeric.pi*_Numeric.arange(100)/100.
9d6685e2 1736 data1.shape = (50,2)
b31cbeb9 1737 data1[:,1] = _Numeric.cos(data1[:,0])
9d6685e2
RD
1738 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1739
1740 # A few more points...
b31cbeb9 1741 pi = _Numeric.pi
9d6685e2
RD
1742 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1743 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1744 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1745 marker='square')
1746
1747 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1748
1749def _draw3Objects():
1750 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1751 'cross', 'plus', 'circle']
1752 m=[]
1753 for i in range(len(markerList)):
1754 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1755 marker=markerList[i]))
1756 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1757
1758def _draw4Objects():
1759 # 25,000 point line
b31cbeb9 1760 data1 = _Numeric.arange(5e5,1e6,10)
9d6685e2
RD
1761 data1.shape = (25000, 2)
1762 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1763
1764 # A few more points...
1765 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1766 marker='square')
1767 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1768
1769def _draw5Objects():
1770 # Empty graph with axis defined but no points/lines
1771 points=[]
1772 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1773 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1774
00b9c867
RD
1775def _draw6Objects():
1776 # Bar graph
1777 points1=[(1,0), (1,10)]
1778 line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
1779 points1g=[(2,0), (2,4)]
1780 line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
1781 points1b=[(3,0), (3,6)]
1782 line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
1783
1784 points2=[(4,0), (4,12)]
1785 line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
1786 points2g=[(5,0), (5,8)]
1787 line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
1788 points2b=[(6,0), (6,4)]
1789 line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
1790
1791 return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
1792 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1793
ecf0b9f9
RD
1794def _draw7Objects():
1795 # four bars of various styles colors and sizes
1796 bar1 = PolyBar((0,1), colour='green', size=4, labels='Green bar')
1797 bar2 = PolyBar((2,1), colour='red', size=7, labels='Red bar',
1798 fillstyle=wx.CROSSDIAG_HATCH)
1799 bar3 = PolyBar([(1,3),(3,4)], colour='blue',
1800 labels=['Blue bar 1', 'Blue bar 2'],
1801 fillstyle=wx.TRANSPARENT)
1802 bars = [bar1, bar2, bar3]
1803 return PlotGraphics(bars,
1804 "Graph Title", "Bar names", "Bar height"), getBarTicksGen(*bars)
1805
1806def _draw8Objects():
1807 # four bars in two groups, overlayed to a line plot
1808 x1,x2 = 0.0,0.5
1809 bar1 = PolyBar([(x1,1.),(x1+2,2.)], colour='green', size=4,
1810 legend="1998", fillstyle=wx.CROSSDIAG_HATCH)
1811 bar2 = PolyBar(bar1.offset([1.2,2.5]), colour='red', size=7,
1812 legend="2000", labels=['cars','trucks'])
1813 line1 = PolyLine([(x1,1.5), (x2,1.2), (x1+2,1), (x2+2,2)], colour='blue')
1814 return PlotGraphics([bar1, bar2, line1],
1815 "Graph Title", "Bar names", "Bar height"), getBarTicksGen(bar1, bar2)
9d6685e2
RD
1816
1817class TestFrame(wx.Frame):
1818 def __init__(self, parent, id, title):
1819 wx.Frame.__init__(self, parent, id, title,
1820 wx.DefaultPosition, (600, 400))
1821
1822 # Now Create the menu bar and items
1823 self.mainmenu = wx.MenuBar()
1824
1825 menu = wx.Menu()
1826 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1827 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1828
1829 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1830 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1831
1832 menu.Append(202, 'Print...', 'Print the current plot')
1833 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1834
1835 menu.Append(203, 'Save Plot...', 'Save current plot')
1836 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1837
1838 menu.Append(205, 'E&xit', 'Enough of this already!')
1839 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1840 self.mainmenu.Append(menu, '&File')
1841
1842 menu = wx.Menu()
1843 menu.Append(206, 'Draw1', 'Draw plots1')
1844 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1845 menu.Append(207, 'Draw2', 'Draw plots2')
1846 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1847 menu.Append(208, 'Draw3', 'Draw plots3')
1848 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1849 menu.Append(209, 'Draw4', 'Draw plots4')
1850 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1851 menu.Append(210, 'Draw5', 'Draw plots5')
1852 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
00b9c867
RD
1853 menu.Append(260, 'Draw6', 'Draw plots6')
1854 self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
ecf0b9f9
RD
1855 menu.Append(261, 'Draw7', 'Draw plots7')
1856 self.Bind(wx.EVT_MENU,self.OnPlotDraw7, id=261)
1857 menu.Append(262, 'Draw8', 'Draw plots8')
1858 self.Bind(wx.EVT_MENU,self.OnPlotDraw8, id=262)
00b9c867 1859
9d6685e2
RD
1860
1861 menu.Append(211, '&Redraw', 'Redraw plots')
1862 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1863 menu.Append(212, '&Clear', 'Clear canvas')
1864 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1865 menu.Append(213, '&Scale', 'Scale canvas')
1866 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1867 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1868 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1869 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1870 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1871 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
b31cbeb9
RD
1872 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1873 menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
1874 self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
1875
9d6685e2
RD
1876 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1877 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1878 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1879 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1880 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1881 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1882
1883 self.mainmenu.Append(menu, '&Plot')
1884
1885 menu = wx.Menu()
1886 menu.Append(300, '&About', 'About this thing...')
1887 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1888 self.mainmenu.Append(menu, '&Help')
1889
1890 self.SetMenuBar(self.mainmenu)
1891
1892 # A status bar to tell people what's happening
1893 self.CreateStatusBar(1)
1894
1895 self.client = PlotCanvas(self)
b31cbeb9
RD
1896 #define the function for drawing pointLabels
1897 self.client.SetPointLabelFunc(self.DrawPointLabel)
9d6685e2
RD
1898 # Create mouse event for showing cursor coords in status bar
1899 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
b31cbeb9
RD
1900 # Show closest point when enabled
1901 self.client.Bind(wx.EVT_MOTION, self.OnMotion)
1902
9d6685e2
RD
1903 self.Show(True)
1904
b31cbeb9
RD
1905 def DrawPointLabel(self, dc, mDataDict):
1906 """This is the fuction that defines how the pointLabels are plotted
1907 dc - DC that will be passed
1908 mDataDict - Dictionary of data that you want to use for the pointLabel
1909
1910 As an example I have decided I want a box at the curve point
1911 with some text information about the curve plotted below.
1912 Any wxDC method can be used.
1913 """
1914 # ----------
1915 dc.SetPen(wx.Pen(wx.BLACK))
1916 dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
1917
1918 sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
1919 dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
1920 px,py = mDataDict["pointXY"]
1921 cNum = mDataDict["curveNum"]
1922 pntIn = mDataDict["pIndex"]
1923 legend = mDataDict["legend"]
1924 #make a string to display
1925 s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
1926 dc.DrawText(s, sx , sy+1)
1927 # -----------
1928
9d6685e2
RD
1929 def OnMouseLeftDown(self,event):
1930 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1931 self.SetStatusText(s)
b31cbeb9
RD
1932 event.Skip() #allows plotCanvas OnMouseLeftDown to be called
1933
1934 def OnMotion(self, event):
1935 #show closest point (when enbled)
1936 if self.client.GetEnablePointLabel() == True:
1937 #make up dict with info for the pointLabel
1938 #I've decided to mark the closest point on the closest curve
1939 dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
1940 if dlst != []: #returns [] if none
1941 curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
1942 #make up dictionary to pass to my user function (see DrawPointLabel)
1943 mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
1944 "pointXY":pointXY, "scaledXY":scaledXY}
1945 #pass dict to update the pointLabel
1946 self.client.UpdatePointLabel(mDataDict)
1947 event.Skip() #go to next handler
9d6685e2
RD
1948
1949 def OnFilePageSetup(self, event):
1950 self.client.PageSetup()
1951
1952 def OnFilePrintPreview(self, event):
1953 self.client.PrintPreview()
1954
1955 def OnFilePrint(self, event):
1956 self.client.Printout()
1957
1958 def OnSaveFile(self, event):
1959 self.client.SaveFile()
1960
1961 def OnFileExit(self, event):
1962 self.Close()
1963
1964 def OnPlotDraw1(self, event):
1965 self.resetDefaults()
1966 self.client.Draw(_draw1Objects())
1967
1968 def OnPlotDraw2(self, event):
1969 self.resetDefaults()
1970 self.client.Draw(_draw2Objects())
1971
1972 def OnPlotDraw3(self, event):
1973 self.resetDefaults()
1974 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1975 self.client.SetFontSizeAxis(20)
1976 self.client.SetFontSizeLegend(12)
1977 self.client.SetXSpec('min')
1978 self.client.SetYSpec('none')
1979 self.client.Draw(_draw3Objects())
1980
1981 def OnPlotDraw4(self, event):
1982 self.resetDefaults()
1983 drawObj= _draw4Objects()
1984 self.client.Draw(drawObj)
b31cbeb9
RD
1985## # profile
1986## start = _time.clock()
1987## for x in range(10):
1988## self.client.Draw(drawObj)
1989## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1990## # profile end
9d6685e2
RD
1991
1992 def OnPlotDraw5(self, event):
1993 # Empty plot with just axes
1994 self.resetDefaults()
1995 drawObj= _draw5Objects()
1996 # make the axis X= (0,5), Y=(0,10)
1997 # (default with None is X= (-1,1), Y= (-1,1))
1998 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1999
00b9c867
RD
2000 def OnPlotDraw6(self, event):
2001 #Bar Graph Example
2002 self.resetDefaults()
2003 #self.client.SetEnableLegend(True) #turn on Legend
2004 #self.client.SetEnableGrid(True) #turn on Grid
2005 self.client.SetXSpec('none') #turns off x-axis scale
2006 self.client.SetYSpec('auto')
2007 self.client.Draw(_draw6Objects(), xAxis= (0,7))
2008
ecf0b9f9
RD
2009 def OnPlotDraw7(self, event):
2010 #Bar Graph and custom ticks Example, using PolyBar
2011 self.resetDefaults()
2012 bars, tickGenerator = _draw7Objects()
2013 #note that yTicks is not necessary here, used only
2014 #to show off custom ticks
2015 self.client.Draw(bars, xAxis=(0,4),yAxis = (0,5),
2016 xTicks=tickGenerator, yTicks=[0,1,3])
2017
2018 def OnPlotDraw8(self, event):
2019 #Bar Graph and custom ticks Example, using PolyBar
2020 self.resetDefaults()
2021 plot, tickGenerator = _draw8Objects()
2022 self.client.Draw(plot, xAxis=(0,4),yAxis = (0,5), xTicks=tickGenerator)
2023
2024
9d6685e2
RD
2025 def OnPlotRedraw(self,event):
2026 self.client.Redraw()
2027
2028 def OnPlotClear(self,event):
2029 self.client.Clear()
2030
2031 def OnPlotScale(self, event):
ecf0b9f9
RD
2032 if self.client.BeenDrawn():
2033 self.client.ChangeAxes((1,3.05),(0,1))
2034
9d6685e2
RD
2035
2036 def OnEnableZoom(self, event):
2037 self.client.SetEnableZoom(event.IsChecked())
2038
2039 def OnEnableGrid(self, event):
2040 self.client.SetEnableGrid(event.IsChecked())
2041
2042 def OnEnableLegend(self, event):
2043 self.client.SetEnableLegend(event.IsChecked())
2044
b31cbeb9
RD
2045 def OnEnablePointLabel(self, event):
2046 self.client.SetEnablePointLabel(event.IsChecked())
2047
9d6685e2
RD
2048 def OnScrUp(self, event):
2049 self.client.ScrollUp(1)
2050
2051 def OnScrRt(self,event):
2052 self.client.ScrollRight(2)
2053
2054 def OnReset(self,event):
2055 self.client.Reset()
2056
2057 def OnHelpAbout(self, event):
33785d9f
RD
2058 from wx.lib.dialogs import ScrolledMessageDialog
2059 about = ScrolledMessageDialog(self, __doc__, "About...")
9d6685e2
RD
2060 about.ShowModal()
2061
2062 def resetDefaults(self):
2063 """Just to reset the fonts back to the PlotCanvas defaults"""
2064 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
2065 self.client.SetFontSizeAxis(10)
2066 self.client.SetFontSizeLegend(7)
2067 self.client.SetXSpec('auto')
2068 self.client.SetYSpec('auto')
2069
2070
2071def __test():
2072
2073 class MyApp(wx.App):
2074 def OnInit(self):
2075 wx.InitAllImageHandlers()
2076 frame = TestFrame(None, -1, "PlotCanvas")
2077 #frame.Show(True)
2078 self.SetTopWindow(frame)
2079 return True
2080
2081
2082 app = MyApp(0)
2083 app.MainLoop()
2084
2085if __name__ == '__main__':
2086 __test()