]>
Commit | Line | Data |
---|---|---|
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 | """ | |
48 | This is a simple light weight plotting module that can be used with | |
49 | Boa or easily integrated into your own wxPython application. The | |
50 | emphasis is on small size and fast plotting for large data sets. It | |
51 | has a reasonable number of features to do line and scatter graphs | |
00b9c867 RD |
52 | easily as well as simple bar graphs. It is not as sophisticated or |
53 | as powerful as SciPy Plt or Chaco. Both of these are great packages | |
54 | but consume huge amounts of computer resources for simple plots. | |
55 | They can be found at http://scipy.com | |
9d6685e2 RD |
56 | |
57 | This file contains two parts; first the re-usable library stuff, then, | |
58 | after a "if __name__=='__main__'" test, a simple frame and a few default | |
59 | plots for examples and testing. | |
60 | ||
61 | Based on wxPlotCanvas | |
62 | Written by K.Hinsen, R. Srinivasan; | |
63 | Ported to wxPython Harm van der Heijden, feb 1999 | |
64 | ||
65 | Major 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 | ||
77 | Did a lot of work here to speed markers up. Only a factor of 4 | |
78 | improvement though. Lines are much faster than markers, especially | |
79 | filled markers. Stay away from circles and triangles unless you | |
80 | only have a few thousand points. | |
81 | ||
82 | Times for 25,000 points | |
83 | Line - 0.078 sec | |
84 | Markers | |
85 | Square - 0.22 sec | |
86 | dot - 0.10 | |
87 | circle - 0.87 | |
88 | cross,plus - 0.28 | |
89 | triangle, triangle_down - 0.90 | |
90 | ||
91 | Thanks to Chris Barker for getting this version working on Linux. | |
92 | ||
93 | Zooming 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 |
99 | import string as _string |
100 | import time as _time | |
9d6685e2 RD |
101 | import wx |
102 | ||
00b9c867 | 103 | # Needs Numeric or numarray |
9d6685e2 | 104 | try: |
b31cbeb9 | 105 | import Numeric as _Numeric |
9d6685e2 RD |
106 | except: |
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 | # | |
123 | class 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 |
200 | class 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 | ||
246 | class 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 |
354 | class 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 | ||
477 | def 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 |
496 | class 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 | ||
587 | class 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 | |
1431 | class 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 |
1602 | def 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 | ||
1611 | def 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 | ||
1623 | class 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 | ||
1706 | def _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 | ||
1727 | def _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 | ||
1749 | def _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 | ||
1758 | def _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 | ||
1769 | def _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 |
1775 | def _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 |
1794 | def _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 | ||
1806 | def _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 | |
1817 | class 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 | ||
2071 | def __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 | ||
2085 | if __name__ == '__main__': | |
2086 | __test() |