Ticket #16924: plot.py

File plot.py, 97.7 KB (added by masoud, 5 years ago)

correction in the plot.py

Line 
1#-----------------------------------------------------------------------------
2# Name:        wx.lib.plot.py
3# Purpose:     Line, Bar and Scatter Graphs
4#
5# Author:      Gordon Williams
6#
7# Created:     2003/11/03
8# RCS-ID:      $Id$
9# Copyright:   (c) 2002
10# Licence:     Use as you wish.
11#-----------------------------------------------------------------------------
12# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13#
14# o 2.5 compatability update.
15# o Renamed to plot.py in the wx.lib directory.
16# o Reworked test frame to work with wx demo framework. This saves a bit
17#   of tedious cut and paste, and the test app is excellent.
18#
19# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
20#
21# o wxScrolledMessageDialog -> ScrolledMessageDialog
22#
23# Oct 6, 2004  Gordon Williams (g_will@cyberus.ca)
24#   - Added bar graph demo
25#   - Modified line end shape from round to square.
26#   - Removed FloatDCWrapper for conversion to ints and ints in arguments
27#
28# Oct 15, 2004  Gordon Williams (g_will@cyberus.ca)
29#   - Imported modules given leading underscore to name.
30#   - Added Cursor Line Tracking and User Point Labels.
31#   - Demo for Cursor Line Tracking and Point Labels.
32#   - Size of plot preview frame adjusted to show page better.
33#   - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
34#   - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
35#       can be in either user coords or screen coords.
36#
37# Jun 22, 2009  Florian Hoech (florian.hoech@gmx.de)
38#   - Fixed exception when drawing empty plots on Mac OS X
39#   - Fixed exception when trying to draw point labels on Mac OS X (Mac OS X
40#     point label drawing code is still slow and only supports wx.COPY)
41#   - Moved label positions away from axis lines a bit
42#   - Added PolySpline class and modified demo 1 and 2 to use it
43#   - Added center and diagonal lines option (Set/GetEnableCenterLines,
44#     Set/GetEnableDiagonals)
45#   - Added anti-aliasing option with optional high-resolution mode
46#     (Set/GetEnableAntiAliasing, Set/GetEnableHiRes) and demo
47#   - Added option to specify exact number of tick marks to use for each axis
48#     (SetXSpec(<number>, SetYSpec(<number>) -- work like 'min', but with
49#     <number> tick marks)
50#   - Added support for background and foreground colours (enabled via
51#     SetBackgroundColour/SetForegroundColour on a PlotCanvas instance)
52#   - Changed PlotCanvas printing initialization from occuring in __init__ to
53#     occur on access. This will postpone any IPP and / or CUPS warnings
54#     which appear on stderr on some Linux systems until printing functionality
55#     is actually used.
56#
57#
58
59"""
60This is a simple light weight plotting module that can be used with
61Boa or easily integrated into your own wxPython application.  The
62emphasis is on small size and fast plotting for large data sets.  It
63has a reasonable number of features to do line and scatter graphs
64easily as well as simple bar graphs.  It is not as sophisticated or
65as powerful as SciPy Plt or Chaco.  Both of these are great packages
66but consume huge amounts of computer resources for simple plots.
67They can be found at http://scipy.com
68
69This file contains two parts; first the re-usable library stuff, then,
70after a "if __name__=='__main__'" test, a simple frame and a few default
71plots for examples and testing.
72
73Based on wxPlotCanvas
74Written by K.Hinsen, R. Srinivasan;
75Ported to wxPython Harm van der Heijden, feb 1999
76
77Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
78    -More style options
79    -Zooming using mouse "rubber band"
80    -Scroll left, right
81    -Grid(graticule)
82    -Printing, preview, and page set up (margins)
83    -Axis and title labels
84    -Cursor xy axis values
85    -Doc strings and lots of comments
86    -Optimizations for large number of points
87    -Legends
88
89Did a lot of work here to speed markers up. Only a factor of 4
90improvement though. Lines are much faster than markers, especially
91filled markers.  Stay away from circles and triangles unless you
92only have a few thousand points.
93
94Times for 25,000 points
95Line - 0.078 sec
96Markers
97Square -                   0.22 sec
98dot -                      0.10
99circle -                   0.87
100cross,plus -               0.28
101triangle, triangle_down -  0.90
102
103Thanks to Chris Barker for getting this version working on Linux.
104
105Zooming controls with mouse (when enabled):
106    Left mouse drag - Zoom box.
107    Left mouse double click - reset zoom.
108    Right mouse click - zoom out centred on click location.
109"""
110
111import string as _string
112import time as _time
113import sys
114import wx
115
116# Needs NumPy
117try:
118    import numpy as np
119except:
120    msg = """
121    This module requires the NumPy module, which could not be
122    imported.  It probably is not installed (it's not part of the
123    standard Python distribution). See the Numeric Python site
124    (http://numpy.scipy.org) for information on downloading source or
125    binaries."""
126    raise ImportError("NumPy not found.\n" + msg)
127
128
129#
130# Plotting classes...
131#
132class PolyPoints:
133
134    """Base Class for lines and markers
135        - All methods are private.
136    """
137
138    def __init__(self, points, attr):
139        self._points = np.array(points).astype(np.float64)
140        self._logscale = (False, False)
141        self._pointSize = (1.0, 1.0)
142        self.currentScale = (1, 1)
143        self.currentShift = (0, 0)
144        self.scaled = self.points
145        self.attributes = {}
146        self.attributes.update(self._attributes)
147        for name, value in attr.items():
148            if name not in self._attributes.keys():
149                raise KeyError(
150                    "Style attribute incorrect. Should be one of %s" % self._attributes.keys())
151            self.attributes[name] = value
152
153    def setLogScale(self, logscale):
154        self._logscale = logscale
155
156    def __getattr__(self, name):
157        if name == 'points':
158            if len(self._points) > 0:
159                data = np.array(self._points, copy=True)
160                if self._logscale[0]:
161                    data = self.log10(data, 0)
162                if self._logscale[1]:
163                    data = self.log10(data, 1)
164                return data
165            else:
166                return self._points
167        else:
168            raise AttributeError(name)
169
170    def log10(self, data, ind):
171        data = np.compress(data[:, ind] > 0, data, 0)
172        data[:, ind] = np.log10(data[:, ind])
173        return data
174
175    def boundingBox(self):
176        if len(self.points) == 0:
177            # no curves to draw
178            # defaults to (-1,-1) and (1,1) but axis can be set in Draw
179            minXY = np.array([-1.0, -1.0])
180            maxXY = np.array([1.0, 1.0])
181        else:
182            minXY = np.minimum.reduce(self.points)
183            maxXY = np.maximum.reduce(self.points)
184        return minXY, maxXY
185
186    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
187        if len(self.points) == 0:
188            # no curves to draw
189            return
190        if (scale is not self.currentScale) or (shift is not self.currentShift):
191            # update point scaling
192            self.scaled = scale * self.points + shift
193            self.currentScale = scale
194            self.currentShift = shift
195        # else unchanged use the current scaling
196
197    def getLegend(self):
198        return self.attributes['legend']
199
200    def getClosestPoint(self, pntXY, pointScaled=True):
201        """Returns the index of closest point on the curve, pointXY, scaledXY, distance
202            x, y in user coords
203            if pointScaled == True based on screen coords
204            if pointScaled == False based on user coords
205        """
206        if pointScaled == True:
207            # Using screen coords
208            p = self.scaled
209            pxy = self.currentScale * np.array(pntXY) + self.currentShift
210        else:
211            # Using user coords
212            p = self.points
213            pxy = np.array(pntXY)
214        # determine distance for each point
215        d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1))  # sqrt(dx^2+dy^2)
216        pntIndex = np.argmin(d)
217        dist = d[pntIndex]
218        return [pntIndex, self.points[pntIndex], self.scaled[pntIndex] / self._pointSize, dist]
219
220
221class PolyLine(PolyPoints):
222
223    """Class to define line type and style
224        - All methods except __init__ are private.
225    """
226
227    _attributes = {'colour': 'black',
228                   'width': 1,
229                   'style': wx.PENSTYLE_SOLID,
230                   'legend': ''}
231
232    def __init__(self, points, **attr):
233        """
234        Creates PolyLine object
235
236        :param `points`: sequence (array, tuple or list) of (x,y) points making up line
237        :keyword `attr`: keyword attributes, default to:
238
239         ==========================  ================================
240         'colour'= 'black'           wx.Pen Colour any wx.Colour
241         'width'= 1                  Pen width
242         'style'= wx.PENSTYLE_SOLID  wx.Pen style
243         'legend'= ''                Line Legend to display
244         ==========================  ================================
245
246        """
247        PolyPoints.__init__(self, points, attr)
248
249    def draw(self, dc, printerScale, coord=None):
250        colour = self.attributes['colour']
251        width = self.attributes['width'] * printerScale * self._pointSize[0]
252        style = self.attributes['style']
253        if not isinstance(colour, wx.Colour):
254            colour = wx.NamedColour(colour)
255        pen = wx.Pen(colour, width, style)
256        pen.SetCap(wx.CAP_BUTT)
257        dc.SetPen(pen)
258        if coord == None:
259            if len(self.scaled):  # bugfix for Mac OS X
260                dc.DrawLines(self.scaled)
261        else:
262            dc.DrawLines(coord)  # draw legend line
263
264    def getSymExtent(self, printerScale):
265        """Width and Height of Marker"""
266        h = self.attributes['width'] * printerScale * self._pointSize[0]
267        w = 5 * h
268        return (w, h)
269
270
271class PolySpline(PolyLine):
272
273    """Class to define line type and style
274        - All methods except __init__ are private.
275    """
276
277    _attributes = {'colour': 'black',
278                   'width': 1,
279                   'style': wx.PENSTYLE_SOLID,
280                   'legend': ''}
281
282    def __init__(self, points, **attr):
283        """
284        Creates PolyLine object
285
286        :param `points`: sequence (array, tuple or list) of (x,y) points making up spline
287        :keyword `attr`: keyword attributes, default to:
288
289         ==========================  ================================
290         'colour'= 'black'           wx.Pen Colour any wx.Colour
291         'width'= 1                  Pen width
292         'style'= wx.PENSTYLE_SOLID  wx.Pen style
293         'legend'= ''                Line Legend to display
294         ==========================  ================================
295
296        """
297        PolyLine.__init__(self, points, **attr)
298
299    def draw(self, dc, printerScale, coord=None):
300        colour = self.attributes['colour']
301        width = self.attributes['width'] * printerScale * self._pointSize[0]
302        style = self.attributes['style']
303        if not isinstance(colour, wx.Colour):
304            colour = wx.NamedColour(colour)
305        pen = wx.Pen(colour, width, style)
306        pen.SetCap(wx.CAP_ROUND)
307        dc.SetPen(pen)
308        if coord == None:
309            if len(self.scaled):  # bugfix for Mac OS X
310                dc.DrawSpline(self.scaled)
311        else:
312            dc.DrawLines(coord)  # draw legend line
313
314
315class PolyMarker(PolyPoints):
316
317    """Class to define marker type and style
318        - All methods except __init__ are private.
319    """
320
321    _attributes = {'colour': 'black',
322                   'width': 1,
323                   'size': 2,
324                   'fillcolour': None,
325                   'fillstyle': wx.BRUSHSTYLE_SOLID,
326                   'marker': 'circle',
327                   'legend': ''}
328
329    def __init__(self, points, **attr):
330        """
331        Creates PolyMarker object
332
333        :param `points`: sequence (array, tuple or list) of (x,y) points
334        :keyword `attr`: keyword attributes, default to:
335
336         ================================ ================================
337         'colour'= 'black'                wx.Pen Colour any wx.Colour
338         'width'= 1                       Pen width
339         'size'= 2                        Marker size
340         'fillcolour'= same as colour     wx.Brush Colour any wx.Colour
341         'fillstyle'= wx.BRUSHSTYLE_SOLID wx.Brush fill style (use wx.BRUSHSTYLE_TRANSPARENT for no fill)
342         'style'= wx.FONTFAMILY_SOLID     wx.Pen style
343         'marker'= 'circle'               Marker shape
344         'legend'= ''                     Line Legend to display
345         ================================ ================================
346
347         Marker Shapes:
348         - 'circle'
349         - 'dot'
350         - 'square'
351         - 'triangle'
352         - 'triangle_down'
353         - 'cross'
354         - 'plus'
355        """
356
357        PolyPoints.__init__(self, points, attr)
358
359    def draw(self, dc, printerScale, coord=None):
360        colour = self.attributes['colour']
361        width = self.attributes['width'] * printerScale * self._pointSize[0]
362        size = self.attributes['size'] * printerScale * self._pointSize[0]
363        fillcolour = self.attributes['fillcolour']
364        fillstyle = self.attributes['fillstyle']
365        marker = self.attributes['marker']
366
367        if colour and not isinstance(colour, wx.Colour):
368            colour = wx.NamedColour(colour)
369        if fillcolour and not isinstance(fillcolour, wx.Colour):
370            fillcolour = wx.NamedColour(fillcolour)
371
372        dc.SetPen(wx.Pen(colour, width))
373        if fillcolour:
374            dc.SetBrush(wx.Brush(fillcolour, fillstyle))
375        else:
376            dc.SetBrush(wx.Brush(colour, fillstyle))
377        if coord == None:
378            if len(self.scaled):  # bugfix for Mac OS X
379                self._drawmarkers(dc, self.scaled, marker, size)
380        else:
381            self._drawmarkers(dc, coord, marker, size)  # draw legend marker
382
383    def getSymExtent(self, printerScale):
384        """Width and Height of Marker"""
385        s = 5 * self.attributes['size'] * printerScale * self._pointSize[0]
386        return (s, s)
387
388    def _drawmarkers(self, dc, coords, marker, size=1):
389        f = eval('self._' + marker)
390        f(dc, coords, size)
391
392    def _circle(self, dc, coords, size=1):
393        fact = 2.5 * size
394        wh = 5.0 * size
395        rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh]
396        rect[:, 0:2] = coords - [fact, fact]
397        dc.DrawEllipseList(rect.astype(np.int32))
398
399    def _dot(self, dc, coords, size=1):
400        dc.DrawPointList(coords)
401
402    def _square(self, dc, coords, size=1):
403        fact = 2.5 * size
404        wh = 5.0 * size
405        rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh]
406        rect[:, 0:2] = coords - [fact, fact]
407        dc.DrawRectangleList(rect.astype(np.int32))
408
409    def _triangle(self, dc, coords, size=1):
410        shape = [(-2.5 * size, 1.44 * size),
411                 (2.5 * size, 1.44 * size), (0.0, -2.88 * size)]
412        poly = np.repeat(coords, 3, 0)
413        poly.shape = (len(coords), 3, 2)
414        poly += shape
415        dc.DrawPolygonList(poly.astype(np.int32))
416
417    def _triangle_down(self, dc, coords, size=1):
418        shape = [(-2.5 * size, -1.44 * size),
419                 (2.5 * size, -1.44 * size), (0.0, 2.88 * size)]
420        poly = np.repeat(coords, 3, 0)
421        poly.shape = (len(coords), 3, 2)
422        poly += shape
423        dc.DrawPolygonList(poly.astype(np.int32))
424
425    def _cross(self, dc, coords, size=1):
426        fact = 2.5 * size
427        for f in [[-fact, -fact, fact, fact], [-fact, fact, fact, -fact]]:
428            lines = np.concatenate((coords, coords), axis=1) + f
429            dc.DrawLineList(lines.astype(np.int32))
430
431    def _plus(self, dc, coords, size=1):
432        fact = 2.5 * size
433        for f in [[-fact, 0, fact, 0], [0, -fact, 0, fact]]:
434            lines = np.concatenate((coords, coords), axis=1) + f
435            dc.DrawLineList(lines.astype(np.int32))
436
437
438class PlotGraphics:
439
440    """Container to hold PolyXXX objects and graph labels
441        - All methods except __init__ are private.
442    """
443
444    def __init__(self, objects, title='', xLabel='', yLabel=''):
445        """Creates PlotGraphics object
446        objects - list of PolyXXX objects to make graph
447        title - title shown at top of graph
448        xLabel - label shown on x-axis
449        yLabel - label shown on y-axis
450        """
451        if type(objects) not in [list, tuple]:
452            raise TypeError("objects argument should be list or tuple")
453        self.objects = objects
454        self.title = title
455        self.xLabel = xLabel
456        self.yLabel = yLabel
457        self._pointSize = (1.0, 1.0)
458
459    def setLogScale(self, logscale):
460        if type(logscale) != tuple:
461            raise TypeError(
462                'logscale must be a tuple of bools, e.g. (False, False)')
463        if len(self.objects) == 0:
464            return
465        for o in self.objects:
466            o.setLogScale(logscale)
467
468    def boundingBox(self):
469        p1, p2 = self.objects[0].boundingBox()
470        for o in self.objects[1:]:
471            p1o, p2o = o.boundingBox()
472            p1 = np.minimum(p1, p1o)
473            p2 = np.maximum(p2, p2o)
474        return p1, p2
475
476    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
477        for o in self.objects:
478            o.scaleAndShift(scale, shift)
479
480    def setPrinterScale(self, scale):
481        """Thickens up lines and markers only for printing"""
482        self.printerScale = scale
483
484    def setXLabel(self, xLabel=''):
485        """Set the X axis label on the graph"""
486        self.xLabel = xLabel
487
488    def setYLabel(self, yLabel=''):
489        """Set the Y axis label on the graph"""
490        self.yLabel = yLabel
491
492    def setTitle(self, title=''):
493        """Set the title at the top of graph"""
494        self.title = title
495
496    def getXLabel(self):
497        """Get x axis label string"""
498        return self.xLabel
499
500    def getYLabel(self):
501        """Get y axis label string"""
502        return self.yLabel
503
504    def getTitle(self, title=''):
505        """Get the title at the top of graph"""
506        return self.title
507
508    def draw(self, dc):
509        for o in self.objects:
510            # t=_time.clock()          # profile info
511            o._pointSize = self._pointSize
512            o.draw(dc, self.printerScale)
513            #dt= _time.clock()-t
514            #print(o, "time=", dt)
515
516    def getSymExtent(self, printerScale):
517        """Get max width and height of lines and markers symbols for legend"""
518        self.objects[0]._pointSize = self._pointSize
519        symExt = self.objects[0].getSymExtent(printerScale)
520        for o in self.objects[1:]:
521            o._pointSize = self._pointSize
522            oSymExt = o.getSymExtent(printerScale)
523            symExt = np.maximum(symExt, oSymExt)
524        return symExt
525
526    def getLegendNames(self):
527        """Returns list of legend names"""
528        lst = [None] * len(self)
529        for i in range(len(self)):
530            lst[i] = self.objects[i].getLegend()
531        return lst
532
533    def __len__(self):
534        return len(self.objects)
535
536    def __getitem__(self, item):
537        return self.objects[item]
538
539
540#-------------------------------------------------------------------------
541# Main window that you will want to import into your application.
542
543class PlotCanvas(wx.Panel):
544
545    """
546    Subclass of a wx.Panel which holds two scrollbars and the actual
547    plotting canvas (self.canvas). It allows for simple general plotting
548    of data with zoom, labels, and automatic axis scaling."""
549
550    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
551                 size=wx.DefaultSize, style=0, name="plotCanvas"):
552        """Constructs a panel, which can be a child of a frame or
553        any other non-control window"""
554
555        wx.Panel.__init__(self, parent, id, pos, size, style, name)
556
557        sizer = wx.FlexGridSizer(2, 2, 0, 0)
558        self.canvas = wx.Window(self, -1)
559        self.sb_vert = wx.ScrollBar(self, -1, style=wx.SB_VERTICAL)
560        self.sb_vert.SetScrollbar(0, 1000, 1000, 1000)
561        self.sb_hor = wx.ScrollBar(self, -1, style=wx.SB_HORIZONTAL)
562        self.sb_hor.SetScrollbar(0, 1000, 1000, 1000)
563
564        sizer.Add(self.canvas, 1, wx.EXPAND)
565        sizer.Add(self.sb_vert, 0, wx.EXPAND)
566        sizer.Add(self.sb_hor, 0, wx.EXPAND)
567        sizer.Add((0, 0))
568
569        sizer.AddGrowableRow(0, 1)
570        sizer.AddGrowableCol(0, 1)
571
572        self.sb_vert.Show(False)
573        self.sb_hor.Show(False)
574
575        self.SetSizer(sizer)
576        self.Fit()
577
578        self.border = (1, 1)
579
580        self.SetBackgroundColour("white")
581
582        # Create some mouse events for zooming
583        self.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
584        self.canvas.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
585        self.canvas.Bind(wx.EVT_MOTION, self.OnMotion)
586        self.canvas.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
587        self.canvas.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
588
589        # scrollbar events
590        self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnScroll)
591        self.Bind(wx.EVT_SCROLL_PAGEUP, self.OnScroll)
592        self.Bind(wx.EVT_SCROLL_PAGEDOWN, self.OnScroll)
593        self.Bind(wx.EVT_SCROLL_LINEUP, self.OnScroll)
594        self.Bind(wx.EVT_SCROLL_LINEDOWN, self.OnScroll)
595
596        # set curser as cross-hairs
597        self.canvas.SetCursor(wx.CROSS_CURSOR)
598 
599        self.HandCursor = wx.CursorFromImage(Hand.GetImage())
600        self.GrabHandCursor = wx.CursorFromImage(GrabHand.GetImage())
601        self.MagCursor = wx.CursorFromImage(MagPlus.GetImage())
602
603        #self.HandCursor = wx.Cursor(Hand.GetImage())
604        #self.GrabHandCursor = wx.Cursor(GrabHand.GetImage())
605        #self.MagCursor = wx.Cursor(MagPlus.GetImage())
606 
607
608        # Things for printing
609        self._print_data = None
610        self._pageSetupData = None
611        self.printerScale = 1
612        self.parent = parent
613
614        # scrollbar variables
615        self._sb_ignore = False
616        self._adjustingSB = False
617        self._sb_xfullrange = 0
618        self._sb_yfullrange = 0
619        self._sb_xunit = 0
620        self._sb_yunit = 0
621
622        self._dragEnabled = False
623        self._screenCoordinates = np.array([0.0, 0.0])
624
625        self._logscale = (False, False)
626
627        # Zooming variables
628        self._zoomInFactor = 0.5
629        self._zoomOutFactor = 2
630        self._zoomCorner1 = np.array([0.0, 0.0])  # left mouse down corner
631        self._zoomCorner2 = np.array([0.0, 0.0])   # left mouse up corner
632        self._zoomEnabled = False
633        self._hasDragged = False
634
635        # Drawing Variables
636        self.last_draw = None
637        self._pointScale = 1
638        self._pointShift = 0
639        self._xSpec = 'auto'
640        self._ySpec = 'auto'
641        self._gridEnabled = False
642        self._legendEnabled = False
643        self._titleEnabled = True
644        self._centerLinesEnabled = False
645        self._diagonalsEnabled = False
646
647        # Fonts
648        self._fontCache = {}
649        self._fontSizeAxis = 10
650        self._fontSizeTitle = 15
651        self._fontSizeLegend = 7
652
653        # pointLabels
654        self._pointLabelEnabled = False
655        self.last_PointLabel = None
656        self._pointLabelFunc = None
657        self.canvas.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
658        if sys.platform != "darwin":
659            self._logicalFunction = wx.EQUIV  # (NOT src) XOR dst
660        else:
661            # wx.EQUIV not supported on Mac OS X
662            self._logicalFunction = wx.COPY
663
664        self._useScientificNotation = False
665
666        self._antiAliasingEnabled = False
667        self._hiResEnabled = False
668        self._pointSize = (1.0, 1.0)
669        self._fontScale = 1.0
670
671        self.canvas.Bind(wx.EVT_PAINT, self.OnPaint)
672        self.canvas.Bind(wx.EVT_SIZE, self.OnSize)
673        # OnSize called to make sure the buffer is initialized.
674        # This might result in OnSize getting called twice on some
675        # platforms at initialization, but little harm done.
676        self.OnSize(None)  # sets the initial size based on client size
677
678        self._gridColour = wx.BLACK
679
680    def SetCursor(self, cursor):
681        self.canvas.SetCursor(cursor)
682
683    def GetGridColour(self):
684        return self._gridColour
685
686    def SetGridColour(self, colour):
687        if isinstance(colour, wx.Colour):
688            self._gridColour = colour
689        else:
690            self._gridColour = wx.Colour(colour)
691
692    # SaveFile
693    def SaveFile(self, fileName=''):
694        """Saves the file to the type specified in the extension. If no file
695        name is specified a dialog box is provided.  Returns True if sucessful,
696        otherwise False.
697
698        .bmp  Save a Windows bitmap file.
699        .xbm  Save an X bitmap file.
700        .xpm  Save an XPM bitmap file.
701        .png  Save a Portable Network Graphics file.
702        .jpg  Save a Joint Photographic Experts Group file.
703        """
704        extensions = {
705            "bmp": wx.BITMAP_TYPE_BMP,       # Save a Windows bitmap file.
706            "xbm": wx.BITMAP_TYPE_XBM,       # Save an X bitmap file.
707            "xpm": wx.BITMAP_TYPE_XPM,       # Save an XPM bitmap file.
708            "jpg": wx.BITMAP_TYPE_JPEG,      # Save a JPG file.
709            "png": wx.BITMAP_TYPE_PNG,       # Save a PNG file.
710        }
711
712        fType = _string.lower(fileName[-3:])
713        dlg1 = None
714        while fType not in extensions:
715
716            if dlg1:                   # FileDialog exists: Check for extension
717                dlg2 = wx.MessageDialog(self, 'File name extension\n'
718                                        'must be one of\nbmp, xbm, xpm, png, or jpg',
719                                        'File Name Error', wx.OK | wx.ICON_ERROR)
720                try:
721                    dlg2.ShowModal()
722                finally:
723                    dlg2.Destroy()
724            # FileDialog doesn't exist: just check one
725            else:
726                dlg1 = wx.FileDialog(
727                    self,
728                    "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
729                    "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
730                    wx.SAVE | wx.OVERWRITE_PROMPT
731                )
732
733            if dlg1.ShowModal() == wx.ID_OK:
734                fileName = dlg1.GetPath()
735                fType = _string.lower(fileName[-3:])
736            else:                      # exit without saving
737                dlg1.Destroy()
738                return False
739
740        if dlg1:
741            dlg1.Destroy()
742
743        # Save Bitmap
744        res = self._Buffer.SaveFile(fileName, extensions[fType])
745        return res
746
747    @property
748    def print_data(self):
749        if not self._print_data:
750            self._print_data = wx.PrintData()
751            self._print_data.SetPaperId(wx.PAPER_LETTER)
752            self._print_data.SetOrientation(wx.LANDSCAPE)
753        return self._print_data
754
755    @property
756    def pageSetupData(self):
757        if not self._pageSetupData:
758            self._pageSetupData = wx.PageSetupDialogData()
759            self._pageSetupData.SetMarginBottomRight((25, 25))
760            self._pageSetupData.SetMarginTopLeft((25, 25))
761            self._pageSetupData.SetPrintData(self.print_data)
762        return self._pageSetupData
763
764    def PageSetup(self):
765        """Brings up the page setup dialog"""
766        data = self.pageSetupData
767        data.SetPrintData(self.print_data)
768        dlg = wx.PageSetupDialog(self.parent, data)
769        try:
770            if dlg.ShowModal() == wx.ID_OK:
771                data = dlg.GetPageSetupData()  # returns wx.PageSetupDialogData
772                # updates page parameters from dialog
773                self.pageSetupData.SetMarginBottomRight(
774                    data.GetMarginBottomRight())
775                self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
776                self.pageSetupData.SetPrintData(data.GetPrintData())
777                self._print_data = wx.PrintData(
778                    data.GetPrintData())  # updates print_data
779        finally:
780            dlg.Destroy()
781
782    def Printout(self, paper=None):
783        """Print current plot."""
784        if paper != None:
785            self.print_data.SetPaperId(paper)
786        pdd = wx.PrintDialogData(self.print_data)
787        printer = wx.Printer(pdd)
788        out = PlotPrintout(self)
789        print_ok = printer.Print(self.parent, out)
790        if print_ok:
791            self._print_data = wx.PrintData(
792                printer.GetPrintDialogData().GetPrintData())
793        out.Destroy()
794
795    def PrintPreview(self):
796        """Print-preview current plot."""
797        printout = PlotPrintout(self)
798        printout2 = PlotPrintout(self)
799        self.preview = wx.PrintPreview(printout, printout2, self.print_data)
800        if not self.preview.IsOk():
801            wx.MessageDialog(self, "Print Preview failed.\n"
802                             "Check that default printer is configured\n",
803                             "Print error", wx.OK | wx.CENTRE).ShowModal()
804        self.preview.SetZoom(40)
805        # search up tree to find frame instance
806        frameInst = self
807        while not isinstance(frameInst, wx.Frame):
808            frameInst = frameInst.GetParent()
809        frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
810        frame.Initialize()
811        frame.SetPosition(self.GetPosition())
812        frame.SetSize((600, 550))
813        frame.Centre(wx.BOTH)
814        frame.Show(True)
815
816    def setLogScale(self, logscale):
817        if type(logscale) != tuple:
818            raise TypeError(
819                'logscale must be a tuple of bools, e.g. (False, False)')
820        if self.last_draw is not None:
821            graphics, xAxis, yAxis = self.last_draw
822            graphics.setLogScale(logscale)
823            self.last_draw = (graphics, None, None)
824        self.SetXSpec('min')
825        self.SetYSpec('min')
826        self._logscale = logscale
827
828    def getLogScale(self):
829        return self._logscale
830
831    def SetFontSizeAxis(self, point=10):
832        """Set the tick and axis label font size (default is 10 point)"""
833        self._fontSizeAxis = point
834
835    def GetFontSizeAxis(self):
836        """Get current tick and axis label font size in points"""
837        return self._fontSizeAxis
838
839    def SetFontSizeTitle(self, point=15):
840        """Set Title font size (default is 15 point)"""
841        self._fontSizeTitle = point
842
843    def GetFontSizeTitle(self):
844        """Get current Title font size in points"""
845        return self._fontSizeTitle
846
847    def SetFontSizeLegend(self, point=7):
848        """Set Legend font size (default is 7 point)"""
849        self._fontSizeLegend = point
850
851    def GetFontSizeLegend(self):
852        """Get current Legend font size in points"""
853        return self._fontSizeLegend
854
855    def SetShowScrollbars(self, value):
856        """Set True to show scrollbars"""
857        if value not in [True, False]:
858            raise TypeError("Value should be True or False")
859        if value == self.GetShowScrollbars():
860            return
861        self.sb_vert.Show(value)
862        self.sb_hor.Show(value)
863        wx.CallAfter(self.Layout)
864
865    def GetShowScrollbars(self):
866        """Set True to show scrollbars"""
867        return self.sb_vert.IsShown()
868
869    def SetUseScientificNotation(self, useScientificNotation):
870        self._useScientificNotation = useScientificNotation
871
872    def GetUseScientificNotation(self):
873        return self._useScientificNotation
874
875    def SetEnableAntiAliasing(self, enableAntiAliasing):
876        """Set True to enable anti-aliasing."""
877        self._antiAliasingEnabled = enableAntiAliasing
878        self.Redraw()
879
880    def GetEnableAntiAliasing(self):
881        return self._antiAliasingEnabled
882
883    def SetEnableHiRes(self, enableHiRes):
884        """Set True to enable high-resolution mode when using anti-aliasing."""
885        self._hiResEnabled = enableHiRes
886        self.Redraw()
887
888    def GetEnableHiRes(self):
889        return self._hiResEnabled
890
891    def SetEnableDrag(self, value):
892        """Set True to enable drag."""
893        if value not in [True, False]:
894            raise TypeError("Value should be True or False")
895        if value:
896            if self.GetEnableZoom():
897                self.SetEnableZoom(False)
898            self.SetCursor(self.HandCursor)
899        else:
900            self.SetCursor(wx.CROSS_CURSOR)
901        self._dragEnabled = value
902
903    def GetEnableDrag(self):
904        return self._dragEnabled
905
906    def SetEnableZoom(self, value):
907        """Set True to enable zooming."""
908        if value not in [True, False]:
909            raise TypeError("Value should be True or False")
910        if value:
911            if self.GetEnableDrag():
912                self.SetEnableDrag(False)
913            self.SetCursor(self.MagCursor)
914        else:
915            self.SetCursor(wx.CROSS_CURSOR)
916        self._zoomEnabled = value
917
918    def GetEnableZoom(self):
919        """True if zooming enabled."""
920        return self._zoomEnabled
921
922    def SetEnableGrid(self, value):
923        """Set True, 'Horizontal' or 'Vertical' to enable grid."""
924        if value not in [True, False, 'Horizontal', 'Vertical']:
925            raise TypeError(
926                "Value should be True, False, Horizontal or Vertical")
927        self._gridEnabled = value
928        self.Redraw()
929
930    def GetEnableGrid(self):
931        """True if grid enabled."""
932        return self._gridEnabled
933
934    def SetEnableCenterLines(self, value):
935        """Set True, 'Horizontal' or 'Vertical' to enable center line(s)."""
936        if value not in [True, False, 'Horizontal', 'Vertical']:
937            raise TypeError(
938                "Value should be True, False, Horizontal or Vertical")
939        self._centerLinesEnabled = value
940        self.Redraw()
941
942    def GetEnableCenterLines(self):
943        """True if grid enabled."""
944        return self._centerLinesEnabled
945
946    def SetEnableDiagonals(self, value):
947        """Set True, 'Bottomleft-Topright' or 'Bottomright-Topleft' to enable
948        center line(s)."""
949        if value not in [True, False, 'Bottomleft-Topright', 'Bottomright-Topleft']:
950            raise TypeError(
951                "Value should be True, False, Bottomleft-Topright or Bottomright-Topleft")
952        self._diagonalsEnabled = value
953        self.Redraw()
954
955    def GetEnableDiagonals(self):
956        """True if grid enabled."""
957        return self._diagonalsEnabled
958
959    def SetEnableLegend(self, value):
960        """Set True to enable legend."""
961        if value not in [True, False]:
962            raise TypeError("Value should be True or False")
963        self._legendEnabled = value
964        self.Redraw()
965
966    def GetEnableLegend(self):
967        """True if Legend enabled."""
968        return self._legendEnabled
969
970    def SetEnableTitle(self, value):
971        """Set True to enable title."""
972        if value not in [True, False]:
973            raise TypeError("Value should be True or False")
974        self._titleEnabled = value
975        self.Redraw()
976
977    def GetEnableTitle(self):
978        """True if title enabled."""
979        return self._titleEnabled
980
981    def SetEnablePointLabel(self, value):
982        """Set True to enable pointLabel."""
983        if value not in [True, False]:
984            raise TypeError("Value should be True or False")
985        self._pointLabelEnabled = value
986        self.Redraw()  # will erase existing pointLabel if present
987        self.last_PointLabel = None
988
989    def GetEnablePointLabel(self):
990        """True if pointLabel enabled."""
991        return self._pointLabelEnabled
992
993    def SetPointLabelFunc(self, func):
994        """Sets the function with custom code for pointLabel drawing
995            ******** more info needed ***************
996        """
997        self._pointLabelFunc = func
998
999    def GetPointLabelFunc(self):
1000        """Returns pointLabel Drawing Function"""
1001        return self._pointLabelFunc
1002
1003    def Reset(self):
1004        """Unzoom the plot."""
1005        self.last_PointLabel = None  # reset pointLabel
1006        if self.last_draw is not None:
1007            self._Draw(self.last_draw[0])
1008
1009    def ScrollRight(self, units):
1010        """Move view right number of axis units."""
1011        self.last_PointLabel = None  # reset pointLabel
1012        if self.last_draw is not None:
1013            graphics, xAxis, yAxis = self.last_draw
1014            xAxis = (xAxis[0] + units, xAxis[1] + units)
1015            self._Draw(graphics, xAxis, yAxis)
1016
1017    def ScrollUp(self, units):
1018        """Move view up number of axis units."""
1019        self.last_PointLabel = None  # reset pointLabel
1020        if self.last_draw is not None:
1021            graphics, xAxis, yAxis = self.last_draw
1022            yAxis = (yAxis[0] + units, yAxis[1] + units)
1023            self._Draw(graphics, xAxis, yAxis)
1024
1025    def GetXY(self, event):
1026        """Wrapper around _getXY, which handles log scales"""
1027        x, y = self._getXY(event)
1028        if self.getLogScale()[0]:
1029            x = np.power(10, x)
1030        if self.getLogScale()[1]:
1031            y = np.power(10, y)
1032        return x, y
1033
1034    def _getXY(self, event):
1035        """Takes a mouse event and returns the XY user axis values."""
1036        x, y = self.PositionScreenToUser(event.GetPosition())
1037        return x, y
1038
1039    def PositionUserToScreen(self, pntXY):
1040        """Converts User position to Screen Coordinates"""
1041        userPos = np.array(pntXY)
1042        x, y = userPos * self._pointScale + self._pointShift
1043        return x, y
1044
1045    def PositionScreenToUser(self, pntXY):
1046        """Converts Screen position to User Coordinates"""
1047        screenPos = np.array(pntXY)
1048        x, y = (screenPos - self._pointShift) / self._pointScale
1049        return x, y
1050
1051    def SetXSpec(self, type='auto'):
1052        """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
1053        where:
1054
1055        * 'none' - shows no axis or tick mark values
1056        * 'min' - shows min bounding box values
1057        * 'auto' - rounds axis range to sensible values
1058        * <number> - like 'min', but with <number> tick marks
1059        """
1060        self._xSpec = type
1061
1062    def SetYSpec(self, type='auto'):
1063        """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
1064        where:
1065
1066        * 'none' - shows no axis or tick mark values
1067        * 'min' - shows min bounding box values
1068        * 'auto' - rounds axis range to sensible values
1069        * <number> - like 'min', but with <number> tick marks
1070        """
1071        self._ySpec = type
1072
1073    def GetXSpec(self):
1074        """Returns current XSpec for axis"""
1075        return self._xSpec
1076
1077    def GetYSpec(self):
1078        """Returns current YSpec for axis"""
1079        return self._ySpec
1080
1081    def GetXMaxRange(self):
1082        xAxis = self._getXMaxRange()
1083        if self.getLogScale()[0]:
1084            xAxis = np.power(10, xAxis)
1085        return xAxis
1086
1087    def _getXMaxRange(self):
1088        """Returns (minX, maxX) x-axis range for displayed graph"""
1089        graphics = self.last_draw[0]
1090        p1, p2 = graphics.boundingBox()     # min, max points of graphics
1091        xAxis = self._axisInterval(self._xSpec, p1[0], p2[0])  # in user units
1092        return xAxis
1093
1094    def GetYMaxRange(self):
1095        yAxis = self._getYMaxRange()
1096        if self.getLogScale()[1]:
1097            yAxis = np.power(10, yAxis)
1098        return yAxis
1099
1100    def _getYMaxRange(self):
1101        """Returns (minY, maxY) y-axis range for displayed graph"""
1102        graphics = self.last_draw[0]
1103        p1, p2 = graphics.boundingBox()     # min, max points of graphics
1104        yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
1105        return yAxis
1106
1107    def GetXCurrentRange(self):
1108        xAxis = self._getXCurrentRange()
1109        if self.getLogScale()[0]:
1110            xAxis = np.power(10, xAxis)
1111        return xAxis
1112
1113    def _getXCurrentRange(self):
1114        """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
1115        return self.last_draw[1]
1116
1117    def GetYCurrentRange(self):
1118        yAxis = self._getYCurrentRange()
1119        if self.getLogScale()[1]:
1120            yAxis = np.power(10, yAxis)
1121        return yAxis
1122
1123    def _getYCurrentRange(self):
1124        """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
1125        return self.last_draw[2]
1126
1127    def Draw(self, graphics, xAxis=None, yAxis=None, dc=None):
1128        """Wrapper around _Draw, which handles log axes"""
1129        graphics.setLogScale(self.getLogScale())
1130
1131        # check Axis is either tuple or none
1132        if type(xAxis) not in [type(None), tuple]:
1133            raise TypeError(
1134                "xAxis should be None or (minX,maxX)" + str(type(xAxis)))
1135        if type(yAxis) not in [type(None), tuple]:
1136            raise TypeError(
1137                "yAxis should be None or (minY,maxY)" + str(type(xAxis)))
1138
1139        # check case for axis = (a,b) where a==b caused by improper zooms
1140        if xAxis != None:
1141            if xAxis[0] == xAxis[1]:
1142                return
1143            if self.getLogScale()[0]:
1144                xAxis = np.log10(xAxis)
1145        if yAxis != None:
1146            if yAxis[0] == yAxis[1]:
1147                return
1148            if self.getLogScale()[1]:
1149                yAxis = np.log10(yAxis)
1150        self._Draw(graphics, xAxis, yAxis, dc)
1151
1152    def _Draw(self, graphics, xAxis=None, yAxis=None, dc=None):
1153        """\
1154        Draw objects in graphics with specified x and y axis.
1155        graphics- instance of PlotGraphics with list of PolyXXX objects
1156        xAxis - tuple with (min, max) axis range to view
1157        yAxis - same as xAxis
1158        dc - drawing context - doesn't have to be specified.
1159        If it's not, the offscreen buffer is used
1160        """
1161
1162        if dc == None:
1163            # sets new dc and clears it
1164            dc = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer)
1165            bbr = wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)
1166            dc.SetBackground(bbr)
1167            dc.SetBackgroundMode(wx.SOLID)
1168            dc.Clear()
1169        if self._antiAliasingEnabled:
1170            if not isinstance(dc, wx.GCDC):
1171                try:
1172                    dc = wx.GCDC(dc)
1173                except Exception:
1174                    pass
1175                else:
1176                    if self._hiResEnabled:
1177                        # high precision - each logical unit is 1/20 of a point
1178                        dc.SetMapMode(wx.MM_TWIPS)
1179                    self._pointSize = tuple(
1180                        1.0 / lscale for lscale in dc.GetLogicalScale())
1181                    self._setSize()
1182        elif self._pointSize != (1.0, 1.0):
1183            self._pointSize = (1.0, 1.0)
1184            self._setSize()
1185        if (sys.platform in ("darwin", "win32") or not isinstance(dc, wx.GCDC) or wx.VERSION >= (2, 9)):
1186            self._fontScale = sum(self._pointSize) / 2.0
1187        else:
1188            # on Linux, we need to correct the font size by a certain factor if wx.GCDC is used,
1189            # to make text the same size as if wx.GCDC weren't used
1190            screenppi = map(float, wx.ScreenDC().GetPPI())
1191            ppi = dc.GetPPI()
1192            self._fontScale = (screenppi[
1193                               0] / ppi[0] * self._pointSize[0] + screenppi[1] / ppi[1] * self._pointSize[1]) / 2.0
1194        graphics._pointSize = self._pointSize
1195
1196        dc.SetTextForeground(self.GetForegroundColour())
1197        dc.SetTextBackground(self.GetBackgroundColour())
1198
1199        # dc.Clear()
1200
1201        # set font size for every thing but title and legend
1202        dc.SetFont(self._getFont(self._fontSizeAxis))
1203
1204        # sizes axis to axis type, create lower left and upper right corners of
1205        # plot
1206        if xAxis == None or yAxis == None:
1207            # One or both axis not specified in Draw
1208            p1, p2 = graphics.boundingBox()     # min, max points of graphics
1209            if xAxis == None:
1210                xAxis = self._axisInterval(
1211                    self._xSpec, p1[0], p2[0])  # in user units
1212            if yAxis == None:
1213                yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
1214            # Adjust bounding box for axis spec
1215            # lower left corner user scale (xmin,ymin)
1216            p1[0], p1[1] = xAxis[0], yAxis[0]
1217            # upper right corner user scale (xmax,ymax)
1218            p2[0], p2[1] = xAxis[1], yAxis[1]
1219        else:
1220            # Both axis specified in Draw
1221            # lower left corner user scale (xmin,ymin)
1222            p1 = np.array([xAxis[0], yAxis[0]])
1223            # upper right corner user scale (xmax,ymax)
1224            p2 = np.array([xAxis[1], yAxis[1]])
1225
1226        # saves most recient values
1227        self.last_draw = (graphics, np.array(xAxis), np.array(yAxis))
1228
1229        # Get ticks and textExtents for axis if required
1230        if self._xSpec is not 'none':
1231            xticks = self._xticks(xAxis[0], xAxis[1])
1232        else:
1233            xticks = None
1234        if xticks:
1235            # w h of x axis text last number on axis
1236            xTextExtent = dc.GetTextExtent(xticks[-1][1])
1237        else:
1238            xTextExtent = (0, 0)  # No text for ticks
1239        if self._ySpec is not 'none':
1240            yticks = self._yticks(yAxis[0], yAxis[1])
1241        else:
1242            yticks = None
1243        if yticks:
1244            if self.getLogScale()[1]:
1245                yTextExtent = dc.GetTextExtent('-2e-2')
1246            else:
1247                yTextExtentBottom = dc.GetTextExtent(yticks[0][1])
1248                yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
1249                yTextExtent = (max(yTextExtentBottom[0], yTextExtentTop[0]),
1250                               max(yTextExtentBottom[1], yTextExtentTop[1]))
1251        else:
1252            yticks = None
1253            yTextExtent = (0, 0)  # No text for ticks
1254
1255        # TextExtents for Title and Axis Labels
1256        titleWH, xLabelWH, yLabelWH = self._titleLablesWH(dc, graphics)
1257
1258        # TextExtents for Legend
1259        legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
1260
1261        # room around graph area
1262        # use larger of number width or legend width
1263        rhsW = max(xTextExtent[0], legendBoxWH[0]) + 5 * self._pointSize[0]
1264        lhsW = yTextExtent[0] + yLabelWH[1] + 3 * self._pointSize[0]
1265        bottomH = max(
1266            xTextExtent[1], yTextExtent[1] / 2.) + xLabelWH[1] + 2 * self._pointSize[1]
1267        topH = yTextExtent[1] / 2. + titleWH[1]
1268        # make plot area smaller by text size
1269        textSize_scale = np.array([rhsW + lhsW, bottomH + topH])
1270        # shift plot area by this amount
1271        textSize_shift = np.array([lhsW, bottomH])
1272
1273        # draw title if requested
1274        if self._titleEnabled:
1275            dc.SetFont(self._getFont(self._fontSizeTitle))
1276            titlePos = (self.plotbox_origin[0] + lhsW + (self.plotbox_size[0] - lhsW - rhsW) / 2. - titleWH[0] / 2.,
1277                        self.plotbox_origin[1] - self.plotbox_size[1])
1278            dc.DrawText(graphics.getTitle(), titlePos[0], titlePos[1])
1279
1280        # draw label text
1281        dc.SetFont(self._getFont(self._fontSizeAxis))
1282        xLabelPos = (self.plotbox_origin[0] + lhsW + (self.plotbox_size[0] - lhsW - rhsW) / 2. - xLabelWH[0] / 2.,
1283                     self.plotbox_origin[1] - xLabelWH[1])
1284        dc.DrawText(graphics.getXLabel(), xLabelPos[0], xLabelPos[1])
1285        yLabelPos = (self.plotbox_origin[0] - 3 * self._pointSize[0],
1286                     self.plotbox_origin[1] - bottomH - (self.plotbox_size[1] - bottomH - topH) / 2. + yLabelWH[0] / 2.)
1287        if graphics.getYLabel():  # bug fix for Linux
1288            dc.DrawRotatedText(
1289                graphics.getYLabel(), yLabelPos[0], yLabelPos[1], 90)
1290
1291        # drawing legend makers and text
1292        if self._legendEnabled:
1293            self._drawLegend(
1294                dc, graphics, rhsW, topH, legendBoxWH, legendSymExt, legendTextExt)
1295
1296        # allow for scaling and shifting plotted points
1297        scale = (self.plotbox_size - textSize_scale) / \
1298            (p2 - p1) * np.array((1, -1))
1299        shift = -p1 * scale + self.plotbox_origin + \
1300            textSize_shift * np.array((1, -1))
1301        # make available for mouse events
1302        self._pointScale = scale / self._pointSize
1303        self._pointShift = shift / self._pointSize
1304        self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
1305
1306        graphics.scaleAndShift(scale, shift)
1307        # thicken up lines and markers if printing
1308        graphics.setPrinterScale(self.printerScale)
1309
1310        # set clipping area so drawing does not occur outside axis box
1311        ptx, pty, rectWidth, rectHeight = self._point2ClientCoord(p1, p2)
1312        # allow graph to overlap axis lines by adding units to width and height
1313        dc.SetClippingRegion(ptx * self._pointSize[0], pty * self._pointSize[
1314                             1], rectWidth * self._pointSize[0] + 2, rectHeight * self._pointSize[1] + 1)
1315        # Draw the lines and markers
1316        #start = _time.clock()
1317        graphics.draw(dc)
1318        # print("entire graphics drawing took: %f second"%(_time.clock() - start))
1319        # remove the clipping region
1320        dc.DestroyClippingRegion()
1321
1322        self._adjustScrollbars()
1323
1324    def Redraw(self, dc=None):
1325        """Redraw the existing plot."""
1326        if self.last_draw is not None:
1327            graphics, xAxis, yAxis = self.last_draw
1328            self._Draw(graphics, xAxis, yAxis, dc)
1329
1330    def Clear(self):
1331        """Erase the window."""
1332        self.last_PointLabel = None  # reset pointLabel
1333        dc = wx.BufferedDC(wx.ClientDC(self.canvas), self._Buffer)
1334        bbr = wx.Brush(self.GetBackgroundColour(), wx.SOLID)
1335        dc.SetBackground(bbr)
1336        dc.SetBackgroundMode(wx.SOLID)
1337        dc.Clear()
1338        if self._antiAliasingEnabled:
1339            try:
1340                dc = wx.GCDC(dc)
1341            except Exception:
1342                pass
1343        dc.SetTextForeground(self.GetForegroundColour())
1344        dc.SetTextBackground(self.GetBackgroundColour())
1345        self.last_draw = None
1346
1347    def Zoom(self, Center, Ratio):
1348        """ Zoom on the plot
1349            Centers on the X,Y coords given in Center
1350            Zooms by the Ratio = (Xratio, Yratio) given
1351        """
1352        self.last_PointLabel = None  # reset maker
1353        x, y = Center
1354        if self.last_draw != None:
1355            (graphics, xAxis, yAxis) = self.last_draw
1356            w = (xAxis[1] - xAxis[0]) * Ratio[0]
1357            h = (yAxis[1] - yAxis[0]) * Ratio[1]
1358            xAxis = (x - w / 2, x + w / 2)
1359            yAxis = (y - h / 2, y + h / 2)
1360            self._Draw(graphics, xAxis, yAxis)
1361
1362    def GetClosestPoints(self, pntXY, pointScaled=True):
1363        """Returns list with
1364            [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1365            list for each curve.
1366            Returns [] if no curves are being plotted.
1367
1368            x, y in user coords
1369            if pointScaled == True based on screen coords
1370            if pointScaled == False based on user coords
1371        """
1372        if self.last_draw == None:
1373            # no graph available
1374            return []
1375        graphics, xAxis, yAxis = self.last_draw
1376        l = []
1377        for curveNum, obj in enumerate(graphics):
1378            # check there are points in the curve
1379            if len(obj.points) == 0:
1380                continue  # go to next obj
1381            #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1382            cn = [curveNum] + \
1383                [obj.getLegend()] + obj.getClosestPoint(pntXY, pointScaled)
1384            l.append(cn)
1385        return l
1386
1387    def GetClosestPoint(self, pntXY, pointScaled=True):
1388        """Returns list with
1389            [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
1390            list for only the closest curve.
1391            Returns [] if no curves are being plotted.
1392
1393            x, y in user coords
1394            if pointScaled == True based on screen coords
1395            if pointScaled == False based on user coords
1396        """
1397        # closest points on screen based on screen scaling (pointScaled= True)
1398        # list [curveNumber, index, pointXY, scaledXY, distance] for each curve
1399        closestPts = self.GetClosestPoints(pntXY, pointScaled)
1400        if closestPts == []:
1401            return []  # no graph present
1402        # find one with least distance
1403        dists = [c[-1] for c in closestPts]
1404        mdist = min(dists)  # Min dist
1405        i = dists.index(mdist)  # index for min dist
1406        return closestPts[i]  # this is the closest point on closest curve
1407
1408    GetClosetPoint = GetClosestPoint
1409
1410    def UpdatePointLabel(self, mDataDict):
1411        """Updates the pointLabel point on screen with data contained in
1412            mDataDict.
1413
1414            mDataDict will be passed to your function set by
1415            SetPointLabelFunc.  It can contain anything you
1416            want to display on the screen at the scaledXY point
1417            you specify.
1418
1419            This function can be called from parent window with onClick,
1420            onMotion events etc.
1421        """
1422        if self.last_PointLabel != None:
1423            # compare pointXY
1424            if np.sometrue(mDataDict["pointXY"] != self.last_PointLabel["pointXY"]):
1425                # closest changed
1426                self._drawPointLabel(self.last_PointLabel)  # erase old
1427                self._drawPointLabel(mDataDict)  # plot new
1428        else:
1429            # just plot new with no erase
1430            self._drawPointLabel(mDataDict)  # plot new
1431        # save for next erase
1432        self.last_PointLabel = mDataDict
1433
1434    # event handlers **********************************
1435    def OnMotion(self, event):
1436        if self._zoomEnabled and event.LeftIsDown():
1437            if self._hasDragged:
1438                self._drawRubberBand(
1439                    self._zoomCorner1, self._zoomCorner2)  # remove old
1440            else:
1441                self._hasDragged = True
1442            self._zoomCorner2[0], self._zoomCorner2[1] = self._getXY(event)
1443            self._drawRubberBand(
1444                self._zoomCorner1, self._zoomCorner2)  # add new
1445        elif self._dragEnabled and event.LeftIsDown():
1446            coordinates = event.GetPosition()
1447            newpos, oldpos = map(np.array, map(
1448                self.PositionScreenToUser, [coordinates, self._screenCoordinates]))
1449            dist = newpos - oldpos
1450            self._screenCoordinates = coordinates
1451
1452            if self.last_draw is not None:
1453                graphics, xAxis, yAxis = self.last_draw
1454                yAxis -= dist[1]
1455                xAxis -= dist[0]
1456                self._Draw(graphics, xAxis, yAxis)
1457
1458    def OnMouseLeftDown(self, event):
1459        self._zoomCorner1[0], self._zoomCorner1[1] = self._getXY(event)
1460        self._screenCoordinates = np.array(event.GetPosition())
1461        if self._dragEnabled:
1462            self.SetCursor(self.GrabHandCursor)
1463            self.canvas.CaptureMouse()
1464
1465    def OnMouseLeftUp(self, event):
1466        if self._zoomEnabled:
1467            if self._hasDragged == True:
1468                self._drawRubberBand(
1469                    self._zoomCorner1, self._zoomCorner2)  # remove old
1470                self._zoomCorner2[0], self._zoomCorner2[1] = self._getXY(event)
1471                self._hasDragged = False  # reset flag
1472                minX, minY = np.minimum(self._zoomCorner1, self._zoomCorner2)
1473                maxX, maxY = np.maximum(self._zoomCorner1, self._zoomCorner2)
1474                self.last_PointLabel = None  # reset pointLabel
1475                if self.last_draw != None:
1476                    self._Draw(
1477                        self.last_draw[0], xAxis=(minX, maxX), yAxis = (minY, maxY), dc = None)
1478            # else: # A box has not been drawn, zoom in on a point
1479            # this interfered with the double click, so I've disables it.
1480            #    X,Y = self._getXY(event)
1481            #    self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
1482        if self._dragEnabled:
1483            self.SetCursor(self.HandCursor)
1484            if self.canvas.HasCapture():
1485                self.canvas.ReleaseMouse()
1486
1487    def OnMouseDoubleClick(self, event):
1488        if self._zoomEnabled:
1489            # Give a little time for the click to be totally finished
1490            # before (possibly) removing the scrollbars and trigering
1491            # size events, etc.
1492            wx.CallLater(200, self.Reset)
1493
1494    def OnMouseRightDown(self, event):
1495        if self._zoomEnabled:
1496            X, Y = self._getXY(event)
1497            self.Zoom((X, Y), (self._zoomOutFactor, self._zoomOutFactor))
1498
1499    def OnPaint(self, event):
1500        # All that is needed here is to draw the buffer to screen
1501        if self.last_PointLabel != None:
1502            self._drawPointLabel(self.last_PointLabel)  # erase old
1503            self.last_PointLabel = None
1504        dc = wx.BufferedPaintDC(self.canvas, self._Buffer)
1505        if self._antiAliasingEnabled:
1506            try:
1507                dc = wx.GCDC(dc)
1508            except Exception:
1509                pass
1510
1511    def OnSize(self, event):
1512        # The Buffer init is done here, to make sure the buffer is always
1513        # the same size as the Window
1514        Size = self.canvas.GetClientSize()
1515        Size.width = max(1, Size.width)
1516        Size.height = max(1, Size.height)
1517
1518        # Make new offscreen bitmap: this bitmap will always have the
1519        # current drawing in it, so it can be used to save the image to
1520        # a file, or whatever.
1521        self._Buffer = wx.EmptyBitmap(Size.width, Size.height)
1522        self._setSize()
1523
1524        self.last_PointLabel = None  # reset pointLabel
1525
1526        if self.last_draw is None:
1527            self.Clear()
1528        else:
1529            graphics, xSpec, ySpec = self.last_draw
1530            self._Draw(graphics, xSpec, ySpec)
1531
1532    def OnLeave(self, event):
1533        """Used to erase pointLabel when mouse outside window"""
1534        if self.last_PointLabel != None:
1535            self._drawPointLabel(self.last_PointLabel)  # erase old
1536            self.last_PointLabel = None
1537
1538    def OnScroll(self, evt):
1539        if not self._adjustingSB:
1540            self._sb_ignore = True
1541            sbpos = evt.GetPosition()
1542
1543            if evt.GetOrientation() == wx.VERTICAL:
1544                fullrange, pagesize = self.sb_vert.GetRange(
1545                ), self.sb_vert.GetPageSize()
1546                sbpos = fullrange - pagesize - sbpos
1547                dist = sbpos * self._sb_xunit - \
1548                    (self._getXCurrentRange()[0] - self._sb_xfullrange)
1549                self.ScrollUp(dist)
1550
1551            if evt.GetOrientation() == wx.HORIZONTAL:
1552                dist = sbpos * self._sb_xunit - \
1553                    (self._getXCurrentRange()[0] - self._sb_xfullrange[0])
1554                self.ScrollRight(dist)
1555
1556    # Private Methods **************************************************
1557    def _setSize(self, width=None, height=None):
1558        """DC width and height."""
1559        if width == None:
1560            (self.width, self.height) = self.canvas.GetClientSize()
1561        else:
1562            self.width, self.height = width, height
1563        self.width *= self._pointSize[0]  # high precision
1564        self.height *= self._pointSize[1]  # high precision
1565        self.plotbox_size = 0.97 * np.array([self.width, self.height])
1566        xo = 0.5 * (self.width - self.plotbox_size[0])
1567        yo = self.height - 0.5 * (self.height - self.plotbox_size[1])
1568        self.plotbox_origin = np.array([xo, yo])
1569
1570    def _setPrinterScale(self, scale):
1571        """Used to thicken lines and increase marker size for print out."""
1572        # line thickness on printer is very thin at 600 dot/in. Markers small
1573        self.printerScale = scale
1574
1575    def _printDraw(self, printDC):
1576        """Used for printing."""
1577        if self.last_draw != None:
1578            graphics, xSpec, ySpec = self.last_draw
1579            self._Draw(graphics, xSpec, ySpec, printDC)
1580
1581    def _drawPointLabel(self, mDataDict):
1582        """Draws and erases pointLabels"""
1583        width = self._Buffer.GetWidth()
1584        height = self._Buffer.GetHeight()
1585        if sys.platform != "darwin":
1586            tmp_Buffer = wx.Bitmap(width, height)
1587            dcs = wx.MemoryDC()
1588            dcs.SelectObject(tmp_Buffer)
1589            dcs.Clear()
1590        else:
1591            tmp_Buffer = self._Buffer.GetSubBitmap((0, 0, width, height))
1592            dcs = wx.MemoryDC(self._Buffer)
1593        self._pointLabelFunc(dcs, mDataDict)  # custom user pointLabel function
1594
1595        dc = wx.ClientDC(self.canvas)
1596        dc = wx.BufferedDC(dc, self._Buffer)
1597        # this will erase if called twice
1598        dc.Blit(0, 0, width, height, dcs, 0, 0, self._logicalFunction)
1599        if sys.platform == "darwin":
1600            self._Buffer = tmp_Buffer
1601
1602    def _drawLegend(self, dc, graphics, rhsW, topH, legendBoxWH, legendSymExt, legendTextExt):
1603        """Draws legend symbols and text"""
1604        # top right hand corner of graph box is ref corner
1605        trhc = self.plotbox_origin + \
1606            (self.plotbox_size - [rhsW, topH]) * [1, -1]
1607        # border space between legend sym and graph box
1608        legendLHS = .091 * legendBoxWH[0]
1609        # 1.1 used as space between lines
1610        lineHeight = max(legendSymExt[1], legendTextExt[1]) * 1.1
1611        dc.SetFont(self._getFont(self._fontSizeLegend))
1612        for i in range(len(graphics)):
1613            o = graphics[i]
1614            s = i * lineHeight
1615            if isinstance(o, PolyMarker):
1616                # draw marker with legend
1617                pnt = (trhc[0] + legendLHS + legendSymExt[0] / 2.,
1618                       trhc[1] + s + lineHeight / 2.)
1619                o.draw(dc, self.printerScale, coord=np.array([pnt]))
1620            elif isinstance(o, PolyLine):
1621                # draw line with legend
1622                pnt1 = (trhc[0] + legendLHS, trhc[1] + s + lineHeight / 2.)
1623                pnt2 = (trhc[0] + legendLHS + legendSymExt[0],
1624                        trhc[1] + s + lineHeight / 2.)
1625                o.draw(dc, self.printerScale, coord=np.array([pnt1, pnt2]))
1626            else:
1627                raise TypeError(
1628                    "object is neither PolyMarker or PolyLine instance")
1629            # draw legend txt
1630            pnt = (trhc[0] + legendLHS + legendSymExt[0] + 5 * self._pointSize[0],
1631                   trhc[1] + s + lineHeight / 2. - legendTextExt[1] / 2)
1632            dc.DrawText(o.getLegend(), pnt[0], pnt[1])
1633        dc.SetFont(self._getFont(self._fontSizeAxis))  # reset
1634
1635    def _titleLablesWH(self, dc, graphics):
1636        """Draws Title and labels and returns width and height for each"""
1637        # TextExtents for Title and Axis Labels
1638        dc.SetFont(self._getFont(self._fontSizeTitle))
1639        if self._titleEnabled:
1640            title = graphics.getTitle()
1641            titleWH = dc.GetTextExtent(title)
1642        else:
1643            titleWH = (0, 0)
1644        dc.SetFont(self._getFont(self._fontSizeAxis))
1645        xLabel, yLabel = graphics.getXLabel(), graphics.getYLabel()
1646        xLabelWH = dc.GetTextExtent(xLabel)
1647        yLabelWH = dc.GetTextExtent(yLabel)
1648        return titleWH, xLabelWH, yLabelWH
1649
1650    def _legendWH(self, dc, graphics):
1651        """Returns the size in screen units for legend box"""
1652        if self._legendEnabled != True:
1653            legendBoxWH = symExt = txtExt = (0, 0)
1654        else:
1655            # find max symbol size
1656            symExt = graphics.getSymExtent(self.printerScale)
1657            # find max legend text extent
1658            dc.SetFont(self._getFont(self._fontSizeLegend))
1659            txtList = graphics.getLegendNames()
1660            txtExt = dc.GetTextExtent(txtList[0])
1661            for txt in graphics.getLegendNames()[1:]:
1662                txtExt = np.maximum(txtExt, dc.GetTextExtent(txt))
1663            maxW = symExt[0] + txtExt[0]
1664            maxH = max(symExt[1], txtExt[1])
1665            # padding .1 for lhs of legend box and space between lines
1666            maxW = maxW * 1.1
1667            maxH = maxH * 1.1 * len(txtList)
1668            dc.SetFont(self._getFont(self._fontSizeAxis))
1669            legendBoxWH = (maxW, maxH)
1670        return (legendBoxWH, symExt, txtExt)
1671
1672    def _drawRubberBand(self, corner1, corner2):
1673        """Draws/erases rect box from corner1 to corner2"""
1674        ptx, pty, rectWidth, rectHeight = self._point2ClientCoord(
1675            corner1, corner2)
1676        # draw rectangle
1677        dc = wx.ClientDC(self.canvas)
1678        dc.SetPen(wx.Pen(wx.BLACK))
1679        dc.SetBrush(wx.Brush(wx.WHITE, wx.BRUSHSTYLE_TRANSPARENT))
1680        dc.SetLogicalFunction(wx.INVERT)
1681        dc.DrawRectangle(ptx, pty, rectWidth, rectHeight)
1682        dc.SetLogicalFunction(wx.COPY)
1683
1684    def _getFont(self, size):
1685        """Take font size, adjusts if printing and returns wx.Font"""
1686        s = size * self.printerScale * self._fontScale
1687        of = self.GetFont()
1688        # Linux speed up to get font from cache rather than X font server
1689        key = (int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1690        font = self._fontCache.get(key, None)
1691        if font:
1692            return font                 # yeah! cache hit
1693        else:
1694            font = wx.Font(
1695                int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
1696            self._fontCache[key] = font
1697            return font
1698
1699    def _point2ClientCoord(self, corner1, corner2):
1700        """Converts user point coords to client screen int coords x,y,width,height"""
1701        c1 = np.array(corner1)
1702        c2 = np.array(corner2)
1703        # convert to screen coords
1704        pt1 = c1 * self._pointScale + self._pointShift
1705        pt2 = c2 * self._pointScale + self._pointShift
1706        # make height and width positive
1707        pul = np.minimum(pt1, pt2)  # Upper left corner
1708        plr = np.maximum(pt1, pt2)  # Lower right corner
1709        rectWidth, rectHeight = plr - pul
1710        ptx, pty = pul
1711        return ptx, pty, rectWidth, rectHeight
1712
1713    def _axisInterval(self, spec, lower, upper):
1714        """Returns sensible axis range for given spec"""
1715        if spec == 'none' or spec == 'min' or isinstance(spec, (float, int)):
1716            if lower == upper:
1717                return lower - 0.5, upper + 0.5
1718            else:
1719                return lower, upper
1720        elif spec == 'auto':
1721            range = upper - lower
1722            if range == 0.:
1723                return lower - 0.5, upper + 0.5
1724            log = np.log10(range)
1725            power = np.floor(log)
1726            fraction = log - power
1727            if fraction <= 0.05:
1728                power = power - 1
1729            grid = 10. ** power
1730            lower = lower - lower % grid
1731            mod = upper % grid
1732            if mod != 0:
1733                upper = upper - mod + grid
1734            return lower, upper
1735        elif type(spec) == type(()):
1736            lower, upper = spec
1737            if lower <= upper:
1738                return lower, upper
1739            else:
1740                return upper, lower
1741        else:
1742            raise ValueError(str(spec) + ': illegal axis specification')
1743
1744    def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
1745
1746        # increases thickness for printing only
1747        penWidth = self.printerScale * self._pointSize[0]
1748        dc.SetPen(wx.Pen(self._gridColour, penWidth))
1749
1750        # set length of tick marks--long ones make grid
1751        if self._gridEnabled:
1752            x, y, width, height = self._point2ClientCoord(p1, p2)
1753            if self._gridEnabled == 'Horizontal':
1754                yTickLength = (width / 2.0 + 1) * self._pointSize[1]
1755                xTickLength = 3 * self.printerScale * self._pointSize[0]
1756            elif self._gridEnabled == 'Vertical':
1757                yTickLength = 3 * self.printerScale * self._pointSize[1]
1758                xTickLength = (height / 2.0 + 1) * self._pointSize[0]
1759            else:
1760                yTickLength = (width / 2.0 + 1) * self._pointSize[1]
1761                xTickLength = (height / 2.0 + 1) * self._pointSize[0]
1762        else:
1763            # lengthens lines for printing
1764            yTickLength = 3 * self.printerScale * self._pointSize[1]
1765            xTickLength = 3 * self.printerScale * self._pointSize[0]
1766
1767        if self._xSpec is not 'none':
1768            lower, upper = p1[0], p2[0]
1769            text = 1
1770            # miny, maxy and tick lengths
1771            for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]:
1772                for x, label in xticks:
1773                    pt = scale * np.array([x, y]) + shift
1774                    # draws tick mark d units
1775                    dc.DrawLine(pt[0], pt[1], pt[0], pt[1] + d)
1776                    if text:
1777                        dc.DrawText(
1778                            label, pt[0], pt[1] + 2 * self._pointSize[1])
1779                a1 = scale * np.array([lower, y]) + shift
1780                a2 = scale * np.array([upper, y]) + shift
1781                # draws upper and lower axis line
1782                dc.DrawLine(a1[0], a1[1], a2[0], a2[1])
1783                text = 0  # axis values not drawn on top side
1784
1785        if self._ySpec is not 'none':
1786            lower, upper = p1[1], p2[1]
1787            text = 1
1788            h = dc.GetCharHeight()
1789            for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
1790                for y, label in yticks:
1791                    pt = scale * np.array([x, y]) + shift
1792                    dc.DrawLine(pt[0], pt[1], pt[0] - d, pt[1])
1793                    if text:
1794                        dc.DrawText(label, pt[0] - dc.GetTextExtent(label)[0] - 3 * self._pointSize[0],
1795                                    pt[1] - 0.75 * h)
1796                a1 = scale * np.array([x, lower]) + shift
1797                a2 = scale * np.array([x, upper]) + shift
1798                dc.DrawLine(a1[0], a1[1], a2[0], a2[1])
1799                text = 0    # axis values not drawn on right side
1800
1801        if self._centerLinesEnabled:
1802            if self._centerLinesEnabled in ('Horizontal', True):
1803                y1 = scale[1] * p1[1] + shift[1]
1804                y2 = scale[1] * p2[1] + shift[1]
1805                y = (y1 - y2) / 2.0 + y2
1806                dc.DrawLine(
1807                    scale[0] * p1[0] + shift[0], y, scale[0] * p2[0] + shift[0], y)
1808            if self._centerLinesEnabled in ('Vertical', True):
1809                x1 = scale[0] * p1[0] + shift[0]
1810                x2 = scale[0] * p2[0] + shift[0]
1811                x = (x1 - x2) / 2.0 + x2
1812                dc.DrawLine(
1813                    x, scale[1] * p1[1] + shift[1], x, scale[1] * p2[1] + shift[1])
1814
1815        if self._diagonalsEnabled:
1816            if self._diagonalsEnabled in ('Bottomleft-Topright', True):
1817                dc.DrawLine(scale[0] * p1[0] + shift[0], scale[1] * p1[1] +
1818                            shift[1], scale[0] * p2[0] + shift[0], scale[1] * p2[1] + shift[1])
1819            if self._diagonalsEnabled in ('Bottomright-Topleft', True):
1820                dc.DrawLine(scale[0] * p1[0] + shift[0], scale[1] * p2[1] +
1821                            shift[1], scale[0] * p2[0] + shift[0], scale[1] * p1[1] + shift[1])
1822
1823    def _xticks(self, *args):
1824        if self._logscale[0]:
1825            return self._logticks(*args)
1826        else:
1827            attr = {'numticks': self._xSpec}
1828            return self._ticks(*args, **attr)
1829
1830    def _yticks(self, *args):
1831        if self._logscale[1]:
1832            return self._logticks(*args)
1833        else:
1834            attr = {'numticks': self._ySpec}
1835            return self._ticks(*args, **attr)
1836
1837    def _logticks(self, lower, upper):
1838        #lower,upper = map(np.log10,[lower,upper])
1839        # print('logticks',lower,upper)
1840        ticks = []
1841        mag = np.power(10, np.floor(lower))
1842        if upper - lower > 6:
1843            t = np.power(10, np.ceil(lower))
1844            base = np.power(10, np.floor((upper - lower) / 6))
1845
1846            def inc(t):
1847                return t * base - t
1848        else:
1849            t = np.ceil(np.power(10, lower) / mag) * mag
1850
1851            def inc(t):
1852                return 10 ** int(np.floor(np.log10(t) + 1e-16))
1853        majortick = int(np.log10(mag))
1854        while t <= pow(10, upper):
1855            if majortick != int(np.floor(np.log10(t) + 1e-16)):
1856                majortick = int(np.floor(np.log10(t) + 1e-16))
1857                ticklabel = '1e%d' % majortick
1858            else:
1859                if upper - lower < 2:
1860                    minortick = int(t / pow(10, majortick) + .5)
1861                    ticklabel = '%de%d' % (minortick, majortick)
1862                else:
1863                    ticklabel = ''
1864            ticks.append((np.log10(t), ticklabel))
1865            t += inc(t)
1866        if len(ticks) == 0:
1867            ticks = [(0, '')]
1868        return ticks
1869
1870    def _ticks(self, lower, upper, numticks=None):
1871        if isinstance(numticks, (float, int)):
1872            ideal = (upper - lower) / float(numticks)
1873        else:
1874            ideal = (upper - lower) / 7.
1875        log = np.log10(ideal)
1876        power = np.floor(log)
1877        if isinstance(numticks, (float, int)):
1878            grid = ideal
1879        else:
1880            fraction = log - power
1881            factor = 1.
1882            error = fraction
1883            for f, lf in self._multiples:
1884                e = np.fabs(fraction - lf)
1885                if e < error:
1886                    error = e
1887                    factor = f
1888            grid = factor * 10. ** power
1889        if self._useScientificNotation and (power > 4 or power < -4):
1890            format = '%+7.1e'
1891        elif power >= 0:
1892            digits = max(1, int(power))
1893            format = '%' + repr(digits) + '.0f'
1894        else:
1895            digits = -int(power)
1896            format = '%' + repr(digits + 2) + '.' + repr(digits) + 'f'
1897        ticks = []
1898        t = -grid * np.floor(-lower / grid)
1899        while t <= upper:
1900            if t == -0:
1901                t = 0
1902            ticks.append((t, format % (t,)))
1903            t = t + grid
1904        return ticks
1905
1906    _multiples = [(2., np.log10(2.)), (5., np.log10(5.))]
1907
1908    def _adjustScrollbars(self):
1909        if self._sb_ignore:
1910            self._sb_ignore = False
1911            return
1912
1913        if not self.GetShowScrollbars():
1914            return
1915
1916        self._adjustingSB = True
1917        needScrollbars = False
1918
1919        # horizontal scrollbar
1920        r_current = self._getXCurrentRange()
1921        r_max = list(self._getXMaxRange())
1922        sbfullrange = float(self.sb_hor.GetRange())
1923
1924        r_max[0] = min(r_max[0], r_current[0])
1925        r_max[1] = max(r_max[1], r_current[1])
1926
1927        self._sb_xfullrange = r_max
1928
1929        unit = (r_max[1] - r_max[0]) / float(self.sb_hor.GetRange())
1930        pos = int((r_current[0] - r_max[0]) / unit)
1931
1932        if pos >= 0:
1933            pagesize = int((r_current[1] - r_current[0]) / unit)
1934
1935            self.sb_hor.SetScrollbar(pos, pagesize, sbfullrange, pagesize)
1936            self._sb_xunit = unit
1937            needScrollbars = needScrollbars or (pagesize != sbfullrange)
1938        else:
1939            self.sb_hor.SetScrollbar(0, 1000, 1000, 1000)
1940
1941        # vertical scrollbar
1942        r_current = self._getYCurrentRange()
1943        r_max = list(self._getYMaxRange())
1944        sbfullrange = float(self.sb_vert.GetRange())
1945
1946        r_max[0] = min(r_max[0], r_current[0])
1947        r_max[1] = max(r_max[1], r_current[1])
1948
1949        self._sb_yfullrange = r_max
1950
1951        unit = (r_max[1] - r_max[0]) / sbfullrange
1952        pos = int((r_current[0] - r_max[0]) / unit)
1953
1954        if pos >= 0:
1955            pagesize = int((r_current[1] - r_current[0]) / unit)
1956            pos = (sbfullrange - 1 - pos - pagesize)
1957            self.sb_vert.SetScrollbar(pos, pagesize, sbfullrange, pagesize)
1958            self._sb_yunit = unit
1959            needScrollbars = needScrollbars or (pagesize != sbfullrange)
1960        else:
1961            self.sb_vert.SetScrollbar(0, 1000, 1000, 1000)
1962
1963        self.SetShowScrollbars(needScrollbars)
1964        self._adjustingSB = False
1965
1966#-------------------------------------------------------------------------
1967# Used to layout the printer page
1968
1969
1970class PlotPrintout(wx.Printout):
1971
1972    """Controls how the plot is made in printing and previewing"""
1973    # Do not change method names in this class,
1974    # we have to override wx.Printout methods here!
1975
1976    def __init__(self, graph):
1977        """graph is instance of plotCanvas to be printed or previewed"""
1978        wx.Printout.__init__(self)
1979        self.graph = graph
1980
1981    def HasPage(self, page):
1982        if page == 1:
1983            return True
1984        else:
1985            return False
1986
1987    def GetPageInfo(self):
1988        return (1, 1, 1, 1)  # disable page numbers
1989
1990    def OnPrintPage(self, page):
1991        dc = self.GetDC()  # allows using floats for certain functions
1992##        print("PPI Printer",self.GetPPIPrinter())
1993##        print("PPI Screen", self.GetPPIScreen())
1994##        print("DC GetSize", dc.GetSize())
1995##        print("GetPageSizePixels", self.GetPageSizePixels())
1996        # Note PPIScreen does not give the correct number
1997        # Calulate everything for printer and then scale for preview
1998        PPIPrinter = self.GetPPIPrinter()        # printer dots/inch (w,h)
1999        # PPIScreen= self.GetPPIScreen()          # screen dots/inch (w,h)
2000        dcSize = dc.GetSize()                    # DC size
2001        if self.graph._antiAliasingEnabled and not isinstance(dc, wx.GCDC):
2002            try:
2003                dc = wx.GCDC(dc)
2004            except Exception:
2005                pass
2006            else:
2007                if self.graph._hiResEnabled:
2008                    # high precision - each logical unit is 1/20 of a point
2009                    dc.SetMapMode(wx.MM_TWIPS)
2010        pageSize = self.GetPageSizePixels()  # page size in terms of pixcels
2011        clientDcSize = self.graph.GetClientSize()
2012
2013        # find what the margins are (mm)
2014        margLeftSize, margTopSize = self.graph.pageSetupData.GetMarginTopLeft()
2015        margRightSize, margBottomSize = self.graph.pageSetupData.GetMarginBottomRight()
2016
2017        # calculate offset and scale for dc
2018        pixLeft = margLeftSize * PPIPrinter[0] / 25.4  # mm*(dots/in)/(mm/in)
2019        pixRight = margRightSize * PPIPrinter[0] / 25.4
2020        pixTop = margTopSize * PPIPrinter[1] / 25.4
2021        pixBottom = margBottomSize * PPIPrinter[1] / 25.4
2022
2023        plotAreaW = pageSize[0] - (pixLeft + pixRight)
2024        plotAreaH = pageSize[1] - (pixTop + pixBottom)
2025
2026        # ratio offset and scale to screen size if preview
2027        if self.IsPreview():
2028            ratioW = float(dcSize[0]) / pageSize[0]
2029            ratioH = float(dcSize[1]) / pageSize[1]
2030            pixLeft *= ratioW
2031            pixTop *= ratioH
2032            plotAreaW *= ratioW
2033            plotAreaH *= ratioH
2034
2035        # rescale plot to page or preview plot area
2036        self.graph._setSize(plotAreaW, plotAreaH)
2037
2038        # Set offset and scale
2039        dc.SetDeviceOrigin(pixLeft, pixTop)
2040
2041        # Thicken up pens and increase marker size for printing
2042        ratioW = float(plotAreaW) / clientDcSize[0]
2043        ratioH = float(plotAreaH) / clientDcSize[1]
2044        aveScale = (ratioW + ratioH) / 2
2045        if self.graph._antiAliasingEnabled and not self.IsPreview():
2046            scale = dc.GetUserScale()
2047            dc.SetUserScale(
2048                scale[0] / self.graph._pointSize[0], scale[1] / self.graph._pointSize[1])
2049        self.graph._setPrinterScale(aveScale)  # tickens up pens for printing
2050
2051        self.graph._printDraw(dc)
2052        # rescale back to original
2053        self.graph._setSize()
2054        self.graph._setPrinterScale(1)
2055        self.graph.Redraw()  # to get point label scale and shift correct
2056
2057        return True
2058
2059
2060#----------------------------------------------------------------------
2061from wx.lib.embeddedimage import PyEmbeddedImage
2062
2063MagPlus = PyEmbeddedImage(
2064    "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAOFJ"
2065    "REFUeJy1VdEOxCAIo27//8XbuKfuPASGZ0Zisoi2FJABbZM3bY8c13lo5GvbjioBPAUEB0Yc"
2066    "VZ0iGRRc56Ee8DcikEgrJD8EFpzRegQASiRtBtzuA0hrdRPYQxaEKyJPG6IHyiK3xnNZvUSS"
2067    "NvUuzgYh0il4y14nCFPk5XgmNbRbQbVotGo9msj47G3UXJ7fuz8Q8FAGEu0/PbZh2D3NoshU"
2068    "1VUydBGVZKMimlGeErdNGUmf/x7YpjMjcf8HVYvS2adr6aFVlCy/5Ijk9q8SeCR9isJR8SeJ"
2069    "8pv7S0Wu2Acr0qdj3w7DRAAAAABJRU5ErkJggg==")
2070
2071GrabHand = PyEmbeddedImage(
2072    "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAARFJ"
2073    "REFUeJy1VdESgzAIS2j//4s3s5fRQ6Rad5M7H0oxCZhWSpK1TjwUBCBJAIBItL1fijlfe1yJ"
2074    "8noCGC9KgrXO7f0SyZEDAF/H2opsAHv9V/548nplT5Jo7YAFQKQ1RMWzmHUS96suqdBrHkuV"
2075    "uxpdJjCS8CfGXWdJ2glzcquKSR5c46QOtCpgNyIHj6oieAXg3282QvMX45hy8a8H0VonJZUO"
2076    "clesjOPg/dhBTq64o1Kacz4Ri2x5RKsf8+wcWQaJJL+A+xRcZHeQeBKjK+5EFiVJ4xy4x2Mn"
2077    "1Vk4U5/DWmfPieiqbye7a3tV/cCsWKu76K76KUFFchVnhigJ/hmktelm/m3e3b8k+Ec8PqLH"
2078    "CT4JRfyK9o1xYwAAAABJRU5ErkJggg==")
2079
2080Hand = PyEmbeddedImage(
2081    "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAARBJ"
2082    "REFUeJytluECwiAIhDn1/Z942/UnjCGoq+6XNeWDC1xAqbKr6zyo61Ibds60J8GBT0yS3IEM"
2083    "ABuIpJTa4IOLiAAQksuKyixLH1ShHgTgZl8KiALxOsODPoEMkgJ25Su6zoO3ZrjRnI96OLIq"
2084    "k7dsqOCboDa4XV/nwQEQVeFtmMnvbSJja+oagKBUaLn9hzd7VipRa9ostIv0O1uhzzaqNJxk"
2085    "hViwDVxqg51kksMg9r2rDDIFwHCap130FBhdMzeAfWg//6Ki5WWQrHSv6EIUeVs0g3wT3J7r"
2086    "FmWQp/JJDXeRh2TXcJa91zAH2uN2mvXFsrIrsjS8rnftWmWfAiLIStuD9m9h9belvzgS/1fP"
2087    "X7075IwDENteAAAAAElFTkSuQmCC")
2088
2089#---------------------------------------------------------------------------
2090# if running standalone...
2091#
2092#     ...a sample implementation using the above
2093#
2094
2095
2096def _draw1Objects():
2097    # 100 points sin function, plotted as green circles
2098    data1 = 2. * np.pi * np.arange(200) / 200.
2099    data1.shape = (100, 2)
2100    data1[:, 1] = np.sin(data1[:, 0])
2101    markers1 = PolyMarker(
2102        data1, legend='Green Markers', colour='green', marker='circle', size=1)
2103
2104    # 50 points cos function, plotted as red line
2105    data1 = 2. * np.pi * np.arange(100) / 100.
2106    data1.shape = (50, 2)
2107    data1[:, 1] = np.cos(data1[:, 0])
2108    lines = PolySpline(data1, legend='Red Line', colour='red')
2109
2110    # A few more points...
2111    pi = np.pi
2112    markers2 = PolyMarker([(0., 0.), (pi / 4., 1.), (pi / 2, 0.),
2113                           (3. * pi / 4., -1)], legend='Cross Legend', colour='blue',
2114                          marker='cross')
2115
2116    return PlotGraphics([markers1, lines, markers2], "Graph Title", "X Axis", "Y Axis")
2117
2118
2119def _draw2Objects():
2120    # 100 points sin function, plotted as green dots
2121    data1 = 2. * np.pi * np.arange(200) / 200.
2122    data1.shape = (100, 2)
2123    data1[:, 1] = np.sin(data1[:, 0])
2124    line1 = PolySpline(
2125        data1, legend='Green Line', colour='green', width=6, style=wx.PENSTYLE_DOT)
2126
2127    # 50 points cos function, plotted as red dot-dash
2128    data1 = 2. * np.pi * np.arange(100) / 100.
2129    data1.shape = (50, 2)
2130    data1[:, 1] = np.cos(data1[:, 0])
2131    line2 = PolySpline(
2132        data1, legend='Red Line', colour='red', width=3, style=wx.PENSTYLE_DOT_DASH)
2133
2134    # A few more points...
2135    pi = np.pi
2136    markers1 = PolyMarker([(0., 0.), (pi / 4., 1.), (pi / 2, 0.),
2137                           (3. * pi / 4., -1)], legend='Cross Hatch Square', colour='blue', width=3, size=6,
2138                          fillcolour='red', fillstyle=wx.CROSSDIAG_HATCH,
2139                          marker='square')
2140
2141    return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
2142
2143
2144def _draw3Objects():
2145    markerList = ['circle', 'dot', 'square', 'triangle', 'triangle_down',
2146                  'cross', 'plus', 'circle']
2147    m = []
2148    for i in range(len(markerList)):
2149        m.append(PolyMarker([(2 * i + .5, i + .5)], legend=markerList[i], colour='blue',
2150                            marker=markerList[i]))
2151    return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
2152
2153
2154def _draw4Objects():
2155    # 25,000 point line
2156    data1 = np.arange(5e5, 1e6, 10)
2157    data1.shape = (25000, 2)
2158    line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
2159
2160    # A few more points...
2161    markers2 = PolyMarker(data1, legend='Square', colour='blue',
2162                          marker='square')
2163    return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
2164
2165
2166def _draw5Objects():
2167    # Empty graph with axis defined but no points/lines
2168    points = []
2169    line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
2170    return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
2171
2172
2173def _draw6Objects():
2174    # Bar graph
2175    points1 = [(1, 0), (1, 10)]
2176    line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
2177    points1g = [(2, 0), (2, 4)]
2178    line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
2179    points1b = [(3, 0), (3, 6)]
2180    line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
2181
2182    points2 = [(4, 0), (4, 12)]
2183    line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
2184    points2g = [(5, 0), (5, 8)]
2185    line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
2186    points2b = [(6, 0), (6, 4)]
2187    line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
2188
2189    return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
2190                        "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
2191
2192
2193def _draw7Objects():
2194    # Empty graph with axis defined but no points/lines
2195    x = np.arange(1, 1000, 1)
2196    y1 = 4.5 * x ** 2
2197    y2 = 2.2 * x ** 3
2198    points1 = np.transpose([x, y1])
2199    points2 = np.transpose([x, y2])
2200    line1 = PolyLine(points1, legend='quadratic', colour='blue', width=1)
2201    line2 = PolyLine(points2, legend='cubic', colour='red', width=1)
2202    return PlotGraphics([line1, line2], "double log plot", "Value X", "Value Y")
2203
2204
2205class TestFrame(wx.Frame):
2206
2207    def __init__(self, parent, id, title):
2208        wx.Frame.__init__(self, parent, id, title,
2209                          wx.DefaultPosition, (600, 400))
2210
2211        # Now Create the menu bar and items
2212        self.mainmenu = wx.MenuBar()
2213
2214        menu = wx.Menu()
2215        menu.Append(200, 'Page Setup...', 'Setup the printer page')
2216        self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
2217
2218        menu.Append(201, 'Print Preview...', 'Show the current plot on page')
2219        self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
2220
2221        menu.Append(202, 'Print...', 'Print the current plot')
2222        self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
2223
2224        menu.Append(203, 'Save Plot...', 'Save current plot')
2225        self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
2226
2227        menu.Append(205, 'E&xit', 'Enough of this already!')
2228        self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
2229        self.mainmenu.Append(menu, '&File')
2230
2231        menu = wx.Menu()
2232        menu.Append(206, 'Draw1', 'Draw plots1')
2233        self.Bind(wx.EVT_MENU, self.OnPlotDraw1, id=206)
2234        menu.Append(207, 'Draw2', 'Draw plots2')
2235        self.Bind(wx.EVT_MENU, self.OnPlotDraw2, id=207)
2236        menu.Append(208, 'Draw3', 'Draw plots3')
2237        self.Bind(wx.EVT_MENU, self.OnPlotDraw3, id=208)
2238        menu.Append(209, 'Draw4', 'Draw plots4')
2239        self.Bind(wx.EVT_MENU, self.OnPlotDraw4, id=209)
2240        menu.Append(210, 'Draw5', 'Draw plots5')
2241        self.Bind(wx.EVT_MENU, self.OnPlotDraw5, id=210)
2242        menu.Append(260, 'Draw6', 'Draw plots6')
2243        self.Bind(wx.EVT_MENU, self.OnPlotDraw6, id=260)
2244        menu.Append(261, 'Draw7', 'Draw plots7')
2245        self.Bind(wx.EVT_MENU, self.OnPlotDraw7, id=261)
2246
2247        menu.Append(211, '&Redraw', 'Redraw plots')
2248        self.Bind(wx.EVT_MENU, self.OnPlotRedraw, id=211)
2249        menu.Append(212, '&Clear', 'Clear canvas')
2250        self.Bind(wx.EVT_MENU, self.OnPlotClear, id=212)
2251        menu.Append(213, '&Scale', 'Scale canvas')
2252        self.Bind(wx.EVT_MENU, self.OnPlotScale, id=213)
2253        menu.Append(
2254            214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
2255        self.Bind(wx.EVT_MENU, self.OnEnableZoom, id=214)
2256        menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
2257        self.Bind(wx.EVT_MENU, self.OnEnableGrid, id=215)
2258        menu.Append(
2259            217, 'Enable &Drag', 'Activates dragging mode', kind=wx.ITEM_CHECK)
2260        self.Bind(wx.EVT_MENU, self.OnEnableDrag, id=217)
2261        menu.Append(
2262            220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
2263        self.Bind(wx.EVT_MENU, self.OnEnableLegend, id=220)
2264        menu.Append(
2265            222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
2266        self.Bind(wx.EVT_MENU, self.OnEnablePointLabel, id=222)
2267
2268        menu.Append(
2269            223, 'Enable &Anti-Aliasing', 'Smooth output', kind=wx.ITEM_CHECK)
2270        self.Bind(wx.EVT_MENU, self.OnEnableAntiAliasing, id=223)
2271        menu.Append(224, 'Enable &High-Resolution AA',
2272                    'Draw in higher resolution', kind=wx.ITEM_CHECK)
2273        self.Bind(wx.EVT_MENU, self.OnEnableHiRes, id=224)
2274
2275        menu.Append(
2276            226, 'Enable Center Lines', 'Draw center lines', kind=wx.ITEM_CHECK)
2277        self.Bind(wx.EVT_MENU, self.OnEnableCenterLines, id=226)
2278        menu.Append(
2279            227, 'Enable Diagonal Lines', 'Draw diagonal lines', kind=wx.ITEM_CHECK)
2280        self.Bind(wx.EVT_MENU, self.OnEnableDiagonals, id=227)
2281
2282        menu.Append(
2283            231, 'Set Gray Background', 'Change background colour to gray')
2284        self.Bind(wx.EVT_MENU, self.OnBackgroundGray, id=231)
2285        menu.Append(
2286            232, 'Set &White Background', 'Change background colour to white')
2287        self.Bind(wx.EVT_MENU, self.OnBackgroundWhite, id=232)
2288        menu.Append(
2289            233, 'Set Red Label Text', 'Change label text colour to red')
2290        self.Bind(wx.EVT_MENU, self.OnForegroundRed, id=233)
2291        menu.Append(
2292            234, 'Set &Black Label Text', 'Change label text colour to black')
2293        self.Bind(wx.EVT_MENU, self.OnForegroundBlack, id=234)
2294
2295        menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
2296        self.Bind(wx.EVT_MENU, self.OnScrUp, id=225)
2297        menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
2298        self.Bind(wx.EVT_MENU, self.OnScrRt, id=230)
2299        menu.Append(235, '&Plot Reset', 'Reset to original plot')
2300        self.Bind(wx.EVT_MENU, self.OnReset, id=235)
2301
2302        self.mainmenu.Append(menu, '&Plot')
2303
2304        menu = wx.Menu()
2305        menu.Append(300, '&About', 'About this thing...')
2306        self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
2307        self.mainmenu.Append(menu, '&Help')
2308
2309        self.SetMenuBar(self.mainmenu)
2310
2311        # A status bar to tell people what's happening
2312        self.CreateStatusBar(1)
2313
2314        self.client = PlotCanvas(self)
2315        # define the function for drawing pointLabels
2316        self.client.SetPointLabelFunc(self.DrawPointLabel)
2317        # Create mouse event for showing cursor coords in status bar
2318        self.client.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
2319        # Show closest point when enabled
2320        self.client.canvas.Bind(wx.EVT_MOTION, self.OnMotion)
2321
2322        self.Show(True)
2323
2324    def DrawPointLabel(self, dc, mDataDict):
2325        """This is the fuction that defines how the pointLabels are plotted
2326            dc - DC that will be passed
2327            mDataDict - Dictionary of data that you want to use for the pointLabel
2328
2329            As an example I have decided I want a box at the curve point
2330            with some text information about the curve plotted below.
2331            Any wxDC method can be used.
2332        """
2333        # ----------
2334        dc.SetPen(wx.Pen(wx.BLACK))
2335        dc.SetBrush(wx.Brush(wx.BLACK, wx.BRUSHSTYLE_SOLID))
2336
2337        sx, sy = mDataDict["scaledXY"]  # scaled x,y of closest point
2338        # 10by10 square centered on point
2339        dc.DrawRectangle(sx - 5, sy - 5, 10, 10)
2340        px, py = mDataDict["pointXY"]
2341        cNum = mDataDict["curveNum"]
2342        pntIn = mDataDict["pIndex"]
2343        legend = mDataDict["legend"]
2344        # make a string to display
2345        s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" % (
2346            cNum, legend, px, py, pntIn)
2347        dc.DrawText(s, sx, sy + 1)
2348        # -----------
2349
2350    def OnMouseLeftDown(self, event):
2351        s = "Left Mouse Down at Point: (%.4f, %.4f)" % self.client._getXY(
2352            event)
2353        self.SetStatusText(s)
2354        event.Skip()  # allows plotCanvas OnMouseLeftDown to be called
2355
2356    def OnMotion(self, event):
2357        # show closest point (when enbled)
2358        if self.client.GetEnablePointLabel() == True:
2359            # make up dict with info for the pointLabel
2360            # I've decided to mark the closest point on the closest curve
2361            dlst = self.client.GetClosestPoint(
2362                self.client._getXY(event), pointScaled=True)
2363            if dlst != []:  # returns [] if none
2364                curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
2365                # make up dictionary to pass to my user function (see
2366                # DrawPointLabel)
2367                mDataDict = {"curveNum": curveNum, "legend": legend, "pIndex": pIndex,
2368                             "pointXY": pointXY, "scaledXY": scaledXY}
2369                # pass dict to update the pointLabel
2370                self.client.UpdatePointLabel(mDataDict)
2371        event.Skip()  # go to next handler
2372
2373    def OnFilePageSetup(self, event):
2374        self.client.PageSetup()
2375
2376    def OnFilePrintPreview(self, event):
2377        self.client.PrintPreview()
2378
2379    def OnFilePrint(self, event):
2380        self.client.Printout()
2381
2382    def OnSaveFile(self, event):
2383        self.client.SaveFile()
2384
2385    def OnFileExit(self, event):
2386        self.Close()
2387
2388    def OnPlotDraw1(self, event):
2389        self.resetDefaults()
2390        self.client.Draw(_draw1Objects())
2391
2392    def OnPlotDraw2(self, event):
2393        self.resetDefaults()
2394        self.client.Draw(_draw2Objects())
2395
2396    def OnPlotDraw3(self, event):
2397        self.resetDefaults()
2398        self.client.SetFont(
2399            wx.Font(10, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
2400        self.client.SetFontSizeAxis(20)
2401        self.client.SetFontSizeLegend(12)
2402        self.client.SetXSpec('min')
2403        self.client.SetYSpec('none')
2404        self.client.Draw(_draw3Objects())
2405
2406    def OnPlotDraw4(self, event):
2407        self.resetDefaults()
2408        drawObj = _draw4Objects()
2409        self.client.Draw(drawObj)
2410# profile
2411##        start = _time.clock()
2412# for x in range(10):
2413# self.client.Draw(drawObj)
2414##        print("10 plots of Draw4 took: %f sec."%(_time.clock() - start))
2415# profile end
2416
2417    def OnPlotDraw5(self, event):
2418        # Empty plot with just axes
2419        self.resetDefaults()
2420        drawObj = _draw5Objects()
2421        # make the axis X= (0,5), Y=(0,10)
2422        # (default with None is X= (-1,1), Y= (-1,1))
2423        self.client.Draw(drawObj, xAxis=(0, 5), yAxis= (0, 10))
2424
2425    def OnPlotDraw6(self, event):
2426        # Bar Graph Example
2427        self.resetDefaults()
2428        # self.client.SetEnableLegend(True)   #turn on Legend
2429        # self.client.SetEnableGrid(True)     #turn on Grid
2430        self.client.SetXSpec('none')  # turns off x-axis scale
2431        self.client.SetYSpec('auto')
2432        self.client.Draw(_draw6Objects(), xAxis=(0, 7))
2433
2434    def OnPlotDraw7(self, event):
2435        # log scale example
2436        self.resetDefaults()
2437        self.client.setLogScale((True, True))
2438        self.client.Draw(_draw7Objects())
2439
2440    def OnPlotRedraw(self, event):
2441        self.client.Redraw()
2442
2443    def OnPlotClear(self, event):
2444        self.client.Clear()
2445
2446    def OnPlotScale(self, event):
2447        if self.client.last_draw != None:
2448            graphics, xAxis, yAxis = self.client.last_draw
2449            self.client.Draw(graphics, (1, 3.05), (0, 1))
2450
2451    def OnEnableZoom(self, event):
2452        self.client.SetEnableZoom(event.IsChecked())
2453        self.mainmenu.Check(217, not event.IsChecked())
2454
2455    def OnEnableGrid(self, event):
2456        self.client.SetEnableGrid(event.IsChecked())
2457
2458    def OnEnableDrag(self, event):
2459        self.client.SetEnableDrag(event.IsChecked())
2460        self.mainmenu.Check(214, not event.IsChecked())
2461
2462    def OnEnableLegend(self, event):
2463        self.client.SetEnableLegend(event.IsChecked())
2464
2465    def OnEnablePointLabel(self, event):
2466        self.client.SetEnablePointLabel(event.IsChecked())
2467
2468    def OnEnableAntiAliasing(self, event):
2469        self.client.SetEnableAntiAliasing(event.IsChecked())
2470
2471    def OnEnableHiRes(self, event):
2472        self.client.SetEnableHiRes(event.IsChecked())
2473
2474    def OnEnableCenterLines(self, event):
2475        self.client.SetEnableCenterLines(event.IsChecked())
2476
2477    def OnEnableDiagonals(self, event):
2478        self.client.SetEnableDiagonals(event.IsChecked())
2479
2480    def OnBackgroundGray(self, event):
2481        self.client.SetBackgroundColour("#CCCCCC")
2482        self.client.Redraw()
2483
2484    def OnBackgroundWhite(self, event):
2485        self.client.SetBackgroundColour("white")
2486        self.client.Redraw()
2487
2488    def OnForegroundRed(self, event):
2489        self.client.SetForegroundColour("red")
2490        self.client.Redraw()
2491
2492    def OnForegroundBlack(self, event):
2493        self.client.SetForegroundColour("black")
2494        self.client.Redraw()
2495
2496    def OnScrUp(self, event):
2497        self.client.ScrollUp(1)
2498
2499    def OnScrRt(self, event):
2500        self.client.ScrollRight(2)
2501
2502    def OnReset(self, event):
2503        self.client.Reset()
2504
2505    def OnHelpAbout(self, event):
2506        from wx.lib.dialogs import ScrolledMessageDialog
2507        about = ScrolledMessageDialog(self, __doc__, "About...")
2508        about.ShowModal()
2509
2510    def resetDefaults(self):
2511        """Just to reset the fonts back to the PlotCanvas defaults"""
2512        self.client.SetFont(
2513            wx.Font(10, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
2514        self.client.SetFontSizeAxis(10)
2515        self.client.SetFontSizeLegend(7)
2516        self.client.setLogScale((False, False))
2517        self.client.SetXSpec('auto')
2518        self.client.SetYSpec('auto')
2519
2520
2521def __test():
2522
2523    class MyApp(wx.App):
2524
2525        def OnInit(self):
2526            wx.InitAllImageHandlers()
2527            frame = TestFrame(None, -1, "PlotCanvas")
2528            # frame.Show(True)
2529            self.SetTopWindow(frame)
2530            return True
2531
2532    app = MyApp(0)
2533    app.MainLoop()
2534
2535if __name__ == '__main__':
2536    __test()