]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/plot.py
Give BitmapButton its own demo, get rid of ErrorDialogs
[wxWidgets.git] / wxPython / wx / lib / plot.py
1 #-----------------------------------------------------------------------------
2 # Name: wx.lib.plot.py
3 # Purpose:
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 #
19 # 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
20 #
21 # o wxScrolledMessageDialog -> ScrolledMessageDialog
22 #
23
24 """
25 This is a simple light weight plotting module that can be used with
26 Boa or easily integrated into your own wxPython application. The
27 emphasis is on small size and fast plotting for large data sets. It
28 has a reasonable number of features to do line and scatter graphs
29 easily. It is not as sophisticated or as powerful as SciPy Plt or
30 Chaco. Both of these are great packages but consume huge amounts of
31 computer resources for simple plots. They can be found at
32 http://scipy.com
33
34 This file contains two parts; first the re-usable library stuff, then,
35 after a "if __name__=='__main__'" test, a simple frame and a few default
36 plots for examples and testing.
37
38 Based on wxPlotCanvas
39 Written by K.Hinsen, R. Srinivasan;
40 Ported to wxPython Harm van der Heijden, feb 1999
41
42 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
43 -More style options
44 -Zooming using mouse "rubber band"
45 -Scroll left, right
46 -Grid(graticule)
47 -Printing, preview, and page set up (margins)
48 -Axis and title labels
49 -Cursor xy axis values
50 -Doc strings and lots of comments
51 -Optimizations for large number of points
52 -Legends
53
54 Did a lot of work here to speed markers up. Only a factor of 4
55 improvement though. Lines are much faster than markers, especially
56 filled markers. Stay away from circles and triangles unless you
57 only have a few thousand points.
58
59 Times for 25,000 points
60 Line - 0.078 sec
61 Markers
62 Square - 0.22 sec
63 dot - 0.10
64 circle - 0.87
65 cross,plus - 0.28
66 triangle, triangle_down - 0.90
67
68 Thanks to Chris Barker for getting this version working on Linux.
69
70 Zooming controls with mouse (when enabled):
71 Left mouse drag - Zoom box.
72 Left mouse double click - reset zoom.
73 Right mouse click - zoom out centred on click location.
74 """
75
76 import string
77 import time
78 import wx
79
80 # Needs Numeric
81 try:
82 import Numeric
83 except:
84 try:
85 import numarray as Numeric #if numarray is used it is renamed Numeric
86 except:
87 msg= """
88 This module requires the Numeric or numarray module,
89 which could not be imported. It probably is not installed
90 (it's not part of the standard Python distribution). See the
91 Python site (http://www.python.org) for information on
92 downloading source or binaries."""
93 raise ImportError, "Numeric or numarray not found. \n" + msg
94
95
96
97 #
98 # Plotting classes...
99 #
100 class PolyPoints:
101 """Base Class for lines and markers
102 - All methods are private.
103 """
104
105 def __init__(self, points, attr):
106 self.points = Numeric.array(points)
107 self.currentScale= (1,1)
108 self.currentShift= (0,0)
109 self.scaled = self.points
110 self.attributes = {}
111 self.attributes.update(self._attributes)
112 for name, value in attr.items():
113 if name not in self._attributes.keys():
114 raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
115 self.attributes[name] = value
116
117 def boundingBox(self):
118 if len(self.points) == 0:
119 # no curves to draw
120 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
121 minXY= Numeric.array([-1,-1])
122 maxXY= Numeric.array([ 1, 1])
123 else:
124 minXY= Numeric.minimum.reduce(self.points)
125 maxXY= Numeric.maximum.reduce(self.points)
126 return minXY, maxXY
127
128 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
129 if len(self.points) == 0:
130 # no curves to draw
131 return
132 if (scale is not self.currentScale) or (shift is not self.currentShift):
133 # update point scaling
134 self.scaled = scale*self.points+shift
135 self.currentScale= scale
136 self.currentShift= shift
137 # else unchanged use the current scaling
138
139 def getLegend(self):
140 return self.attributes['legend']
141
142
143 class PolyLine(PolyPoints):
144 """Class to define line type and style
145 - All methods except __init__ are private.
146 """
147
148 _attributes = {'colour': 'black',
149 'width': 1,
150 'style': wx.SOLID,
151 'legend': ''}
152
153 def __init__(self, points, **attr):
154 """Creates PolyLine object
155 points - sequence (array, tuple or list) of (x,y) points making up line
156 **attr - key word attributes
157 Defaults:
158 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
159 'width'= 1, - Pen width
160 'style'= wx.SOLID, - wx.Pen style
161 'legend'= '' - Line Legend to display
162 """
163 PolyPoints.__init__(self, points, attr)
164
165 def draw(self, dc, printerScale, coord= None):
166 colour = self.attributes['colour']
167 width = self.attributes['width'] * printerScale
168 style= self.attributes['style']
169 dc.SetPen(wx.Pen(wx.NamedColour(colour), int(width), style))
170 if coord == None:
171 dc.DrawLines(self.scaled)
172 else:
173 dc.DrawLines(coord) # draw legend line
174
175 def getSymExtent(self, printerScale):
176 """Width and Height of Marker"""
177 h= self.attributes['width'] * printerScale
178 w= 5 * h
179 return (w,h)
180
181
182 class PolyMarker(PolyPoints):
183 """Class to define marker type and style
184 - All methods except __init__ are private.
185 """
186
187 _attributes = {'colour': 'black',
188 'width': 1,
189 'size': 2,
190 'fillcolour': None,
191 'fillstyle': wx.SOLID,
192 'marker': 'circle',
193 'legend': ''}
194
195 def __init__(self, points, **attr):
196 """Creates PolyMarker object
197 points - sequence (array, tuple or list) of (x,y) points
198 **attr - key word attributes
199 Defaults:
200 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
201 'width'= 1, - Pen width
202 'size'= 2, - Marker size
203 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
204 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
205 'marker'= 'circle' - Marker shape
206 'legend'= '' - Marker Legend to display
207
208 Marker Shapes:
209 - 'circle'
210 - 'dot'
211 - 'square'
212 - 'triangle'
213 - 'triangle_down'
214 - 'cross'
215 - 'plus'
216 """
217
218 PolyPoints.__init__(self, points, attr)
219
220 def draw(self, dc, printerScale, coord= None):
221 colour = self.attributes['colour']
222 width = self.attributes['width'] * printerScale
223 size = self.attributes['size'] * printerScale
224 fillcolour = self.attributes['fillcolour']
225 fillstyle = self.attributes['fillstyle']
226 marker = self.attributes['marker']
227
228 dc.SetPen(wx.Pen(wx.NamedColour(colour),int(width)))
229 if fillcolour:
230 dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
231 else:
232 dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
233 if coord == None:
234 self._drawmarkers(dc, self.scaled, marker, size)
235 else:
236 self._drawmarkers(dc, coord, marker, size) # draw legend marker
237
238 def getSymExtent(self, printerScale):
239 """Width and Height of Marker"""
240 s= 5*self.attributes['size'] * printerScale
241 return (s,s)
242
243 def _drawmarkers(self, dc, coords, marker,size=1):
244 f = eval('self._' +marker)
245 f(dc, coords, size)
246
247 def _circle(self, dc, coords, size=1):
248 fact= 2.5*size
249 wh= 5.0*size
250 rect= Numeric.zeros((len(coords),4),Numeric.Float)+[0.0,0.0,wh,wh]
251 rect[:,0:2]= coords-[fact,fact]
252 dc.DrawEllipseList(rect.astype(Numeric.Int32))
253
254 def _dot(self, dc, coords, size=1):
255 dc.DrawPointList(coords)
256
257 def _square(self, dc, coords, size=1):
258 fact= 2.5*size
259 wh= 5.0*size
260 rect= Numeric.zeros((len(coords),4),Numeric.Float)+[0.0,0.0,wh,wh]
261 rect[:,0:2]= coords-[fact,fact]
262 dc.DrawRectangleList(rect.astype(Numeric.Int32))
263
264 def _triangle(self, dc, coords, size=1):
265 shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
266 poly= Numeric.repeat(coords,3)
267 poly.shape= (len(coords),3,2)
268 poly += shape
269 dc.DrawPolygonList(poly.astype(Numeric.Int32))
270
271 def _triangle_down(self, dc, coords, size=1):
272 shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
273 poly= Numeric.repeat(coords,3)
274 poly.shape= (len(coords),3,2)
275 poly += shape
276 dc.DrawPolygonList(poly.astype(Numeric.Int32))
277
278 def _cross(self, dc, coords, size=1):
279 fact= 2.5*size
280 for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
281 lines= Numeric.concatenate((coords,coords),axis=1)+f
282 dc.DrawLineList(lines.astype(Numeric.Int32))
283
284 def _plus(self, dc, coords, size=1):
285 fact= 2.5*size
286 for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
287 lines= Numeric.concatenate((coords,coords),axis=1)+f
288 dc.DrawLineList(lines.astype(Numeric.Int32))
289
290 class PlotGraphics:
291 """Container to hold PolyXXX objects and graph labels
292 - All methods except __init__ are private.
293 """
294
295 def __init__(self, objects, title='', xLabel='', yLabel= ''):
296 """Creates PlotGraphics object
297 objects - list of PolyXXX objects to make graph
298 title - title shown at top of graph
299 xLabel - label shown on x-axis
300 yLabel - label shown on y-axis
301 """
302 if type(objects) not in [list,tuple]:
303 raise TypeError, "objects argument should be list or tuple"
304 self.objects = objects
305 self.title= title
306 self.xLabel= xLabel
307 self.yLabel= yLabel
308
309 def boundingBox(self):
310 p1, p2 = self.objects[0].boundingBox()
311 for o in self.objects[1:]:
312 p1o, p2o = o.boundingBox()
313 p1 = Numeric.minimum(p1, p1o)
314 p2 = Numeric.maximum(p2, p2o)
315 return p1, p2
316
317 def scaleAndShift(self, scale=(1,1), shift=(0,0)):
318 for o in self.objects:
319 o.scaleAndShift(scale, shift)
320
321 def setPrinterScale(self, scale):
322 """Thickens up lines and markers only for printing"""
323 self.printerScale= scale
324
325 def setXLabel(self, xLabel= ''):
326 """Set the X axis label on the graph"""
327 self.xLabel= xLabel
328
329 def setYLabel(self, yLabel= ''):
330 """Set the Y axis label on the graph"""
331 self.yLabel= yLabel
332
333 def setTitle(self, title= ''):
334 """Set the title at the top of graph"""
335 self.title= title
336
337 def getXLabel(self):
338 """Get x axis label string"""
339 return self.xLabel
340
341 def getYLabel(self):
342 """Get y axis label string"""
343 return self.yLabel
344
345 def getTitle(self, title= ''):
346 """Get the title at the top of graph"""
347 return self.title
348
349 def draw(self, dc):
350 for o in self.objects:
351 #t=time.clock() # profile info
352 o.draw(dc, self.printerScale)
353 #dt= time.clock()-t
354 #print o, "time=", dt
355
356 def getSymExtent(self, printerScale):
357 """Get max width and height of lines and markers symbols for legend"""
358 symExt = self.objects[0].getSymExtent(printerScale)
359 for o in self.objects[1:]:
360 oSymExt = o.getSymExtent(printerScale)
361 symExt = Numeric.maximum(symExt, oSymExt)
362 return symExt
363
364 def getLegendNames(self):
365 """Returns list of legend names"""
366 lst = [None]*len(self)
367 for i in range(len(self)):
368 lst[i]= self.objects[i].getLegend()
369 return lst
370
371 def __len__(self):
372 return len(self.objects)
373
374 def __getitem__(self, item):
375 return self.objects[item]
376
377
378 #-------------------------------------------------------------------------------
379 # Main window that you will want to import into your application.
380
381 class PlotCanvas(wx.Window):
382 """Subclass of a wx.Window to allow simple general plotting
383 of data with zoom, labels, and automatic axis scaling."""
384
385 def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
386 size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
387 """Constucts a window, which can be a child of a frame, dialog or
388 any other non-control window"""
389
390 wx.Window.__init__(self, parent, id, pos, size, style, name)
391 self.border = (1,1)
392
393 self.SetBackgroundColour("white")
394
395 self.Bind(wx.EVT_PAINT, self.OnPaint)
396 self.Bind(wx.EVT_SIZE, self.OnSize)
397
398 # Create some mouse events for zooming
399 self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
400 self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
401 self.Bind(wx.EVT_MOTION, self.OnMotion)
402 self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
403 self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
404
405 # set curser as cross-hairs
406 self.SetCursor(wx.CROSS_CURSOR)
407
408 # Things for printing
409 self.print_data = wx.PrintData()
410 self.print_data.SetPaperId(wx.PAPER_LETTER)
411 self.print_data.SetOrientation(wx.LANDSCAPE)
412 self.pageSetupData= wx.PageSetupDialogData()
413 self.pageSetupData.SetMarginBottomRight((25,25))
414 self.pageSetupData.SetMarginTopLeft((25,25))
415 self.pageSetupData.SetPrintData(self.print_data)
416 self.printerScale = 1
417 self.parent= parent
418
419 # Zooming variables
420 self._zoomInFactor = 0.5
421 self._zoomOutFactor = 2
422 self._zoomCorner1= Numeric.array([0.0, 0.0]) # left mouse down corner
423 self._zoomCorner2= Numeric.array([0.0, 0.0]) # left mouse up corner
424 self._zoomEnabled= False
425 self._hasDragged= False
426
427 # Drawing Variables
428 self.last_draw = None
429 self._pointScale= 1
430 self._pointShift= 0
431 self._xSpec= 'auto'
432 self._ySpec= 'auto'
433 self._gridEnabled= False
434 self._legendEnabled= False
435
436 # Fonts
437 self._fontCache = {}
438 self._fontSizeAxis= 10
439 self._fontSizeTitle= 15
440 self._fontSizeLegend= 7
441
442 # OnSize called to make sure the buffer is initialized.
443 # This might result in OnSize getting called twice on some
444 # platforms at initialization, but little harm done.
445 self.OnSize(None) # sets the initial size based on client size
446
447
448 # SaveFile
449 def SaveFile(self, fileName= ''):
450 """Saves the file to the type specified in the extension. If no file
451 name is specified a dialog box is provided. Returns True if sucessful,
452 otherwise False.
453
454 .bmp Save a Windows bitmap file.
455 .xbm Save an X bitmap file.
456 .xpm Save an XPM bitmap file.
457 .png Save a Portable Network Graphics file.
458 .jpg Save a Joint Photographic Experts Group file.
459 """
460 if string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
461 dlg1 = wx.FileDialog(
462 self,
463 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
464 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
465 wx.SAVE|wx.OVERWRITE_PROMPT
466 )
467 try:
468 while 1:
469 if dlg1.ShowModal() == wx.ID_OK:
470 fileName = dlg1.GetPath()
471 # Check for proper exension
472 if string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
473 dlg2 = wx.MessageDialog(self, 'File name extension\n'
474 'must be one of\n'
475 'bmp, xbm, xpm, png, or jpg',
476 'File Name Error', wx.OK | wx.ICON_ERROR)
477 try:
478 dlg2.ShowModal()
479 finally:
480 dlg2.Destroy()
481 else:
482 break # now save file
483 else: # exit without saving
484 return False
485 finally:
486 dlg1.Destroy()
487
488 # File name has required extension
489 fType = string.lower(fileName[-3:])
490 if fType == "bmp":
491 tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
492 elif fType == "xbm":
493 tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
494 elif fType == "xpm":
495 tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
496 elif fType == "jpg":
497 tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
498 else:
499 tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
500 # Save Bitmap
501 res= self._Buffer.SaveFile(fileName, tp)
502 return res
503
504 def PageSetup(self):
505 """Brings up the page setup dialog"""
506 data = self.pageSetupData
507 data.SetPrintData(self.print_data)
508 dlg = wx.PageSetupDialog(self.parent, data)
509 try:
510 if dlg.ShowModal() == wx.ID_OK:
511 data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
512 # updates page parameters from dialog
513 self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
514 self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
515 self.pageSetupData.SetPrintData(data.GetPrintData())
516 self.print_data=data.GetPrintData() # updates print_data
517 finally:
518 dlg.Destroy()
519
520 def Printout(self, paper=None):
521 """Print current plot."""
522 if paper != None:
523 self.print_data.SetPaperId(paper)
524 pdd = wx.PrintDialogData()
525 pdd.SetPrintData(self.print_data)
526 printer = wx.Printer(pdd)
527 out = PlotPrintout(self)
528 print_ok = printer.Print(self.parent, out)
529 if print_ok:
530 self.print_data = printer.GetPrintDialogData().GetPrintData()
531 out.Destroy()
532
533 def PrintPreview(self):
534 """Print-preview current plot."""
535 printout = PlotPrintout(self)
536 printout2 = PlotPrintout(self)
537 self.preview = wx.PrintPreview(printout, printout2, self.print_data)
538 if not self.preview.Ok():
539 wx.MessageDialog(self, "Print Preview failed.\n" \
540 "Check that default printer is configured\n", \
541 "Print error", wx.OK|wx.CENTRE).ShowModal()
542 self.preview.SetZoom(30)
543 # search up tree to find frame instance
544 frameInst= self
545 while not isinstance(frameInst, wx.Frame):
546 frameInst= frameInst.GetParent()
547 frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
548 frame.Initialize()
549 frame.SetPosition(self.GetPosition())
550 frame.SetSize((500,400))
551 frame.Centre(wx.BOTH)
552 frame.Show(True)
553
554 def SetFontSizeAxis(self, point= 10):
555 """Set the tick and axis label font size (default is 10 point)"""
556 self._fontSizeAxis= point
557
558 def GetFontSizeAxis(self):
559 """Get current tick and axis label font size in points"""
560 return self._fontSizeAxis
561
562 def SetFontSizeTitle(self, point= 15):
563 """Set Title font size (default is 15 point)"""
564 self._fontSizeTitle= point
565
566 def GetFontSizeTitle(self):
567 """Get current Title font size in points"""
568 return self._fontSizeTitle
569
570 def SetFontSizeLegend(self, point= 7):
571 """Set Legend font size (default is 7 point)"""
572 self._fontSizeLegend= point
573
574 def GetFontSizeLegend(self):
575 """Get current Legend font size in points"""
576 return self._fontSizeLegend
577
578 def SetEnableZoom(self, value):
579 """Set True to enable zooming."""
580 if value not in [True,False]:
581 raise TypeError, "Value should be True or False"
582 self._zoomEnabled= value
583
584 def GetEnableZoom(self):
585 """True if zooming enabled."""
586 return self._zoomEnabled
587
588 def SetEnableGrid(self, value):
589 """Set True to enable grid."""
590 if value not in [True,False]:
591 raise TypeError, "Value should be True or False"
592 self._gridEnabled= value
593 self.Redraw()
594
595 def GetEnableGrid(self):
596 """True if grid enabled."""
597 return self._gridEnabled
598
599 def SetEnableLegend(self, value):
600 """Set True to enable legend."""
601 if value not in [True,False]:
602 raise TypeError, "Value should be True or False"
603 self._legendEnabled= value
604 self.Redraw()
605
606 def GetEnableLegend(self):
607 """True if Legend enabled."""
608 return self._legendEnabled
609
610 def Reset(self):
611 """Unzoom the plot."""
612 if self.last_draw is not None:
613 self.Draw(self.last_draw[0])
614
615 def ScrollRight(self, units):
616 """Move view right number of axis units."""
617 if self.last_draw is not None:
618 graphics, xAxis, yAxis= self.last_draw
619 xAxis= (xAxis[0]+units, xAxis[1]+units)
620 self.Draw(graphics,xAxis,yAxis)
621
622 def ScrollUp(self, units):
623 """Move view up number of axis units."""
624 if self.last_draw is not None:
625 graphics, xAxis, yAxis= self.last_draw
626 yAxis= (yAxis[0]+units, yAxis[1]+units)
627 self.Draw(graphics,xAxis,yAxis)
628
629 def GetXY(self,event):
630 """Takes a mouse event and returns the XY user axis values."""
631 screenPos= Numeric.array( event.GetPosition())
632 x,y= (screenPos-self._pointShift)/self._pointScale
633 return x,y
634
635 def SetXSpec(self, type= 'auto'):
636 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
637 where:
638 'none' - shows no axis or tick mark values
639 'min' - shows min bounding box values
640 'auto' - rounds axis range to sensible values
641 """
642 self._xSpec= type
643
644 def SetYSpec(self, type= 'auto'):
645 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
646 where:
647 'none' - shows no axis or tick mark values
648 'min' - shows min bounding box values
649 'auto' - rounds axis range to sensible values
650 """
651 self._ySpec= type
652
653 def GetXSpec(self):
654 """Returns current XSpec for axis"""
655 return self._xSpec
656
657 def GetYSpec(self):
658 """Returns current YSpec for axis"""
659 return self._ySpec
660
661 def GetXMaxRange(self):
662 """Returns (minX, maxX) x-axis range for displayed graph"""
663 graphics= self.last_draw[0]
664 p1, p2 = graphics.boundingBox() # min, max points of graphics
665 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
666 return xAxis
667
668 def GetYMaxRange(self):
669 """Returns (minY, maxY) y-axis range for displayed graph"""
670 graphics= self.last_draw[0]
671 p1, p2 = graphics.boundingBox() # min, max points of graphics
672 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
673 return yAxis
674
675 def GetXCurrentRange(self):
676 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
677 return self.last_draw[1]
678
679 def GetYCurrentRange(self):
680 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
681 return self.last_draw[2]
682
683 def Draw(self, graphics, xAxis = None, yAxis = None, dc = None):
684 """Draw objects in graphics with specified x and y axis.
685 graphics- instance of PlotGraphics with list of PolyXXX objects
686 xAxis - tuple with (min, max) axis range to view
687 yAxis - same as xAxis
688 dc - drawing context - doesn't have to be specified.
689 If it's not, the offscreen buffer is used
690 """
691 # check Axis is either tuple or none
692 if type(xAxis) not in [type(None),tuple]:
693 raise TypeError, "xAxis should be None or (minX,maxX)"
694 if type(yAxis) not in [type(None),tuple]:
695 raise TypeError, "yAxis should be None or (minY,maxY)"
696
697 # check case for axis = (a,b) where a==b caused by improper zooms
698 if xAxis != None:
699 if xAxis[0] == xAxis[1]:
700 return
701 if yAxis != None:
702 if yAxis[0] == yAxis[1]:
703 return
704
705 if dc == None:
706 # allows using floats for certain functions
707 dc = FloatDCWrapper(wx.BufferedDC(wx.ClientDC(self), self._Buffer))
708 dc.Clear()
709
710 dc.BeginDrawing()
711 # dc.Clear()
712
713 # set font size for every thing but title and legend
714 dc.SetFont(self._getFont(self._fontSizeAxis))
715
716 # sizes axis to axis type, create lower left and upper right corners of plot
717 if xAxis == None or yAxis == None:
718 # One or both axis not specified in Draw
719 p1, p2 = graphics.boundingBox() # min, max points of graphics
720 if xAxis == None:
721 xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
722 if yAxis == None:
723 yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
724 # Adjust bounding box for axis spec
725 p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
726 p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
727 else:
728 # Both axis specified in Draw
729 p1= Numeric.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
730 p2= Numeric.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
731
732 self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
733
734 # Get ticks and textExtents for axis if required
735 if self._xSpec is not 'none':
736 xticks = self._ticks(xAxis[0], xAxis[1])
737 xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
738 else:
739 xticks = None
740 xTextExtent= (0,0) # No text for ticks
741 if self._ySpec is not 'none':
742 yticks = self._ticks(yAxis[0], yAxis[1])
743 yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
744 yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
745 yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
746 max(yTextExtentBottom[1],yTextExtentTop[1]))
747 else:
748 yticks = None
749 yTextExtent= (0,0) # No text for ticks
750
751 # TextExtents for Title and Axis Labels
752 titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
753
754 # TextExtents for Legend
755 legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
756
757 # room around graph area
758 rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
759 lhsW= yTextExtent[0]+ yLabelWH[1]
760 bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
761 topH= yTextExtent[1]/2. + titleWH[1]
762 textSize_scale= Numeric.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
763 textSize_shift= Numeric.array([lhsW, bottomH]) # shift plot area by this amount
764
765 # drawing title and labels text
766 dc.SetFont(self._getFont(self._fontSizeTitle))
767 titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
768 self.plotbox_origin[1]- self.plotbox_size[1])
769 dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
770 dc.SetFont(self._getFont(self._fontSizeAxis))
771 xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
772 self.plotbox_origin[1]- xLabelWH[1])
773 dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
774 yLabelPos= (self.plotbox_origin[0],
775 self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
776 if graphics.getYLabel(): # bug fix for Linux
777 dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
778
779 # drawing legend makers and text
780 if self._legendEnabled:
781 self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
782
783 # allow for scaling and shifting plotted points
784 scale = (self.plotbox_size-textSize_scale) / (p2-p1)* Numeric.array((1,-1))
785 shift = -p1*scale + self.plotbox_origin + textSize_shift * Numeric.array((1,-1))
786 self._pointScale= scale # make available for mouse events
787 self._pointShift= shift
788 self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
789
790 graphics.scaleAndShift(scale, shift)
791 graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
792
793 # set clipping area so drawing does not occur outside axis box
794 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
795 dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
796 # Draw the lines and markers
797 #start = time.clock()
798 graphics.draw(dc)
799 # print "entire graphics drawing took: %f second"%(time.clock() - start)
800 # remove the clipping region
801 dc.DestroyClippingRegion()
802 dc.EndDrawing()
803
804 def Redraw(self, dc= None):
805 """Redraw the existing plot."""
806 if self.last_draw is not None:
807 graphics, xAxis, yAxis= self.last_draw
808 self.Draw(graphics,xAxis,yAxis,dc)
809
810 def Clear(self):
811 """Erase the window."""
812 dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
813 dc.Clear()
814 self.last_draw = None
815
816 def Zoom(self, Center, Ratio):
817 """ Zoom on the plot
818 Centers on the X,Y coords given in Center
819 Zooms by the Ratio = (Xratio, Yratio) given
820 """
821 x,y = Center
822 if self.last_draw != None:
823 (graphics, xAxis, yAxis) = self.last_draw
824 w = (xAxis[1] - xAxis[0]) * Ratio[0]
825 h = (yAxis[1] - yAxis[0]) * Ratio[1]
826 xAxis = ( x - w/2, x + w/2 )
827 yAxis = ( y - h/2, y + h/2 )
828 self.Draw(graphics, xAxis, yAxis)
829
830
831 # event handlers **********************************
832 def OnMotion(self, event):
833 if self._zoomEnabled and event.LeftIsDown():
834 if self._hasDragged:
835 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
836 else:
837 self._hasDragged= True
838 self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
839 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
840
841 def OnMouseLeftDown(self,event):
842 self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
843
844 def OnMouseLeftUp(self, event):
845 if self._zoomEnabled:
846 if self._hasDragged == True:
847 self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
848 self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
849 self._hasDragged = False # reset flag
850 minX, minY= Numeric.minimum( self._zoomCorner1, self._zoomCorner2)
851 maxX, maxY= Numeric.maximum( self._zoomCorner1, self._zoomCorner2)
852 if self.last_draw != None:
853 self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
854 #else: # A box has not been drawn, zoom in on a point
855 ## this interfered with the double click, so I've disables it.
856 # X,Y = self.GetXY(event)
857 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
858
859 def OnMouseDoubleClick(self,event):
860 if self._zoomEnabled:
861 self.Reset()
862
863 def OnMouseRightDown(self,event):
864 if self._zoomEnabled:
865 X,Y = self.GetXY(event)
866 self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
867
868 def OnPaint(self, event):
869 # All that is needed here is to draw the buffer to screen
870 dc = wx.BufferedPaintDC(self, self._Buffer)
871
872 def OnSize(self,event):
873 # The Buffer init is done here, to make sure the buffer is always
874 # the same size as the Window
875 Size = self.GetClientSize()
876
877 # Make new offscreen bitmap: this bitmap will always have the
878 # current drawing in it, so it can be used to save the image to
879 # a file, or whatever.
880 self._Buffer = wx.EmptyBitmap(Size[0],Size[1])
881 self._setSize()
882 if self.last_draw is None:
883 self.Clear()
884 else:
885 graphics, xSpec, ySpec = self.last_draw
886 self.Draw(graphics,xSpec,ySpec)
887
888
889 # Private Methods **************************************************
890 def _setSize(self, width=None, height=None):
891 """DC width and height."""
892 if width == None:
893 (self.width,self.height) = self.GetClientSize()
894 else:
895 self.width, self.height= width,height
896 self.plotbox_size = 0.97*Numeric.array([self.width, self.height])
897 xo = 0.5*(self.width-self.plotbox_size[0])
898 yo = self.height-0.5*(self.height-self.plotbox_size[1])
899 self.plotbox_origin = Numeric.array([xo, yo])
900
901 def _setPrinterScale(self, scale):
902 """Used to thicken lines and increase marker size for print out."""
903 # line thickness on printer is very thin at 600 dot/in. Markers small
904 self.printerScale= scale
905
906 def _printDraw(self, printDC):
907 """Used for printing."""
908 if self.last_draw != None:
909 graphics, xSpec, ySpec= self.last_draw
910 self.Draw(graphics,xSpec,ySpec,printDC)
911
912 def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
913 """Draws legend symbols and text"""
914 # top right hand corner of graph box is ref corner
915 trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
916 legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
917 lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
918 dc.SetFont(self._getFont(self._fontSizeLegend))
919 for i in range(len(graphics)):
920 o = graphics[i]
921 s= i*lineHeight
922 if isinstance(o,PolyMarker):
923 # draw marker with legend
924 pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
925 o.draw(dc, self.printerScale, coord= Numeric.array([pnt]))
926 elif isinstance(o,PolyLine):
927 # draw line with legend
928 pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
929 pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
930 o.draw(dc, self.printerScale, coord= Numeric.array([pnt1,pnt2]))
931 else:
932 raise TypeError, "object is neither PolyMarker or PolyLine instance"
933 # draw legend txt
934 pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
935 dc.DrawText(o.getLegend(),pnt[0],pnt[1])
936 dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
937
938 def _titleLablesWH(self, dc, graphics):
939 """Draws Title and labels and returns width and height for each"""
940 # TextExtents for Title and Axis Labels
941 dc.SetFont(self._getFont(self._fontSizeTitle))
942 title= graphics.getTitle()
943 titleWH= dc.GetTextExtent(title)
944 dc.SetFont(self._getFont(self._fontSizeAxis))
945 xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
946 xLabelWH= dc.GetTextExtent(xLabel)
947 yLabelWH= dc.GetTextExtent(yLabel)
948 return titleWH, xLabelWH, yLabelWH
949
950 def _legendWH(self, dc, graphics):
951 """Returns the size in screen units for legend box"""
952 if self._legendEnabled != True:
953 legendBoxWH= symExt= txtExt= (0,0)
954 else:
955 # find max symbol size
956 symExt= graphics.getSymExtent(self.printerScale)
957 # find max legend text extent
958 dc.SetFont(self._getFont(self._fontSizeLegend))
959 txtList= graphics.getLegendNames()
960 txtExt= dc.GetTextExtent(txtList[0])
961 for txt in graphics.getLegendNames()[1:]:
962 txtExt= Numeric.maximum(txtExt,dc.GetTextExtent(txt))
963 maxW= symExt[0]+txtExt[0]
964 maxH= max(symExt[1],txtExt[1])
965 # padding .1 for lhs of legend box and space between lines
966 maxW= maxW* 1.1
967 maxH= maxH* 1.1 * len(txtList)
968 dc.SetFont(self._getFont(self._fontSizeAxis))
969 legendBoxWH= (maxW,maxH)
970 return (legendBoxWH, symExt, txtExt)
971
972 def _drawRubberBand(self, corner1, corner2):
973 """Draws/erases rect box from corner1 to corner2"""
974 ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
975 # draw rectangle
976 dc = wx.ClientDC( self )
977 dc.BeginDrawing()
978 dc.SetPen(wx.Pen(wx.BLACK))
979 dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
980 dc.SetLogicalFunction(wx.INVERT)
981 dc.DrawRectangle( (ptx,pty), (rectWidth,rectHeight))
982 dc.SetLogicalFunction(wx.COPY)
983 dc.EndDrawing()
984
985 def _getFont(self,size):
986 """Take font size, adjusts if printing and returns wx.Font"""
987 s = size*self.printerScale
988 of = self.GetFont()
989 # Linux speed up to get font from cache rather than X font server
990 key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
991 font = self._fontCache.get (key, None)
992 if font:
993 return font # yeah! cache hit
994 else:
995 font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
996 self._fontCache[key] = font
997 return font
998
999
1000 def _point2ClientCoord(self, corner1, corner2):
1001 """Converts user point coords to client screen int coords x,y,width,height"""
1002 c1= Numeric.array(corner1)
1003 c2= Numeric.array(corner2)
1004 # convert to screen coords
1005 pt1= c1*self._pointScale+self._pointShift
1006 pt2= c2*self._pointScale+self._pointShift
1007 # make height and width positive
1008 pul= Numeric.minimum(pt1,pt2) # Upper left corner
1009 plr= Numeric.maximum(pt1,pt2) # Lower right corner
1010 rectWidth, rectHeight= plr-pul
1011 ptx,pty= pul
1012 return int(ptx),int(pty),int(rectWidth),int(rectHeight) # return ints
1013
1014 def _axisInterval(self, spec, lower, upper):
1015 """Returns sensible axis range for given spec"""
1016 if spec == 'none' or spec == 'min':
1017 if lower == upper:
1018 return lower-0.5, upper+0.5
1019 else:
1020 return lower, upper
1021 elif spec == 'auto':
1022 range = upper-lower
1023 if range == 0.:
1024 return lower-0.5, upper+0.5
1025 log = Numeric.log10(range)
1026 power = Numeric.floor(log)
1027 fraction = log-power
1028 if fraction <= 0.05:
1029 power = power-1
1030 grid = 10.**power
1031 lower = lower - lower % grid
1032 mod = upper % grid
1033 if mod != 0:
1034 upper = upper - mod + grid
1035 return lower, upper
1036 elif type(spec) == type(()):
1037 lower, upper = spec
1038 if lower <= upper:
1039 return lower, upper
1040 else:
1041 return upper, lower
1042 else:
1043 raise ValueError, str(spec) + ': illegal axis specification'
1044
1045 def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1046
1047 penWidth= self.printerScale # increases thickness for printing only
1048 dc.SetPen(wx.Pen(wx.NamedColour('BLACK'),int(penWidth)))
1049
1050 # set length of tick marks--long ones make grid
1051 if self._gridEnabled:
1052 x,y,width,height= self._point2ClientCoord(p1,p2)
1053 yTickLength= width/2.0 +1
1054 xTickLength= height/2.0 +1
1055 else:
1056 yTickLength= 3 * self.printerScale # lengthens lines for printing
1057 xTickLength= 3 * self.printerScale
1058
1059 if self._xSpec is not 'none':
1060 lower, upper = p1[0],p2[0]
1061 text = 1
1062 for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
1063 a1 = scale*Numeric.array([lower, y])+shift
1064 a2 = scale*Numeric.array([upper, y])+shift
1065 dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
1066 for x, label in xticks:
1067 pt = scale*Numeric.array([x, y])+shift
1068 dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
1069 if text:
1070 dc.DrawText(label,pt[0],pt[1])
1071 text = 0 # axis values not drawn on top side
1072
1073 if self._ySpec is not 'none':
1074 lower, upper = p1[1],p2[1]
1075 text = 1
1076 h = dc.GetCharHeight()
1077 for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1078 a1 = scale*Numeric.array([x, lower])+shift
1079 a2 = scale*Numeric.array([x, upper])+shift
1080 dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
1081 for y, label in yticks:
1082 pt = scale*Numeric.array([x, y])+shift
1083 dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
1084 if text:
1085 dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
1086 pt[1]-0.5*h)
1087 text = 0 # axis values not drawn on right side
1088
1089 def _ticks(self, lower, upper):
1090 ideal = (upper-lower)/7.
1091 log = Numeric.log10(ideal)
1092 power = Numeric.floor(log)
1093 fraction = log-power
1094 factor = 1.
1095 error = fraction
1096 for f, lf in self._multiples:
1097 e = Numeric.fabs(fraction-lf)
1098 if e < error:
1099 error = e
1100 factor = f
1101 grid = factor * 10.**power
1102 if power > 4 or power < -4:
1103 format = '%+7.1e'
1104 elif power >= 0:
1105 digits = max(1, int(power))
1106 format = '%' + `digits`+'.0f'
1107 else:
1108 digits = -int(power)
1109 format = '%'+`digits+2`+'.'+`digits`+'f'
1110 ticks = []
1111 t = -grid*Numeric.floor(-lower/grid)
1112 while t <= upper:
1113 ticks.append( (t, format % (t,)) )
1114 t = t + grid
1115 return ticks
1116
1117 _multiples = [(2., Numeric.log10(2.)), (5., Numeric.log10(5.))]
1118
1119
1120 #-------------------------------------------------------------------------------
1121 # Used to layout the printer page
1122
1123 class PlotPrintout(wx.Printout):
1124 """Controls how the plot is made in printing and previewing"""
1125 # Do not change method names in this class,
1126 # we have to override wx.Printout methods here!
1127 def __init__(self, graph):
1128 """graph is instance of plotCanvas to be printed or previewed"""
1129 wx.Printout.__init__(self)
1130 self.graph = graph
1131
1132 def HasPage(self, page):
1133 if page == 1:
1134 return True
1135 else:
1136 return False
1137
1138 def GetPageInfo(self):
1139 return (0, 1, 1, 1) # disable page numbers
1140
1141 def OnPrintPage(self, page):
1142 dc = FloatDCWrapper(self.GetDC()) # allows using floats for certain functions
1143 ## print "PPI Printer",self.GetPPIPrinter()
1144 ## print "PPI Screen", self.GetPPIScreen()
1145 ## print "DC GetSize", dc.GetSize()
1146 ## print "GetPageSizePixels", self.GetPageSizePixels()
1147 # Note PPIScreen does not give the correct number
1148 # Calulate everything for printer and then scale for preview
1149 PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
1150 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1151 dcSize= dc.GetSize() # DC size
1152 pageSize= self.GetPageSizePixels() # page size in terms of pixcels
1153 clientDcSize= self.graph.GetClientSize()
1154
1155 # find what the margins are (mm)
1156 margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
1157 margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
1158
1159 # calculate offset and scale for dc
1160 pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
1161 pixRight= margRightSize*PPIPrinter[0]/25.4
1162 pixTop= margTopSize*PPIPrinter[1]/25.4
1163 pixBottom= margBottomSize*PPIPrinter[1]/25.4
1164
1165 plotAreaW= pageSize[0]-(pixLeft+pixRight)
1166 plotAreaH= pageSize[1]-(pixTop+pixBottom)
1167
1168 # ratio offset and scale to screen size if preview
1169 if self.IsPreview():
1170 ratioW= float(dcSize[0])/pageSize[0]
1171 ratioH= float(dcSize[1])/pageSize[1]
1172 pixLeft *= ratioW
1173 pixTop *= ratioH
1174 plotAreaW *= ratioW
1175 plotAreaH *= ratioH
1176
1177 # rescale plot to page or preview plot area
1178 self.graph._setSize(plotAreaW,plotAreaH)
1179
1180 # Set offset and scale
1181 dc.SetDeviceOrigin(pixLeft,pixTop)
1182
1183 # Thicken up pens and increase marker size for printing
1184 ratioW= float(plotAreaW)/clientDcSize[0]
1185 ratioH= float(plotAreaH)/clientDcSize[1]
1186 aveScale= (ratioW+ratioH)/2
1187 self.graph._setPrinterScale(aveScale) # tickens up pens for printing
1188
1189 self.graph._printDraw(dc)
1190 # rescale back to original
1191 self.graph._setSize()
1192 self.graph._setPrinterScale(1)
1193
1194 return True
1195
1196 # Hack to allow plotting real numbers for the methods listed.
1197 # All others passed directly to DC.
1198 # For Drawing it is used as
1199 # dc = FloatDCWrapper(wx.BufferedDC(wx.ClientDC(self), self._Buffer))
1200 # For printing is is used as
1201 # dc = FloatDCWrapper(self.GetDC())
1202 class FloatDCWrapper:
1203 def __init__(self, aDC):
1204 self.theDC = aDC
1205
1206 def DrawLine(self, x1,y1,x2,y2):
1207 self.theDC.DrawLine((int(x1),int(y1)),(int(x2),int(y2)))
1208
1209 def DrawText(self, txt, x, y):
1210 self.theDC.DrawText(txt, (int(x), int(y)))
1211
1212 def DrawRotatedText(self, txt, x, y, angle):
1213 self.theDC.DrawRotatedText(txt, (int(x), int(y)), angle)
1214
1215 def SetClippingRegion(self, x, y, width, height):
1216 self.theDC.SetClippingRegion((int(x), int(y)), (int(width), int(height)))
1217
1218 def SetDeviceOrigin(self, x, y):
1219 self.theDC.SetDeviceOrigin(int(x), int(y))
1220
1221 def __getattr__(self, name):
1222 return getattr(self.theDC, name)
1223
1224
1225
1226
1227 #---------------------------------------------------------------------------
1228 # if running standalone...
1229 #
1230 # ...a sample implementation using the above
1231 #
1232
1233 def _draw1Objects():
1234 # 100 points sin function, plotted as green circles
1235 data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
1236 data1.shape = (100, 2)
1237 data1[:,1] = Numeric.sin(data1[:,0])
1238 markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
1239
1240 # 50 points cos function, plotted as red line
1241 data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
1242 data1.shape = (50,2)
1243 data1[:,1] = Numeric.cos(data1[:,0])
1244 lines = PolyLine(data1, legend= 'Red Line', colour='red')
1245
1246 # A few more points...
1247 pi = Numeric.pi
1248 markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1249 (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
1250 marker='cross')
1251
1252 return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
1253
1254 def _draw2Objects():
1255 # 100 points sin function, plotted as green dots
1256 data1 = 2.*Numeric.pi*Numeric.arange(200)/200.
1257 data1.shape = (100, 2)
1258 data1[:,1] = Numeric.sin(data1[:,0])
1259 line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
1260
1261 # 50 points cos function, plotted as red dot-dash
1262 data1 = 2.*Numeric.pi*Numeric.arange(100)/100.
1263 data1.shape = (50,2)
1264 data1[:,1] = Numeric.cos(data1[:,0])
1265 line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
1266
1267 # A few more points...
1268 pi = Numeric.pi
1269 markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
1270 (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
1271 fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
1272 marker='square')
1273
1274 return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
1275
1276 def _draw3Objects():
1277 markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1278 'cross', 'plus', 'circle']
1279 m=[]
1280 for i in range(len(markerList)):
1281 m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
1282 marker=markerList[i]))
1283 return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
1284
1285 def _draw4Objects():
1286 # 25,000 point line
1287 data1 = Numeric.arange(5e5,1e6,10)
1288 data1.shape = (25000, 2)
1289 line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
1290
1291 # A few more points...
1292 markers2 = PolyMarker(data1, legend='Square', colour='blue',
1293 marker='square')
1294 return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
1295
1296 def _draw5Objects():
1297 # Empty graph with axis defined but no points/lines
1298 points=[]
1299 line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
1300 return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
1301
1302
1303 class TestFrame(wx.Frame):
1304 def __init__(self, parent, id, title):
1305 wx.Frame.__init__(self, parent, id, title,
1306 wx.DefaultPosition, (600, 400))
1307
1308 # Now Create the menu bar and items
1309 self.mainmenu = wx.MenuBar()
1310
1311 menu = wx.Menu()
1312 menu.Append(200, 'Page Setup...', 'Setup the printer page')
1313 self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
1314
1315 menu.Append(201, 'Print Preview...', 'Show the current plot on page')
1316 self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
1317
1318 menu.Append(202, 'Print...', 'Print the current plot')
1319 self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
1320
1321 menu.Append(203, 'Save Plot...', 'Save current plot')
1322 self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
1323
1324 menu.Append(205, 'E&xit', 'Enough of this already!')
1325 self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
1326 self.mainmenu.Append(menu, '&File')
1327
1328 menu = wx.Menu()
1329 menu.Append(206, 'Draw1', 'Draw plots1')
1330 self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
1331 menu.Append(207, 'Draw2', 'Draw plots2')
1332 self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
1333 menu.Append(208, 'Draw3', 'Draw plots3')
1334 self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
1335 menu.Append(209, 'Draw4', 'Draw plots4')
1336 self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
1337 menu.Append(210, 'Draw5', 'Draw plots5')
1338 self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
1339
1340 menu.Append(211, '&Redraw', 'Redraw plots')
1341 self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
1342 menu.Append(212, '&Clear', 'Clear canvas')
1343 self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
1344 menu.Append(213, '&Scale', 'Scale canvas')
1345 self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
1346 menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
1347 self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
1348 menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
1349 self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
1350 menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
1351 self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
1352 menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1353 self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
1354 menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1355 self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
1356 menu.Append(235, '&Plot Reset', 'Reset to original plot')
1357 self.Bind(wx.EVT_MENU,self.OnReset, id=235)
1358
1359 self.mainmenu.Append(menu, '&Plot')
1360
1361 menu = wx.Menu()
1362 menu.Append(300, '&About', 'About this thing...')
1363 self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
1364 self.mainmenu.Append(menu, '&Help')
1365
1366 self.SetMenuBar(self.mainmenu)
1367
1368 # A status bar to tell people what's happening
1369 self.CreateStatusBar(1)
1370
1371 self.client = PlotCanvas(self)
1372 # Create mouse event for showing cursor coords in status bar
1373 self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
1374 self.Show(True)
1375
1376 def OnMouseLeftDown(self,event):
1377 s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
1378 self.SetStatusText(s)
1379 event.Skip()
1380
1381 def OnFilePageSetup(self, event):
1382 self.client.PageSetup()
1383
1384 def OnFilePrintPreview(self, event):
1385 self.client.PrintPreview()
1386
1387 def OnFilePrint(self, event):
1388 self.client.Printout()
1389
1390 def OnSaveFile(self, event):
1391 self.client.SaveFile()
1392
1393 def OnFileExit(self, event):
1394 self.Close()
1395
1396 def OnPlotDraw1(self, event):
1397 self.resetDefaults()
1398 self.client.Draw(_draw1Objects())
1399
1400 def OnPlotDraw2(self, event):
1401 self.resetDefaults()
1402 self.client.Draw(_draw2Objects())
1403
1404 def OnPlotDraw3(self, event):
1405 self.resetDefaults()
1406 self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
1407 self.client.SetFontSizeAxis(20)
1408 self.client.SetFontSizeLegend(12)
1409 self.client.SetXSpec('min')
1410 self.client.SetYSpec('none')
1411 self.client.Draw(_draw3Objects())
1412
1413 def OnPlotDraw4(self, event):
1414 self.resetDefaults()
1415 drawObj= _draw4Objects()
1416 self.client.Draw(drawObj)
1417 ## # profile
1418 ## start = time.clock()
1419 ## for x in range(10):
1420 ## self.client.Draw(drawObj)
1421 ## print "10 plots of Draw4 took: %f sec."%(time.clock() - start)
1422 ## # profile end
1423
1424 def OnPlotDraw5(self, event):
1425 # Empty plot with just axes
1426 self.resetDefaults()
1427 drawObj= _draw5Objects()
1428 # make the axis X= (0,5), Y=(0,10)
1429 # (default with None is X= (-1,1), Y= (-1,1))
1430 self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
1431
1432 def OnPlotRedraw(self,event):
1433 self.client.Redraw()
1434
1435 def OnPlotClear(self,event):
1436 self.client.Clear()
1437
1438 def OnPlotScale(self, event):
1439 if self.client.last_draw != None:
1440 graphics, xAxis, yAxis= self.client.last_draw
1441 self.client.Draw(graphics,(1,3.05),(0,1))
1442
1443 def OnEnableZoom(self, event):
1444 self.client.SetEnableZoom(event.IsChecked())
1445
1446 def OnEnableGrid(self, event):
1447 self.client.SetEnableGrid(event.IsChecked())
1448
1449 def OnEnableLegend(self, event):
1450 self.client.SetEnableLegend(event.IsChecked())
1451
1452 def OnScrUp(self, event):
1453 self.client.ScrollUp(1)
1454
1455 def OnScrRt(self,event):
1456 self.client.ScrollRight(2)
1457
1458 def OnReset(self,event):
1459 self.client.Reset()
1460
1461 def OnHelpAbout(self, event):
1462 from wx.lib.dialogs import ScrolledMessageDialog
1463 about = ScrolledMessageDialog(self, __doc__, "About...")
1464 about.ShowModal()
1465
1466 def resetDefaults(self):
1467 """Just to reset the fonts back to the PlotCanvas defaults"""
1468 self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
1469 self.client.SetFontSizeAxis(10)
1470 self.client.SetFontSizeLegend(7)
1471 self.client.SetXSpec('auto')
1472 self.client.SetYSpec('auto')
1473
1474
1475 def __test():
1476
1477 class MyApp(wx.App):
1478 def OnInit(self):
1479 wx.InitAllImageHandlers()
1480 frame = TestFrame(None, -1, "PlotCanvas")
1481 #frame.Show(True)
1482 self.SetTopWindow(frame)
1483 return True
1484
1485
1486 app = MyApp(0)
1487 app.MainLoop()
1488
1489 if __name__ == '__main__':
1490 __test()