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