]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/plot.py
1 #-----------------------------------------------------------------------------
3 # Purpose: Line, Bar and Scatter Graphs
5 # Author: Gordon Williams
10 # Licence: Use as you wish.
11 #-----------------------------------------------------------------------------
12 # 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
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.
19 # 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
21 # o wxScrolledMessageDialog -> ScrolledMessageDialog
23 # Oct 6, 2004 Gordon Williams (g_will@cyberus.ca)
24 # - Added bar graph demo
25 # - Modified line end shape from round to square.
26 # - Removed FloatDCWrapper for conversion to ints and ints in arguments
28 # Oct 15, 2004 Gordon Williams (g_will@cyberus.ca)
29 # - Imported modules given leading underscore to name.
30 # - Added Cursor Line Tracking and User Point Labels.
31 # - Demo for Cursor Line Tracking and Point Labels.
32 # - Size of plot preview frame adjusted to show page better.
33 # - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
34 # - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
35 # can be in either user coords or screen coords.
40 This is a simple light weight plotting module that can be used with
41 Boa or easily integrated into your own wxPython application. The
42 emphasis is on small size and fast plotting for large data sets. It
43 has a reasonable number of features to do line and scatter graphs
44 easily as well as simple bar graphs. It is not as sophisticated or
45 as powerful as SciPy Plt or Chaco. Both of these are great packages
46 but consume huge amounts of computer resources for simple plots.
47 They can be found at http://scipy.com
49 This file contains two parts; first the re-usable library stuff, then,
50 after a "if __name__=='__main__'" test, a simple frame and a few default
51 plots for examples and testing.
54 Written by K.Hinsen, R. Srinivasan;
55 Ported to wxPython Harm van der Heijden, feb 1999
57 Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
59 -Zooming using mouse "rubber band"
62 -Printing, preview, and page set up (margins)
63 -Axis and title labels
64 -Cursor xy axis values
65 -Doc strings and lots of comments
66 -Optimizations for large number of points
69 Did a lot of work here to speed markers up. Only a factor of 4
70 improvement though. Lines are much faster than markers, especially
71 filled markers. Stay away from circles and triangles unless you
72 only have a few thousand points.
74 Times for 25,000 points
81 triangle, triangle_down - 0.90
83 Thanks to Chris Barker for getting this version working on Linux.
85 Zooming controls with mouse (when enabled):
86 Left mouse drag - Zoom box.
87 Left mouse double click - reset zoom.
88 Right mouse click - zoom out centred on click location.
91 import string
as _string
95 # Needs Numeric or numarray
97 import Numeric
as _Numeric
100 import numarray
as _Numeric
#if numarray is used it is renamed Numeric
103 This module requires the Numeric or numarray module,
104 which could not be imported. It probably is not installed
105 (it's not part of the standard Python distribution). See the
106 Python site (http://www.python.org) for information on
107 downloading source or binaries."""
108 raise ImportError, "Numeric or numarray not found. \n" + msg
113 # Plotting classes...
116 """Base Class for lines and markers
117 - All methods are private.
120 def __init__(self
, points
, attr
):
121 self
.points
= _Numeric
.array(points
)
122 self
.currentScale
= (1,1)
123 self
.currentShift
= (0,0)
124 self
.scaled
= self
.points
126 self
.attributes
.update(self
._attributes
)
127 for name
, value
in attr
.items():
128 if name
not in self
._attributes
.keys():
129 raise KeyError, "Style attribute incorrect. Should be one of %s" % self
._attributes
.keys()
130 self
.attributes
[name
] = value
132 def boundingBox(self
):
133 if len(self
.points
) == 0:
135 # defaults to (-1,-1) and (1,1) but axis can be set in Draw
136 minXY
= _Numeric
.array([-1,-1])
137 maxXY
= _Numeric
.array([ 1, 1])
139 minXY
= _Numeric
.minimum
.reduce(self
.points
)
140 maxXY
= _Numeric
.maximum
.reduce(self
.points
)
143 def scaleAndShift(self
, scale
=(1,1), shift
=(0,0)):
144 if len(self
.points
) == 0:
147 if (scale
is not self
.currentScale
) or (shift
is not self
.currentShift
):
148 # update point scaling
149 self
.scaled
= scale
*self
.points
+shift
150 self
.currentScale
= scale
151 self
.currentShift
= shift
152 # else unchanged use the current scaling
155 return self
.attributes
['legend']
157 def getClosestPoint(self
, pntXY
, pointScaled
= True):
158 """Returns the index of closest point on the curve, pointXY, scaledXY, distance
160 if pointScaled == True based on screen coords
161 if pointScaled == False based on user coords
163 if pointScaled
== True:
166 pxy
= self
.currentScale
* _Numeric
.array(pntXY
)+ self
.currentShift
170 pxy
= _Numeric
.array(pntXY
)
171 #determine distance for each point
172 d
= _Numeric
.sqrt(_Numeric
.add
.reduce((p
-pxy
)**2,1)) #sqrt(dx^2+dy^2)
173 pntIndex
= _Numeric
.argmin(d
)
175 return [pntIndex
, self
.points
[pntIndex
], self
.scaled
[pntIndex
], dist
]
178 class PolyLine(PolyPoints
):
179 """Class to define line type and style
180 - All methods except __init__ are private.
183 _attributes
= {'colour': 'black',
188 def __init__(self
, points
, **attr
):
189 """Creates PolyLine object
190 points - sequence (array, tuple or list) of (x,y) points making up line
191 **attr - key word attributes
193 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
194 'width'= 1, - Pen width
195 'style'= wx.SOLID, - wx.Pen style
196 'legend'= '' - Line Legend to display
198 PolyPoints
.__init
__(self
, points
, attr
)
200 def draw(self
, dc
, printerScale
, coord
= None):
201 colour
= self
.attributes
['colour']
202 width
= self
.attributes
['width'] * printerScale
203 style
= self
.attributes
['style']
204 if not isinstance(colour
, wx
.Colour
):
205 colour
= wx
.NamedColour(colour
)
206 pen
= wx
.Pen(colour
, width
, style
)
207 pen
.SetCap(wx
.CAP_BUTT
)
210 dc
.DrawLines(self
.scaled
)
212 dc
.DrawLines(coord
) # draw legend line
214 def getSymExtent(self
, printerScale
):
215 """Width and Height of Marker"""
216 h
= self
.attributes
['width'] * printerScale
221 class PolyMarker(PolyPoints
):
222 """Class to define marker type and style
223 - All methods except __init__ are private.
226 _attributes
= {'colour': 'black',
230 'fillstyle': wx
.SOLID
,
234 def __init__(self
, points
, **attr
):
235 """Creates PolyMarker object
236 points - sequence (array, tuple or list) of (x,y) points
237 **attr - key word attributes
239 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
240 'width'= 1, - Pen width
241 'size'= 2, - Marker size
242 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
243 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
244 'marker'= 'circle' - Marker shape
245 'legend'= '' - Marker Legend to display
257 PolyPoints
.__init
__(self
, points
, attr
)
259 def draw(self
, dc
, printerScale
, coord
= None):
260 colour
= self
.attributes
['colour']
261 width
= self
.attributes
['width'] * printerScale
262 size
= self
.attributes
['size'] * printerScale
263 fillcolour
= self
.attributes
['fillcolour']
264 fillstyle
= self
.attributes
['fillstyle']
265 marker
= self
.attributes
['marker']
267 if colour
and not isinstance(colour
, wx
.Colour
):
268 colour
= wx
.NamedColour(colour
)
269 if fillcolour
and not isinstance(fillcolour
, wx
.Colour
):
270 fillcolour
= wx
.NamedColour(fillcolour
)
272 dc
.SetPen(wx
.Pen(colour
, width
))
274 dc
.SetBrush(wx
.Brush(fillcolour
,fillstyle
))
276 dc
.SetBrush(wx
.Brush(colour
, fillstyle
))
278 self
._drawmarkers
(dc
, self
.scaled
, marker
, size
)
280 self
._drawmarkers
(dc
, coord
, marker
, size
) # draw legend marker
282 def getSymExtent(self
, printerScale
):
283 """Width and Height of Marker"""
284 s
= 5*self
.attributes
['size'] * printerScale
287 def _drawmarkers(self
, dc
, coords
, marker
,size
=1):
288 f
= eval('self._' +marker
)
291 def _circle(self
, dc
, coords
, size
=1):
294 rect
= _Numeric
.zeros((len(coords
),4),_Numeric
.Float
)+[0.0,0.0,wh
,wh
]
295 rect
[:,0:2]= coords
-[fact
,fact
]
296 dc
.DrawEllipseList(rect
.astype(_Numeric
.Int32
))
298 def _dot(self
, dc
, coords
, size
=1):
299 dc
.DrawPointList(coords
)
301 def _square(self
, dc
, coords
, size
=1):
304 rect
= _Numeric
.zeros((len(coords
),4),_Numeric
.Float
)+[0.0,0.0,wh
,wh
]
305 rect
[:,0:2]= coords
-[fact
,fact
]
306 dc
.DrawRectangleList(rect
.astype(_Numeric
.Int32
))
308 def _triangle(self
, dc
, coords
, size
=1):
309 shape
= [(-2.5*size
,1.44*size
), (2.5*size
,1.44*size
), (0.0,-2.88*size
)]
310 poly
= _Numeric
.repeat(coords
,3)
311 poly
.shape
= (len(coords
),3,2)
313 dc
.DrawPolygonList(poly
.astype(_Numeric
.Int32
))
315 def _triangle_down(self
, dc
, coords
, size
=1):
316 shape
= [(-2.5*size
,-1.44*size
), (2.5*size
,-1.44*size
), (0.0,2.88*size
)]
317 poly
= _Numeric
.repeat(coords
,3)
318 poly
.shape
= (len(coords
),3,2)
320 dc
.DrawPolygonList(poly
.astype(_Numeric
.Int32
))
322 def _cross(self
, dc
, coords
, size
=1):
324 for f
in [[-fact
,-fact
,fact
,fact
],[-fact
,fact
,fact
,-fact
]]:
325 lines
= _Numeric
.concatenate((coords
,coords
),axis
=1)+f
326 dc
.DrawLineList(lines
.astype(_Numeric
.Int32
))
328 def _plus(self
, dc
, coords
, size
=1):
330 for f
in [[-fact
,0,fact
,0],[0,-fact
,0,fact
]]:
331 lines
= _Numeric
.concatenate((coords
,coords
),axis
=1)+f
332 dc
.DrawLineList(lines
.astype(_Numeric
.Int32
))
335 """Container to hold PolyXXX objects and graph labels
336 - All methods except __init__ are private.
339 def __init__(self
, objects
, title
='', xLabel
='', yLabel
= ''):
340 """Creates PlotGraphics object
341 objects - list of PolyXXX objects to make graph
342 title - title shown at top of graph
343 xLabel - label shown on x-axis
344 yLabel - label shown on y-axis
346 if type(objects
) not in [list,tuple]:
347 raise TypeError, "objects argument should be list or tuple"
348 self
.objects
= objects
353 def boundingBox(self
):
354 p1
, p2
= self
.objects
[0].boundingBox()
355 for o
in self
.objects
[1:]:
356 p1o
, p2o
= o
.boundingBox()
357 p1
= _Numeric
.minimum(p1
, p1o
)
358 p2
= _Numeric
.maximum(p2
, p2o
)
361 def scaleAndShift(self
, scale
=(1,1), shift
=(0,0)):
362 for o
in self
.objects
:
363 o
.scaleAndShift(scale
, shift
)
365 def setPrinterScale(self
, scale
):
366 """Thickens up lines and markers only for printing"""
367 self
.printerScale
= scale
369 def setXLabel(self
, xLabel
= ''):
370 """Set the X axis label on the graph"""
373 def setYLabel(self
, yLabel
= ''):
374 """Set the Y axis label on the graph"""
377 def setTitle(self
, title
= ''):
378 """Set the title at the top of graph"""
382 """Get x axis label string"""
386 """Get y axis label string"""
389 def getTitle(self
, title
= ''):
390 """Get the title at the top of graph"""
394 for o
in self
.objects
:
395 #t=_time.clock() # profile info
396 o
.draw(dc
, self
.printerScale
)
398 #print o, "time=", dt
400 def getSymExtent(self
, printerScale
):
401 """Get max width and height of lines and markers symbols for legend"""
402 symExt
= self
.objects
[0].getSymExtent(printerScale
)
403 for o
in self
.objects
[1:]:
404 oSymExt
= o
.getSymExtent(printerScale
)
405 symExt
= _Numeric
.maximum(symExt
, oSymExt
)
408 def getLegendNames(self
):
409 """Returns list of legend names"""
410 lst
= [None]*len(self
)
411 for i
in range(len(self
)):
412 lst
[i
]= self
.objects
[i
].getLegend()
416 return len(self
.objects
)
418 def __getitem__(self
, item
):
419 return self
.objects
[item
]
422 #-------------------------------------------------------------------------------
423 # Main window that you will want to import into your application.
425 class PlotCanvas(wx
.Window
):
426 """Subclass of a wx.Window to allow simple general plotting
427 of data with zoom, labels, and automatic axis scaling."""
429 def __init__(self
, parent
, id = -1, pos
=wx
.DefaultPosition
,
430 size
=wx
.DefaultSize
, style
= wx
.DEFAULT_FRAME_STYLE
, name
= ""):
431 """Constucts a window, which can be a child of a frame, dialog or
432 any other non-control window"""
434 wx
.Window
.__init
__(self
, parent
, id, pos
, size
, style
, name
)
437 self
.SetBackgroundColour("white")
439 # Create some mouse events for zooming
440 self
.Bind(wx
.EVT_LEFT_DOWN
, self
.OnMouseLeftDown
)
441 self
.Bind(wx
.EVT_LEFT_UP
, self
.OnMouseLeftUp
)
442 self
.Bind(wx
.EVT_MOTION
, self
.OnMotion
)
443 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
.OnMouseDoubleClick
)
444 self
.Bind(wx
.EVT_RIGHT_DOWN
, self
.OnMouseRightDown
)
446 # set curser as cross-hairs
447 self
.SetCursor(wx
.CROSS_CURSOR
)
449 # Things for printing
450 self
.print_data
= wx
.PrintData()
451 self
.print_data
.SetPaperId(wx
.PAPER_LETTER
)
452 self
.print_data
.SetOrientation(wx
.LANDSCAPE
)
453 self
.pageSetupData
= wx
.PageSetupDialogData()
454 self
.pageSetupData
.SetMarginBottomRight((25,25))
455 self
.pageSetupData
.SetMarginTopLeft((25,25))
456 self
.pageSetupData
.SetPrintData(self
.print_data
)
457 self
.printerScale
= 1
461 self
._zoomInFactor
= 0.5
462 self
._zoomOutFactor
= 2
463 self
._zoomCorner
1= _Numeric
.array([0.0, 0.0]) # left mouse down corner
464 self
._zoomCorner
2= _Numeric
.array([0.0, 0.0]) # left mouse up corner
465 self
._zoomEnabled
= False
466 self
._hasDragged
= False
469 self
.last_draw
= None
474 self
._gridEnabled
= False
475 self
._legendEnabled
= False
479 self
._fontSizeAxis
= 10
480 self
._fontSizeTitle
= 15
481 self
._fontSizeLegend
= 7
484 self
._pointLabelEnabled
= False
485 self
.last_PointLabel
= None
486 self
._pointLabelFunc
= None
487 self
.Bind(wx
.EVT_LEAVE_WINDOW
, self
.OnLeave
)
489 self
.Bind(wx
.EVT_PAINT
, self
.OnPaint
)
490 self
.Bind(wx
.EVT_SIZE
, self
.OnSize
)
491 # OnSize called to make sure the buffer is initialized.
492 # This might result in OnSize getting called twice on some
493 # platforms at initialization, but little harm done.
494 if wx
.Platform
!= "__WXMAC__":
495 self
.OnSize(None) # sets the initial size based on client size
497 self
._gridColour
= wx
.NamedColour('black')
499 def GetGridColour(self
):
500 return self
._gridColour
502 def SetGridColour(self
, colour
):
503 if isinstance(colour
, wx
.Colour
):
504 self
._gridColour
= colour
506 self
._gridColour
= wx
.NamedColour(colour
)
510 def SaveFile(self
, fileName
= ''):
511 """Saves the file to the type specified in the extension. If no file
512 name is specified a dialog box is provided. Returns True if sucessful,
515 .bmp Save a Windows bitmap file.
516 .xbm Save an X bitmap file.
517 .xpm Save an XPM bitmap file.
518 .png Save a Portable Network Graphics file.
519 .jpg Save a Joint Photographic Experts Group file.
521 if _string
.lower(fileName
[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
522 dlg1
= wx
.FileDialog(
524 "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
525 "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
526 wx
.SAVE|wx
.OVERWRITE_PROMPT
530 if dlg1
.ShowModal() == wx
.ID_OK
:
531 fileName
= dlg1
.GetPath()
532 # Check for proper exension
533 if _string
.lower(fileName
[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
534 dlg2
= wx
.MessageDialog(self
, 'File name extension\n'
536 'bmp, xbm, xpm, png, or jpg',
537 'File Name Error', wx
.OK | wx
.ICON_ERROR
)
543 break # now save file
544 else: # exit without saving
549 # File name has required extension
550 fType
= _string
.lower(fileName
[-3:])
552 tp
= wx
.BITMAP_TYPE_BMP
# Save a Windows bitmap file.
554 tp
= wx
.BITMAP_TYPE_XBM
# Save an X bitmap file.
556 tp
= wx
.BITMAP_TYPE_XPM
# Save an XPM bitmap file.
558 tp
= wx
.BITMAP_TYPE_JPEG
# Save a JPG file.
560 tp
= wx
.BITMAP_TYPE_PNG
# Save a PNG file.
562 res
= self
._Buffer
.SaveFile(fileName
, tp
)
566 """Brings up the page setup dialog"""
567 data
= self
.pageSetupData
568 data
.SetPrintData(self
.print_data
)
569 dlg
= wx
.PageSetupDialog(self
.parent
, data
)
571 if dlg
.ShowModal() == wx
.ID_OK
:
572 data
= dlg
.GetPageSetupData() # returns wx.PageSetupDialogData
573 # updates page parameters from dialog
574 self
.pageSetupData
.SetMarginBottomRight(data
.GetMarginBottomRight())
575 self
.pageSetupData
.SetMarginTopLeft(data
.GetMarginTopLeft())
576 self
.pageSetupData
.SetPrintData(data
.GetPrintData())
577 self
.print_data
=wx
.PrintData(data
.GetPrintData()) # updates print_data
581 def Printout(self
, paper
=None):
582 """Print current plot."""
584 self
.print_data
.SetPaperId(paper
)
585 pdd
= wx
.PrintDialogData(self
.print_data
)
586 printer
= wx
.Printer(pdd
)
587 out
= PlotPrintout(self
)
588 print_ok
= printer
.Print(self
.parent
, out
)
590 self
.print_data
= wx
.PrintData(printer
.GetPrintDialogData().GetPrintData())
593 def PrintPreview(self
):
594 """Print-preview current plot."""
595 printout
= PlotPrintout(self
)
596 printout2
= PlotPrintout(self
)
597 self
.preview
= wx
.PrintPreview(printout
, printout2
, self
.print_data
)
598 if not self
.preview
.Ok():
599 wx
.MessageDialog(self
, "Print Preview failed.\n" \
600 "Check that default printer is configured\n", \
601 "Print error", wx
.OK|wx
.CENTRE
).ShowModal()
602 self
.preview
.SetZoom(40)
603 # search up tree to find frame instance
605 while not isinstance(frameInst
, wx
.Frame
):
606 frameInst
= frameInst
.GetParent()
607 frame
= wx
.PreviewFrame(self
.preview
, frameInst
, "Preview")
609 frame
.SetPosition(self
.GetPosition())
610 frame
.SetSize((600,550))
611 frame
.Centre(wx
.BOTH
)
614 def SetFontSizeAxis(self
, point
= 10):
615 """Set the tick and axis label font size (default is 10 point)"""
616 self
._fontSizeAxis
= point
618 def GetFontSizeAxis(self
):
619 """Get current tick and axis label font size in points"""
620 return self
._fontSizeAxis
622 def SetFontSizeTitle(self
, point
= 15):
623 """Set Title font size (default is 15 point)"""
624 self
._fontSizeTitle
= point
626 def GetFontSizeTitle(self
):
627 """Get current Title font size in points"""
628 return self
._fontSizeTitle
630 def SetFontSizeLegend(self
, point
= 7):
631 """Set Legend font size (default is 7 point)"""
632 self
._fontSizeLegend
= point
634 def GetFontSizeLegend(self
):
635 """Get current Legend font size in points"""
636 return self
._fontSizeLegend
638 def SetEnableZoom(self
, value
):
639 """Set True to enable zooming."""
640 if value
not in [True,False]:
641 raise TypeError, "Value should be True or False"
642 self
._zoomEnabled
= value
644 def GetEnableZoom(self
):
645 """True if zooming enabled."""
646 return self
._zoomEnabled
648 def SetEnableGrid(self
, value
):
649 """Set True to enable grid."""
650 if value
not in [True,False,'Horizontal','Vertical']:
651 raise TypeError, "Value should be True, False, Horizontal or Vertical"
652 self
._gridEnabled
= value
655 def GetEnableGrid(self
):
656 """True if grid enabled."""
657 return self
._gridEnabled
659 def SetEnableLegend(self
, value
):
660 """Set True to enable legend."""
661 if value
not in [True,False]:
662 raise TypeError, "Value should be True or False"
663 self
._legendEnabled
= value
666 def GetEnableLegend(self
):
667 """True if Legend enabled."""
668 return self
._legendEnabled
670 def SetEnablePointLabel(self
, value
):
671 """Set True to enable pointLabel."""
672 if value
not in [True,False]:
673 raise TypeError, "Value should be True or False"
674 self
._pointLabelEnabled
= value
675 self
.Redraw() #will erase existing pointLabel if present
676 self
.last_PointLabel
= None
678 def GetEnablePointLabel(self
):
679 """True if pointLabel enabled."""
680 return self
._pointLabelEnabled
682 def SetPointLabelFunc(self
, func
):
683 """Sets the function with custom code for pointLabel drawing
684 ******** more info needed ***************
686 self
._pointLabelFunc
= func
688 def GetPointLabelFunc(self
):
689 """Returns pointLabel Drawing Function"""
690 return self
._pointLabelFunc
693 """Unzoom the plot."""
694 self
.last_PointLabel
= None #reset pointLabel
695 if self
.last_draw
is not None:
696 self
.Draw(self
.last_draw
[0])
698 def ScrollRight(self
, units
):
699 """Move view right number of axis units."""
700 self
.last_PointLabel
= None #reset pointLabel
701 if self
.last_draw
is not None:
702 graphics
, xAxis
, yAxis
= self
.last_draw
703 xAxis
= (xAxis
[0]+units
, xAxis
[1]+units
)
704 self
.Draw(graphics
,xAxis
,yAxis
)
706 def ScrollUp(self
, units
):
707 """Move view up number of axis units."""
708 self
.last_PointLabel
= None #reset pointLabel
709 if self
.last_draw
is not None:
710 graphics
, xAxis
, yAxis
= self
.last_draw
711 yAxis
= (yAxis
[0]+units
, yAxis
[1]+units
)
712 self
.Draw(graphics
,xAxis
,yAxis
)
715 def GetXY(self
,event
):
716 """Takes a mouse event and returns the XY user axis values."""
717 x
,y
= self
.PositionScreenToUser(event
.GetPosition())
720 def PositionUserToScreen(self
, pntXY
):
721 """Converts User position to Screen Coordinates"""
722 userPos
= _Numeric
.array(pntXY
)
723 x
,y
= userPos
* self
._pointScale
+ self
._pointShift
726 def PositionScreenToUser(self
, pntXY
):
727 """Converts Screen position to User Coordinates"""
728 screenPos
= _Numeric
.array(pntXY
)
729 x
,y
= (screenPos
-self
._pointShift
)/self
._pointScale
732 def SetXSpec(self
, type= 'auto'):
733 """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
735 'none' - shows no axis or tick mark values
736 'min' - shows min bounding box values
737 'auto' - rounds axis range to sensible values
741 def SetYSpec(self
, type= 'auto'):
742 """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
744 'none' - shows no axis or tick mark values
745 'min' - shows min bounding box values
746 'auto' - rounds axis range to sensible values
751 """Returns current XSpec for axis"""
755 """Returns current YSpec for axis"""
758 def GetXMaxRange(self
):
759 """Returns (minX, maxX) x-axis range for displayed graph"""
760 graphics
= self
.last_draw
[0]
761 p1
, p2
= graphics
.boundingBox() # min, max points of graphics
762 xAxis
= self
._axisInterval
(self
._xSpec
, p1
[0], p2
[0]) # in user units
765 def GetYMaxRange(self
):
766 """Returns (minY, maxY) y-axis range for displayed graph"""
767 graphics
= self
.last_draw
[0]
768 p1
, p2
= graphics
.boundingBox() # min, max points of graphics
769 yAxis
= self
._axisInterval
(self
._ySpec
, p1
[1], p2
[1])
772 def GetXCurrentRange(self
):
773 """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
774 return self
.last_draw
[1]
776 def GetYCurrentRange(self
):
777 """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
778 return self
.last_draw
[2]
780 def Draw(self
, graphics
, xAxis
= None, yAxis
= None, dc
= None):
781 """Draw objects in graphics with specified x and y axis.
782 graphics- instance of PlotGraphics with list of PolyXXX objects
783 xAxis - tuple with (min, max) axis range to view
784 yAxis - same as xAxis
785 dc - drawing context - doesn't have to be specified.
786 If it's not, the offscreen buffer is used
788 # check Axis is either tuple or none
789 if type(xAxis
) not in [type(None),tuple]:
790 raise TypeError, "xAxis should be None or (minX,maxX)"
791 if type(yAxis
) not in [type(None),tuple]:
792 raise TypeError, "yAxis should be None or (minY,maxY)"
794 # check case for axis = (a,b) where a==b caused by improper zooms
796 if xAxis
[0] == xAxis
[1]:
799 if yAxis
[0] == yAxis
[1]:
803 # sets new dc and clears it
804 dc
= wx
.BufferedDC(wx
.ClientDC(self
), self
._Buffer
)
810 # set font size for every thing but title and legend
811 dc
.SetFont(self
._getFont
(self
._fontSizeAxis
))
813 # sizes axis to axis type, create lower left and upper right corners of plot
814 if xAxis
== None or yAxis
== None:
815 # One or both axis not specified in Draw
816 p1
, p2
= graphics
.boundingBox() # min, max points of graphics
818 xAxis
= self
._axisInterval
(self
._xSpec
, p1
[0], p2
[0]) # in user units
820 yAxis
= self
._axisInterval
(self
._ySpec
, p1
[1], p2
[1])
821 # Adjust bounding box for axis spec
822 p1
[0],p1
[1] = xAxis
[0], yAxis
[0] # lower left corner user scale (xmin,ymin)
823 p2
[0],p2
[1] = xAxis
[1], yAxis
[1] # upper right corner user scale (xmax,ymax)
825 # Both axis specified in Draw
826 p1
= _Numeric
.array([xAxis
[0], yAxis
[0]]) # lower left corner user scale (xmin,ymin)
827 p2
= _Numeric
.array([xAxis
[1], yAxis
[1]]) # upper right corner user scale (xmax,ymax)
829 self
.last_draw
= (graphics
, xAxis
, yAxis
) # saves most recient values
831 # Get ticks and textExtents for axis if required
832 if self
._xSpec
is not 'none':
833 xticks
= self
._ticks
(xAxis
[0], xAxis
[1])
834 xTextExtent
= dc
.GetTextExtent(xticks
[-1][1])# w h of x axis text last number on axis
837 xTextExtent
= (0,0) # No text for ticks
838 if self
._ySpec
is not 'none':
839 yticks
= self
._ticks
(yAxis
[0], yAxis
[1])
840 yTextExtentBottom
= dc
.GetTextExtent(yticks
[0][1])
841 yTextExtentTop
= dc
.GetTextExtent(yticks
[-1][1])
842 yTextExtent
= (max(yTextExtentBottom
[0],yTextExtentTop
[0]),
843 max(yTextExtentBottom
[1],yTextExtentTop
[1]))
846 yTextExtent
= (0,0) # No text for ticks
848 # TextExtents for Title and Axis Labels
849 titleWH
, xLabelWH
, yLabelWH
= self
._titleLablesWH
(dc
, graphics
)
851 # TextExtents for Legend
852 legendBoxWH
, legendSymExt
, legendTextExt
= self
._legendWH
(dc
, graphics
)
854 # room around graph area
855 rhsW
= max(xTextExtent
[0], legendBoxWH
[0]) # use larger of number width or legend width
856 lhsW
= yTextExtent
[0]+ yLabelWH
[1]
857 bottomH
= max(xTextExtent
[1], yTextExtent
[1]/2.)+ xLabelWH
[1]
858 topH
= yTextExtent
[1]/2. + titleWH
[1]
859 textSize_scale
= _Numeric
.array([rhsW
+lhsW
,bottomH
+topH
]) # make plot area smaller by text size
860 textSize_shift
= _Numeric
.array([lhsW
, bottomH
]) # shift plot area by this amount
862 # drawing title and labels text
863 dc
.SetFont(self
._getFont
(self
._fontSizeTitle
))
864 titlePos
= (self
.plotbox_origin
[0]+ lhsW
+ (self
.plotbox_size
[0]-lhsW
-rhsW
)/2.- titleWH
[0]/2.,
865 self
.plotbox_origin
[1]- self
.plotbox_size
[1])
866 dc
.DrawText(graphics
.getTitle(),titlePos
[0],titlePos
[1])
867 dc
.SetFont(self
._getFont
(self
._fontSizeAxis
))
868 xLabelPos
= (self
.plotbox_origin
[0]+ lhsW
+ (self
.plotbox_size
[0]-lhsW
-rhsW
)/2.- xLabelWH
[0]/2.,
869 self
.plotbox_origin
[1]- xLabelWH
[1])
870 dc
.DrawText(graphics
.getXLabel(),xLabelPos
[0],xLabelPos
[1])
871 yLabelPos
= (self
.plotbox_origin
[0],
872 self
.plotbox_origin
[1]- bottomH
- (self
.plotbox_size
[1]-bottomH
-topH
)/2.+ yLabelWH
[0]/2.)
873 if graphics
.getYLabel(): # bug fix for Linux
874 dc
.DrawRotatedText(graphics
.getYLabel(),yLabelPos
[0],yLabelPos
[1],90)
876 # drawing legend makers and text
877 if self
._legendEnabled
:
878 self
._drawLegend
(dc
,graphics
,rhsW
,topH
,legendBoxWH
, legendSymExt
, legendTextExt
)
880 # allow for scaling and shifting plotted points
881 scale
= (self
.plotbox_size
-textSize_scale
) / (p2
-p1
)* _Numeric
.array((1,-1))
882 shift
= -p1
*scale
+ self
.plotbox_origin
+ textSize_shift
* _Numeric
.array((1,-1))
883 self
._pointScale
= scale
# make available for mouse events
884 self
._pointShift
= shift
885 self
._drawAxes
(dc
, p1
, p2
, scale
, shift
, xticks
, yticks
)
887 graphics
.scaleAndShift(scale
, shift
)
888 graphics
.setPrinterScale(self
.printerScale
) # thicken up lines and markers if printing
890 # set clipping area so drawing does not occur outside axis box
891 ptx
,pty
,rectWidth
,rectHeight
= self
._point
2ClientCoord
(p1
, p2
)
892 dc
.SetClippingRegion(ptx
,pty
,rectWidth
,rectHeight
)
893 # Draw the lines and markers
894 #start = _time.clock()
896 # print "entire graphics drawing took: %f second"%(_time.clock() - start)
897 # remove the clipping region
898 dc
.DestroyClippingRegion()
901 def Redraw(self
, dc
= None):
902 """Redraw the existing plot."""
903 if self
.last_draw
is not None:
904 graphics
, xAxis
, yAxis
= self
.last_draw
905 self
.Draw(graphics
,xAxis
,yAxis
,dc
)
908 """Erase the window."""
909 self
.last_PointLabel
= None #reset pointLabel
910 dc
= wx
.BufferedDC(wx
.ClientDC(self
), self
._Buffer
)
912 self
.last_draw
= None
914 def Zoom(self
, Center
, Ratio
):
916 Centers on the X,Y coords given in Center
917 Zooms by the Ratio = (Xratio, Yratio) given
919 self
.last_PointLabel
= None #reset maker
921 if self
.last_draw
!= None:
922 (graphics
, xAxis
, yAxis
) = self
.last_draw
923 w
= (xAxis
[1] - xAxis
[0]) * Ratio
[0]
924 h
= (yAxis
[1] - yAxis
[0]) * Ratio
[1]
925 xAxis
= ( x
- w
/2, x
+ w
/2 )
926 yAxis
= ( y
- h
/2, y
+ h
/2 )
927 self
.Draw(graphics
, xAxis
, yAxis
)
929 def GetClosestPoints(self
, pntXY
, pointScaled
= True):
931 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
933 Returns [] if no curves are being plotted.
936 if pointScaled == True based on screen coords
937 if pointScaled == False based on user coords
939 if self
.last_draw
== None:
942 graphics
, xAxis
, yAxis
= self
.last_draw
944 for curveNum
,obj
in enumerate(graphics
):
945 #check there are points in the curve
946 if len(obj
.points
) == 0:
947 continue #go to next obj
948 #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
949 cn
= [curveNum
]+ [obj
.getLegend()]+ obj
.getClosestPoint( pntXY
, pointScaled
)
953 def GetClosetPoint(self
, pntXY
, pointScaled
= True):
955 [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
956 list for only the closest curve.
957 Returns [] if no curves are being plotted.
960 if pointScaled == True based on screen coords
961 if pointScaled == False based on user coords
963 #closest points on screen based on screen scaling (pointScaled= True)
964 #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
965 closestPts
= self
.GetClosestPoints(pntXY
, pointScaled
)
967 return [] #no graph present
968 #find one with least distance
969 dists
= [c
[-1] for c
in closestPts
]
970 mdist
= min(dists
) #Min dist
971 i
= dists
.index(mdist
) #index for min dist
972 return closestPts
[i
] #this is the closest point on closest curve
974 def UpdatePointLabel(self
, mDataDict
):
975 """Updates the pointLabel point on screen with data contained in
978 mDataDict will be passed to your function set by
979 SetPointLabelFunc. It can contain anything you
980 want to display on the screen at the scaledXY point
983 This function can be called from parent window with onClick,
986 if self
.last_PointLabel
!= None:
988 if mDataDict
["pointXY"] != self
.last_PointLabel
["pointXY"]:
990 self
._drawPointLabel
(self
.last_PointLabel
) #erase old
991 self
._drawPointLabel
(mDataDict
) #plot new
993 #just plot new with no erase
994 self
._drawPointLabel
(mDataDict
) #plot new
996 self
.last_PointLabel
= mDataDict
998 # event handlers **********************************
999 def OnMotion(self
, event
):
1000 if self
._zoomEnabled
and event
.LeftIsDown():
1001 if self
._hasDragged
:
1002 self
._drawRubberBand
(self
._zoomCorner
1, self
._zoomCorner
2) # remove old
1004 self
._hasDragged
= True
1005 self
._zoomCorner
2[0], self
._zoomCorner
2[1] = self
.GetXY(event
)
1006 self
._drawRubberBand
(self
._zoomCorner
1, self
._zoomCorner
2) # add new
1008 def OnMouseLeftDown(self
,event
):
1009 self
._zoomCorner
1[0], self
._zoomCorner
1[1]= self
.GetXY(event
)
1011 def OnMouseLeftUp(self
, event
):
1012 if self
._zoomEnabled
:
1013 if self
._hasDragged
== True:
1014 self
._drawRubberBand
(self
._zoomCorner
1, self
._zoomCorner
2) # remove old
1015 self
._zoomCorner
2[0], self
._zoomCorner
2[1]= self
.GetXY(event
)
1016 self
._hasDragged
= False # reset flag
1017 minX
, minY
= _Numeric
.minimum( self
._zoomCorner
1, self
._zoomCorner
2)
1018 maxX
, maxY
= _Numeric
.maximum( self
._zoomCorner
1, self
._zoomCorner
2)
1019 self
.last_PointLabel
= None #reset pointLabel
1020 if self
.last_draw
!= None:
1021 self
.Draw(self
.last_draw
[0], xAxis
= (minX
,maxX
), yAxis
= (minY
,maxY
), dc
= None)
1022 #else: # A box has not been drawn, zoom in on a point
1023 ## this interfered with the double click, so I've disables it.
1024 # X,Y = self.GetXY(event)
1025 # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1027 def OnMouseDoubleClick(self
,event
):
1028 if self
._zoomEnabled
:
1031 def OnMouseRightDown(self
,event
):
1032 if self
._zoomEnabled
:
1033 X
,Y
= self
.GetXY(event
)
1034 self
.Zoom( (X
,Y
), (self
._zoomOutFactor
, self
._zoomOutFactor
) )
1036 def OnPaint(self
, event
):
1037 # All that is needed here is to draw the buffer to screen
1038 if self
.last_PointLabel
!= None:
1039 self
._drawPointLabel
(self
.last_PointLabel
) #erase old
1040 self
.last_PointLabel
= None
1041 dc
= wx
.BufferedPaintDC(self
, self
._Buffer
)
1043 def OnSize(self
,event
):
1044 # The Buffer init is done here, to make sure the buffer is always
1045 # the same size as the Window
1046 Size
= self
.GetClientSize()
1047 if Size
.width
<= 0 or Size
.height
<= 0:
1050 # Make new offscreen bitmap: this bitmap will always have the
1051 # current drawing in it, so it can be used to save the image to
1052 # a file, or whatever.
1053 self
._Buffer
= wx
.EmptyBitmap(Size
[0],Size
[1])
1056 self
.last_PointLabel
= None #reset pointLabel
1058 if self
.last_draw
is None:
1061 graphics
, xSpec
, ySpec
= self
.last_draw
1062 self
.Draw(graphics
,xSpec
,ySpec
)
1064 def OnLeave(self
, event
):
1065 """Used to erase pointLabel when mouse outside window"""
1066 if self
.last_PointLabel
!= None:
1067 self
._drawPointLabel
(self
.last_PointLabel
) #erase old
1068 self
.last_PointLabel
= None
1071 # Private Methods **************************************************
1072 def _setSize(self
, width
=None, height
=None):
1073 """DC width and height."""
1075 (self
.width
,self
.height
) = self
.GetClientSize()
1077 self
.width
, self
.height
= width
,height
1078 self
.plotbox_size
= 0.97*_Numeric
.array([self
.width
, self
.height
])
1079 xo
= 0.5*(self
.width
-self
.plotbox_size
[0])
1080 yo
= self
.height
-0.5*(self
.height
-self
.plotbox_size
[1])
1081 self
.plotbox_origin
= _Numeric
.array([xo
, yo
])
1083 def _setPrinterScale(self
, scale
):
1084 """Used to thicken lines and increase marker size for print out."""
1085 # line thickness on printer is very thin at 600 dot/in. Markers small
1086 self
.printerScale
= scale
1088 def _printDraw(self
, printDC
):
1089 """Used for printing."""
1090 if self
.last_draw
!= None:
1091 graphics
, xSpec
, ySpec
= self
.last_draw
1092 self
.Draw(graphics
,xSpec
,ySpec
,printDC
)
1094 def _drawPointLabel(self
, mDataDict
):
1095 """Draws and erases pointLabels"""
1096 width
= self
._Buffer
.GetWidth()
1097 height
= self
._Buffer
.GetHeight()
1098 tmp_Buffer
= wx
.EmptyBitmap(width
,height
)
1100 dcs
.SelectObject(tmp_Buffer
)
1103 self
._pointLabelFunc
(dcs
,mDataDict
) #custom user pointLabel function
1106 dc
= wx
.ClientDC( self
)
1107 #this will erase if called twice
1108 dc
.Blit(0, 0, width
, height
, dcs
, 0, 0, wx
.EQUIV
) #(NOT src) XOR dst
1111 def _drawLegend(self
,dc
,graphics
,rhsW
,topH
,legendBoxWH
, legendSymExt
, legendTextExt
):
1112 """Draws legend symbols and text"""
1113 # top right hand corner of graph box is ref corner
1114 trhc
= self
.plotbox_origin
+ (self
.plotbox_size
-[rhsW
,topH
])*[1,-1]
1115 legendLHS
= .091* legendBoxWH
[0] # border space between legend sym and graph box
1116 lineHeight
= max(legendSymExt
[1], legendTextExt
[1]) * 1.1 #1.1 used as space between lines
1117 dc
.SetFont(self
._getFont
(self
._fontSizeLegend
))
1118 for i
in range(len(graphics
)):
1121 if isinstance(o
,PolyMarker
):
1122 # draw marker with legend
1123 pnt
= (trhc
[0]+legendLHS
+legendSymExt
[0]/2., trhc
[1]+s
+lineHeight
/2.)
1124 o
.draw(dc
, self
.printerScale
, coord
= _Numeric
.array([pnt
]))
1125 elif isinstance(o
,PolyLine
):
1126 # draw line with legend
1127 pnt1
= (trhc
[0]+legendLHS
, trhc
[1]+s
+lineHeight
/2.)
1128 pnt2
= (trhc
[0]+legendLHS
+legendSymExt
[0], trhc
[1]+s
+lineHeight
/2.)
1129 o
.draw(dc
, self
.printerScale
, coord
= _Numeric
.array([pnt1
,pnt2
]))
1131 raise TypeError, "object is neither PolyMarker or PolyLine instance"
1133 pnt
= (trhc
[0]+legendLHS
+legendSymExt
[0], trhc
[1]+s
+lineHeight
/2.-legendTextExt
[1]/2)
1134 dc
.DrawText(o
.getLegend(),pnt
[0],pnt
[1])
1135 dc
.SetFont(self
._getFont
(self
._fontSizeAxis
)) # reset
1137 def _titleLablesWH(self
, dc
, graphics
):
1138 """Draws Title and labels and returns width and height for each"""
1139 # TextExtents for Title and Axis Labels
1140 dc
.SetFont(self
._getFont
(self
._fontSizeTitle
))
1141 title
= graphics
.getTitle()
1142 titleWH
= dc
.GetTextExtent(title
)
1143 dc
.SetFont(self
._getFont
(self
._fontSizeAxis
))
1144 xLabel
, yLabel
= graphics
.getXLabel(),graphics
.getYLabel()
1145 xLabelWH
= dc
.GetTextExtent(xLabel
)
1146 yLabelWH
= dc
.GetTextExtent(yLabel
)
1147 return titleWH
, xLabelWH
, yLabelWH
1149 def _legendWH(self
, dc
, graphics
):
1150 """Returns the size in screen units for legend box"""
1151 if self
._legendEnabled
!= True:
1152 legendBoxWH
= symExt
= txtExt
= (0,0)
1154 # find max symbol size
1155 symExt
= graphics
.getSymExtent(self
.printerScale
)
1156 # find max legend text extent
1157 dc
.SetFont(self
._getFont
(self
._fontSizeLegend
))
1158 txtList
= graphics
.getLegendNames()
1159 txtExt
= dc
.GetTextExtent(txtList
[0])
1160 for txt
in graphics
.getLegendNames()[1:]:
1161 txtExt
= _Numeric
.maximum(txtExt
,dc
.GetTextExtent(txt
))
1162 maxW
= symExt
[0]+txtExt
[0]
1163 maxH
= max(symExt
[1],txtExt
[1])
1164 # padding .1 for lhs of legend box and space between lines
1166 maxH
= maxH
* 1.1 * len(txtList
)
1167 dc
.SetFont(self
._getFont
(self
._fontSizeAxis
))
1168 legendBoxWH
= (maxW
,maxH
)
1169 return (legendBoxWH
, symExt
, txtExt
)
1171 def _drawRubberBand(self
, corner1
, corner2
):
1172 """Draws/erases rect box from corner1 to corner2"""
1173 ptx
,pty
,rectWidth
,rectHeight
= self
._point
2ClientCoord
(corner1
, corner2
)
1175 dc
= wx
.ClientDC( self
)
1177 dc
.SetPen(wx
.Pen(wx
.BLACK
))
1178 dc
.SetBrush(wx
.Brush( wx
.WHITE
, wx
.TRANSPARENT
) )
1179 dc
.SetLogicalFunction(wx
.INVERT
)
1180 dc
.DrawRectangle( ptx
,pty
, rectWidth
,rectHeight
)
1181 dc
.SetLogicalFunction(wx
.COPY
)
1184 def _getFont(self
,size
):
1185 """Take font size, adjusts if printing and returns wx.Font"""
1186 s
= size
*self
.printerScale
1188 # Linux speed up to get font from cache rather than X font server
1189 key
= (int(s
), of
.GetFamily (), of
.GetStyle (), of
.GetWeight ())
1190 font
= self
._fontCache
.get (key
, None)
1192 return font
# yeah! cache hit
1194 font
= wx
.Font(int(s
), of
.GetFamily(), of
.GetStyle(), of
.GetWeight())
1195 self
._fontCache
[key
] = font
1199 def _point2ClientCoord(self
, corner1
, corner2
):
1200 """Converts user point coords to client screen int coords x,y,width,height"""
1201 c1
= _Numeric
.array(corner1
)
1202 c2
= _Numeric
.array(corner2
)
1203 # convert to screen coords
1204 pt1
= c1
*self
._pointScale
+self
._pointShift
1205 pt2
= c2
*self
._pointScale
+self
._pointShift
1206 # make height and width positive
1207 pul
= _Numeric
.minimum(pt1
,pt2
) # Upper left corner
1208 plr
= _Numeric
.maximum(pt1
,pt2
) # Lower right corner
1209 rectWidth
, rectHeight
= plr
-pul
1211 return ptx
, pty
, rectWidth
, rectHeight
1213 def _axisInterval(self
, spec
, lower
, upper
):
1214 """Returns sensible axis range for given spec"""
1215 if spec
== 'none' or spec
== 'min':
1217 return lower
-0.5, upper
+0.5
1220 elif spec
== 'auto':
1223 return lower
-0.5, upper
+0.5
1224 log
= _Numeric
.log10(range)
1225 power
= _Numeric
.floor(log
)
1226 fraction
= log
-power
1227 if fraction
<= 0.05:
1230 lower
= lower
- lower
% grid
1233 upper
= upper
- mod
+ grid
1235 elif type(spec
) == type(()):
1242 raise ValueError, str(spec
) + ': illegal axis specification'
1244 def _drawAxes(self
, dc
, p1
, p2
, scale
, shift
, xticks
, yticks
):
1246 penWidth
= self
.printerScale
# increases thickness for printing only
1247 dc
.SetPen(wx
.Pen(self
._gridColour
, penWidth
))
1249 # set length of tick marks--long ones make grid
1250 if self
._gridEnabled
:
1251 x
,y
,width
,height
= self
._point
2ClientCoord
(p1
,p2
)
1252 if self
._gridEnabled
== 'Horizontal':
1253 yTickLength
= width
/2.0 +1
1254 xTickLength
= 3 * self
.printerScale
1255 elif self
._gridEnabled
== 'Vertical':
1256 yTickLength
= 3 * self
.printerScale
1257 xTickLength
= height
/2.0 +1
1259 yTickLength
= width
/2.0 +1
1260 xTickLength
= height
/2.0 +1
1262 yTickLength
= 3 * self
.printerScale
# lengthens lines for printing
1263 xTickLength
= 3 * self
.printerScale
1265 if self
._xSpec
is not 'none':
1266 lower
, upper
= p1
[0],p2
[0]
1268 for y
, d
in [(p1
[1], -xTickLength
), (p2
[1], xTickLength
)]: # miny, maxy and tick lengths
1269 a1
= scale
*_Numeric
.array([lower
, y
])+shift
1270 a2
= scale
*_Numeric
.array([upper
, y
])+shift
1271 dc
.DrawLine(a1
[0],a1
[1],a2
[0],a2
[1]) # draws upper and lower axis line
1272 for x
, label
in xticks
:
1273 pt
= scale
*_Numeric
.array([x
, y
])+shift
1274 dc
.DrawLine(pt
[0],pt
[1],pt
[0],pt
[1] + d
) # draws tick mark d units
1276 dc
.DrawText(label
,pt
[0],pt
[1])
1277 text
= 0 # axis values not drawn on top side
1279 if self
._ySpec
is not 'none':
1280 lower
, upper
= p1
[1],p2
[1]
1282 h
= dc
.GetCharHeight()
1283 for x
, d
in [(p1
[0], -yTickLength
), (p2
[0], yTickLength
)]:
1284 a1
= scale
*_Numeric
.array([x
, lower
])+shift
1285 a2
= scale
*_Numeric
.array([x
, upper
])+shift
1286 dc
.DrawLine(a1
[0],a1
[1],a2
[0],a2
[1])
1287 for y
, label
in yticks
:
1288 pt
= scale
*_Numeric
.array([x
, y
])+shift
1289 dc
.DrawLine(pt
[0],pt
[1],pt
[0]-d
,pt
[1])
1291 dc
.DrawText(label
,pt
[0]-dc
.GetTextExtent(label
)[0],
1293 text
= 0 # axis values not drawn on right side
1295 def _ticks(self
, lower
, upper
):
1296 ideal
= (upper
-lower
)/7.
1297 log
= _Numeric
.log10(ideal
)
1298 power
= _Numeric
.floor(log
)
1299 fraction
= log
-power
1302 for f
, lf
in self
._multiples
:
1303 e
= _Numeric
.fabs(fraction
-lf
)
1307 grid
= factor
* 10.**power
1308 if power
> 4 or power
< -4:
1311 digits
= max(1, int(power
))
1312 format
= '%' + `digits`
+'.0f'
1314 digits
= -int(power
)
1315 format
= '%'+`digits
+2`
+'.'+`digits`
+'f'
1317 t
= -grid
*_Numeric
.floor(-lower
/grid
)
1319 ticks
.append( (t
, format
% (t
,)) )
1323 _multiples
= [(2., _Numeric
.log10(2.)), (5., _Numeric
.log10(5.))]
1326 #-------------------------------------------------------------------------------
1327 # Used to layout the printer page
1329 class PlotPrintout(wx
.Printout
):
1330 """Controls how the plot is made in printing and previewing"""
1331 # Do not change method names in this class,
1332 # we have to override wx.Printout methods here!
1333 def __init__(self
, graph
):
1334 """graph is instance of plotCanvas to be printed or previewed"""
1335 wx
.Printout
.__init
__(self
)
1338 def HasPage(self
, page
):
1344 def GetPageInfo(self
):
1345 return (1, 1, 1, 1) # disable page numbers
1347 def OnPrintPage(self
, page
):
1348 dc
= self
.GetDC() # allows using floats for certain functions
1349 ## print "PPI Printer",self.GetPPIPrinter()
1350 ## print "PPI Screen", self.GetPPIScreen()
1351 ## print "DC GetSize", dc.GetSize()
1352 ## print "GetPageSizePixels", self.GetPageSizePixels()
1353 # Note PPIScreen does not give the correct number
1354 # Calulate everything for printer and then scale for preview
1355 PPIPrinter
= self
.GetPPIPrinter() # printer dots/inch (w,h)
1356 #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
1357 dcSize
= dc
.GetSize() # DC size
1358 pageSize
= self
.GetPageSizePixels() # page size in terms of pixcels
1359 clientDcSize
= self
.graph
.GetClientSize()
1361 # find what the margins are (mm)
1362 margLeftSize
,margTopSize
= self
.graph
.pageSetupData
.GetMarginTopLeft()
1363 margRightSize
, margBottomSize
= self
.graph
.pageSetupData
.GetMarginBottomRight()
1365 # calculate offset and scale for dc
1366 pixLeft
= margLeftSize
*PPIPrinter
[0]/25.4 # mm*(dots/in)/(mm/in)
1367 pixRight
= margRightSize
*PPIPrinter
[0]/25.4
1368 pixTop
= margTopSize
*PPIPrinter
[1]/25.4
1369 pixBottom
= margBottomSize
*PPIPrinter
[1]/25.4
1371 plotAreaW
= pageSize
[0]-(pixLeft
+pixRight
)
1372 plotAreaH
= pageSize
[1]-(pixTop
+pixBottom
)
1374 # ratio offset and scale to screen size if preview
1375 if self
.IsPreview():
1376 ratioW
= float(dcSize
[0])/pageSize
[0]
1377 ratioH
= float(dcSize
[1])/pageSize
[1]
1383 # rescale plot to page or preview plot area
1384 self
.graph
._setSize
(plotAreaW
,plotAreaH
)
1386 # Set offset and scale
1387 dc
.SetDeviceOrigin(pixLeft
,pixTop
)
1389 # Thicken up pens and increase marker size for printing
1390 ratioW
= float(plotAreaW
)/clientDcSize
[0]
1391 ratioH
= float(plotAreaH
)/clientDcSize
[1]
1392 aveScale
= (ratioW
+ratioH
)/2
1393 self
.graph
._setPrinterScale
(aveScale
) # tickens up pens for printing
1395 self
.graph
._printDraw
(dc
)
1396 # rescale back to original
1397 self
.graph
._setSize
()
1398 self
.graph
._setPrinterScale
(1)
1399 self
.graph
.Redraw() #to get point label scale and shift correct
1406 #---------------------------------------------------------------------------
1407 # if running standalone...
1409 # ...a sample implementation using the above
1412 def _draw1Objects():
1413 # 100 points sin function, plotted as green circles
1414 data1
= 2.*_Numeric
.pi
*_Numeric
.arange(200)/200.
1415 data1
.shape
= (100, 2)
1416 data1
[:,1] = _Numeric
.sin(data1
[:,0])
1417 markers1
= PolyMarker(data1
, legend
='Green Markers', colour
='green', marker
='circle',size
=1)
1419 # 50 points cos function, plotted as red line
1420 data1
= 2.*_Numeric
.pi
*_Numeric
.arange(100)/100.
1421 data1
.shape
= (50,2)
1422 data1
[:,1] = _Numeric
.cos(data1
[:,0])
1423 lines
= PolyLine(data1
, legend
= 'Red Line', colour
='red')
1425 # A few more points...
1427 markers2
= PolyMarker([(0., 0.), (pi
/4., 1.), (pi
/2, 0.),
1428 (3.*pi
/4., -1)], legend
='Cross Legend', colour
='blue',
1431 return PlotGraphics([markers1
, lines
, markers2
],"Graph Title", "X Axis", "Y Axis")
1433 def _draw2Objects():
1434 # 100 points sin function, plotted as green dots
1435 data1
= 2.*_Numeric
.pi
*_Numeric
.arange(200)/200.
1436 data1
.shape
= (100, 2)
1437 data1
[:,1] = _Numeric
.sin(data1
[:,0])
1438 line1
= PolyLine(data1
, legend
='Green Line', colour
='green', width
=6, style
=wx
.DOT
)
1440 # 50 points cos function, plotted as red dot-dash
1441 data1
= 2.*_Numeric
.pi
*_Numeric
.arange(100)/100.
1442 data1
.shape
= (50,2)
1443 data1
[:,1] = _Numeric
.cos(data1
[:,0])
1444 line2
= PolyLine(data1
, legend
='Red Line', colour
='red', width
=3, style
= wx
.DOT_DASH
)
1446 # A few more points...
1448 markers1
= PolyMarker([(0., 0.), (pi
/4., 1.), (pi
/2, 0.),
1449 (3.*pi
/4., -1)], legend
='Cross Hatch Square', colour
='blue', width
= 3, size
= 6,
1450 fillcolour
= 'red', fillstyle
= wx
.CROSSDIAG_HATCH
,
1453 return PlotGraphics([markers1
, line1
, line2
], "Big Markers with Different Line Styles")
1455 def _draw3Objects():
1456 markerList
= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
1457 'cross', 'plus', 'circle']
1459 for i
in range(len(markerList
)):
1460 m
.append(PolyMarker([(2*i
+.5,i
+.5)], legend
=markerList
[i
], colour
='blue',
1461 marker
=markerList
[i
]))
1462 return PlotGraphics(m
, "Selection of Markers", "Minimal Axis", "No Axis")
1464 def _draw4Objects():
1466 data1
= _Numeric
.arange(5e5
,1e6
,10)
1467 data1
.shape
= (25000, 2)
1468 line1
= PolyLine(data1
, legend
='Wide Line', colour
='green', width
=5)
1470 # A few more points...
1471 markers2
= PolyMarker(data1
, legend
='Square', colour
='blue',
1473 return PlotGraphics([line1
, markers2
], "25,000 Points", "Value X", "")
1475 def _draw5Objects():
1476 # Empty graph with axis defined but no points/lines
1478 line1
= PolyLine(points
, legend
='Wide Line', colour
='green', width
=5)
1479 return PlotGraphics([line1
], "Empty Plot With Just Axes", "Value X", "Value Y")
1481 def _draw6Objects():
1483 points1
=[(1,0), (1,10)]
1484 line1
= PolyLine(points1
, colour
='green', legend
='Feb.', width
=10)
1485 points1g
=[(2,0), (2,4)]
1486 line1g
= PolyLine(points1g
, colour
='red', legend
='Mar.', width
=10)
1487 points1b
=[(3,0), (3,6)]
1488 line1b
= PolyLine(points1b
, colour
='blue', legend
='Apr.', width
=10)
1490 points2
=[(4,0), (4,12)]
1491 line2
= PolyLine(points2
, colour
='Yellow', legend
='May', width
=10)
1492 points2g
=[(5,0), (5,8)]
1493 line2g
= PolyLine(points2g
, colour
='orange', legend
='June', width
=10)
1494 points2b
=[(6,0), (6,4)]
1495 line2b
= PolyLine(points2b
, colour
='brown', legend
='July', width
=10)
1497 return PlotGraphics([line1
, line1g
, line1b
, line2
, line2g
, line2b
],
1498 "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
1501 class TestFrame(wx
.Frame
):
1502 def __init__(self
, parent
, id, title
):
1503 wx
.Frame
.__init
__(self
, parent
, id, title
,
1504 wx
.DefaultPosition
, (600, 400))
1506 # Now Create the menu bar and items
1507 self
.mainmenu
= wx
.MenuBar()
1510 menu
.Append(200, 'Page Setup...', 'Setup the printer page')
1511 self
.Bind(wx
.EVT_MENU
, self
.OnFilePageSetup
, id=200)
1513 menu
.Append(201, 'Print Preview...', 'Show the current plot on page')
1514 self
.Bind(wx
.EVT_MENU
, self
.OnFilePrintPreview
, id=201)
1516 menu
.Append(202, 'Print...', 'Print the current plot')
1517 self
.Bind(wx
.EVT_MENU
, self
.OnFilePrint
, id=202)
1519 menu
.Append(203, 'Save Plot...', 'Save current plot')
1520 self
.Bind(wx
.EVT_MENU
, self
.OnSaveFile
, id=203)
1522 menu
.Append(205, 'E&xit', 'Enough of this already!')
1523 self
.Bind(wx
.EVT_MENU
, self
.OnFileExit
, id=205)
1524 self
.mainmenu
.Append(menu
, '&File')
1527 menu
.Append(206, 'Draw1', 'Draw plots1')
1528 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw1
, id=206)
1529 menu
.Append(207, 'Draw2', 'Draw plots2')
1530 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw2
, id=207)
1531 menu
.Append(208, 'Draw3', 'Draw plots3')
1532 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw3
, id=208)
1533 menu
.Append(209, 'Draw4', 'Draw plots4')
1534 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw4
, id=209)
1535 menu
.Append(210, 'Draw5', 'Draw plots5')
1536 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw5
, id=210)
1537 menu
.Append(260, 'Draw6', 'Draw plots6')
1538 self
.Bind(wx
.EVT_MENU
,self
.OnPlotDraw6
, id=260)
1541 menu
.Append(211, '&Redraw', 'Redraw plots')
1542 self
.Bind(wx
.EVT_MENU
,self
.OnPlotRedraw
, id=211)
1543 menu
.Append(212, '&Clear', 'Clear canvas')
1544 self
.Bind(wx
.EVT_MENU
,self
.OnPlotClear
, id=212)
1545 menu
.Append(213, '&Scale', 'Scale canvas')
1546 self
.Bind(wx
.EVT_MENU
,self
.OnPlotScale
, id=213)
1547 menu
.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind
=wx
.ITEM_CHECK
)
1548 self
.Bind(wx
.EVT_MENU
,self
.OnEnableZoom
, id=214)
1549 menu
.Append(215, 'Enable &Grid', 'Turn on Grid', kind
=wx
.ITEM_CHECK
)
1550 self
.Bind(wx
.EVT_MENU
,self
.OnEnableGrid
, id=215)
1551 menu
.Append(220, 'Enable &Legend', 'Turn on Legend', kind
=wx
.ITEM_CHECK
)
1552 self
.Bind(wx
.EVT_MENU
,self
.OnEnableLegend
, id=220)
1553 menu
.Append(222, 'Enable &Point Label', 'Show Closest Point', kind
=wx
.ITEM_CHECK
)
1554 self
.Bind(wx
.EVT_MENU
,self
.OnEnablePointLabel
, id=222)
1556 menu
.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
1557 self
.Bind(wx
.EVT_MENU
,self
.OnScrUp
, id=225)
1558 menu
.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
1559 self
.Bind(wx
.EVT_MENU
,self
.OnScrRt
, id=230)
1560 menu
.Append(235, '&Plot Reset', 'Reset to original plot')
1561 self
.Bind(wx
.EVT_MENU
,self
.OnReset
, id=235)
1563 self
.mainmenu
.Append(menu
, '&Plot')
1566 menu
.Append(300, '&About', 'About this thing...')
1567 self
.Bind(wx
.EVT_MENU
, self
.OnHelpAbout
, id=300)
1568 self
.mainmenu
.Append(menu
, '&Help')
1570 self
.SetMenuBar(self
.mainmenu
)
1572 # A status bar to tell people what's happening
1573 self
.CreateStatusBar(1)
1575 self
.client
= PlotCanvas(self
)
1576 #define the function for drawing pointLabels
1577 self
.client
.SetPointLabelFunc(self
.DrawPointLabel
)
1578 # Create mouse event for showing cursor coords in status bar
1579 self
.client
.Bind(wx
.EVT_LEFT_DOWN
, self
.OnMouseLeftDown
)
1580 # Show closest point when enabled
1581 self
.client
.Bind(wx
.EVT_MOTION
, self
.OnMotion
)
1585 def DrawPointLabel(self
, dc
, mDataDict
):
1586 """This is the fuction that defines how the pointLabels are plotted
1587 dc - DC that will be passed
1588 mDataDict - Dictionary of data that you want to use for the pointLabel
1590 As an example I have decided I want a box at the curve point
1591 with some text information about the curve plotted below.
1592 Any wxDC method can be used.
1595 dc
.SetPen(wx
.Pen(wx
.BLACK
))
1596 dc
.SetBrush(wx
.Brush( wx
.BLACK
, wx
.SOLID
) )
1598 sx
, sy
= mDataDict
["scaledXY"] #scaled x,y of closest point
1599 dc
.DrawRectangle( sx
-5,sy
-5, 10, 10) #10by10 square centered on point
1600 px
,py
= mDataDict
["pointXY"]
1601 cNum
= mDataDict
["curveNum"]
1602 pntIn
= mDataDict
["pIndex"]
1603 legend
= mDataDict
["legend"]
1604 #make a string to display
1605 s
= "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum
, legend
, px
, py
, pntIn
)
1606 dc
.DrawText(s
, sx
, sy
+1)
1609 def OnMouseLeftDown(self
,event
):
1610 s
= "Left Mouse Down at Point: (%.4f, %.4f)" % self
.client
.GetXY(event
)
1611 self
.SetStatusText(s
)
1612 event
.Skip() #allows plotCanvas OnMouseLeftDown to be called
1614 def OnMotion(self
, event
):
1615 #show closest point (when enbled)
1616 if self
.client
.GetEnablePointLabel() == True:
1617 #make up dict with info for the pointLabel
1618 #I've decided to mark the closest point on the closest curve
1619 dlst
= self
.client
.GetClosetPoint( self
.client
.GetXY(event
), pointScaled
= True)
1620 if dlst
!= []: #returns [] if none
1621 curveNum
, legend
, pIndex
, pointXY
, scaledXY
, distance
= dlst
1622 #make up dictionary to pass to my user function (see DrawPointLabel)
1623 mDataDict
= {"curveNum":curveNum
, "legend":legend
, "pIndex":pIndex
,\
1624 "pointXY":pointXY
, "scaledXY":scaledXY
}
1625 #pass dict to update the pointLabel
1626 self
.client
.UpdatePointLabel(mDataDict
)
1627 event
.Skip() #go to next handler
1629 def OnFilePageSetup(self
, event
):
1630 self
.client
.PageSetup()
1632 def OnFilePrintPreview(self
, event
):
1633 self
.client
.PrintPreview()
1635 def OnFilePrint(self
, event
):
1636 self
.client
.Printout()
1638 def OnSaveFile(self
, event
):
1639 self
.client
.SaveFile()
1641 def OnFileExit(self
, event
):
1644 def OnPlotDraw1(self
, event
):
1645 self
.resetDefaults()
1646 self
.client
.Draw(_draw1Objects())
1648 def OnPlotDraw2(self
, event
):
1649 self
.resetDefaults()
1650 self
.client
.Draw(_draw2Objects())
1652 def OnPlotDraw3(self
, event
):
1653 self
.resetDefaults()
1654 self
.client
.SetFont(wx
.Font(10,wx
.SCRIPT
,wx
.NORMAL
,wx
.NORMAL
))
1655 self
.client
.SetFontSizeAxis(20)
1656 self
.client
.SetFontSizeLegend(12)
1657 self
.client
.SetXSpec('min')
1658 self
.client
.SetYSpec('none')
1659 self
.client
.Draw(_draw3Objects())
1661 def OnPlotDraw4(self
, event
):
1662 self
.resetDefaults()
1663 drawObj
= _draw4Objects()
1664 self
.client
.Draw(drawObj
)
1666 ## start = _time.clock()
1667 ## for x in range(10):
1668 ## self.client.Draw(drawObj)
1669 ## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
1672 def OnPlotDraw5(self
, event
):
1673 # Empty plot with just axes
1674 self
.resetDefaults()
1675 drawObj
= _draw5Objects()
1676 # make the axis X= (0,5), Y=(0,10)
1677 # (default with None is X= (-1,1), Y= (-1,1))
1678 self
.client
.Draw(drawObj
, xAxis
= (0,5), yAxis
= (0,10))
1680 def OnPlotDraw6(self
, event
):
1682 self
.resetDefaults()
1683 #self.client.SetEnableLegend(True) #turn on Legend
1684 #self.client.SetEnableGrid(True) #turn on Grid
1685 self
.client
.SetXSpec('none') #turns off x-axis scale
1686 self
.client
.SetYSpec('auto')
1687 self
.client
.Draw(_draw6Objects(), xAxis
= (0,7))
1689 def OnPlotRedraw(self
,event
):
1690 self
.client
.Redraw()
1692 def OnPlotClear(self
,event
):
1695 def OnPlotScale(self
, event
):
1696 if self
.client
.last_draw
!= None:
1697 graphics
, xAxis
, yAxis
= self
.client
.last_draw
1698 self
.client
.Draw(graphics
,(1,3.05),(0,1))
1700 def OnEnableZoom(self
, event
):
1701 self
.client
.SetEnableZoom(event
.IsChecked())
1703 def OnEnableGrid(self
, event
):
1704 self
.client
.SetEnableGrid(event
.IsChecked())
1706 def OnEnableLegend(self
, event
):
1707 self
.client
.SetEnableLegend(event
.IsChecked())
1709 def OnEnablePointLabel(self
, event
):
1710 self
.client
.SetEnablePointLabel(event
.IsChecked())
1712 def OnScrUp(self
, event
):
1713 self
.client
.ScrollUp(1)
1715 def OnScrRt(self
,event
):
1716 self
.client
.ScrollRight(2)
1718 def OnReset(self
,event
):
1721 def OnHelpAbout(self
, event
):
1722 from wx
.lib
.dialogs
import ScrolledMessageDialog
1723 about
= ScrolledMessageDialog(self
, __doc__
, "About...")
1726 def resetDefaults(self
):
1727 """Just to reset the fonts back to the PlotCanvas defaults"""
1728 self
.client
.SetFont(wx
.Font(10,wx
.SWISS
,wx
.NORMAL
,wx
.NORMAL
))
1729 self
.client
.SetFontSizeAxis(10)
1730 self
.client
.SetFontSizeLegend(7)
1731 self
.client
.SetXSpec('auto')
1732 self
.client
.SetYSpec('auto')
1737 class MyApp(wx
.App
):
1739 wx
.InitAllImageHandlers()
1740 frame
= TestFrame(None, -1, "PlotCanvas")
1742 self
.SetTopWindow(frame
)
1749 if __name__
== '__main__':