1# -*- coding: utf-8 -*-
2# pylint: disable=E1101, C0330, C0103
3#   E1101: Module X has no Y member
4#   C0330: Wrong continued indentation
5#   C0103: Invalid attribute/variable/method name
6"""
7polyobjects.py
8==============
9
10This contains all of the PolyXXX objects used by :mod:`wx.lib.plot`.
11
12"""
13__docformat__ = "restructuredtext en"
14
15# Standard Library
16import time as _time
17import wx
18import warnings
19from collections import namedtuple
20
21# Third-Party
22try:
23    import numpy as np
24except:
25    msg = """
26    This module requires the NumPy module, which could not be
27    imported.  It probably is not installed (it's not part of the
28    standard Python distribution). See the Numeric Python site
29    (http://numpy.scipy.org) for information on downloading source or
30    binaries."""
31    raise ImportError("NumPy not found.\n" + msg)
32
33# Package
34from .utils import pendingDeprecation
35from .utils import TempStyle
36from .utils import pairwise
37
38
39class PolyPoints(object):
40    """
41    Base Class for lines and markers.
42
43    :param points: The points to plot
44    :type points: list of ``(x, y)`` pairs
45    :param attr: Additional attributes
46    :type attr: dict
47
48    .. warning::
49       All methods are private.
50    """
51
52    def __init__(self, points, attr):
53        self._points = np.array(points).astype(np.float64)
54        self._logscale = (False, False)
55        self._absScale = (False, False)
56        self._symlogscale = (False, False)
57        self._pointSize = (1.0, 1.0)
58        self.currentScale = (1, 1)
59        self.currentShift = (0, 0)
60        self.scaled = self.points
61        self.attributes = {}
62        self.attributes.update(self._attributes)
63        for name, value in attr.items():
64            if name not in self._attributes.keys():
65                err_txt = "Style attribute incorrect. Should be one of {}"
66                raise KeyError(err_txt.format(self._attributes.keys()))
67            self.attributes[name] = value
68
69    @property
70    def logScale(self):
71        """
72        A tuple of ``(x_axis_is_log10, y_axis_is_log10)`` booleans. If a value
73        is ``True``, then that axis is plotted on a logarithmic base 10 scale.
74
75        :getter: Returns the current value of logScale
76        :setter: Sets the value of logScale
77        :type: tuple of bool, length 2
78        :raises ValueError: when setting an invalid value
79        """
80        return self._logscale
81
82    @logScale.setter
83    def logScale(self, logscale):
84        if not isinstance(logscale, tuple) or len(logscale) != 2:
85            raise ValueError("`logscale` must be a 2-tuple of bools")
86        self._logscale = logscale
87
88    def setLogScale(self, logscale):
89        """
90        Set to change the axes to plot Log10(values)
91
92        Value must be a tuple of booleans (x_axis_bool, y_axis_bool)
93
94        .. deprecated:: Feb 27, 2016
95
96           Use the :attr:`~wx.lib.plot.polyobjects.PolyPoints.logScale`
97           property instead.
98        """
99        pendingDeprecation("self.logScale property")
100        self._logscale = logscale
101
102    @property
103    def symLogScale(self):
104        """
105        .. warning::
106
107           Not yet implemented.
108
109        A tuple of ``(x_axis_is_SymLog10, y_axis_is_SymLog10)`` booleans.
110        If a value is ``True``, then that axis is plotted on a symmetric
111        logarithmic base 10 scale.
112
113        A Symmetric Log10 scale means that values can be positive and
114        negative. Any values less than
115        :attr:`~wx.lig.plot.PolyPoints.symLogThresh` will be plotted on
116        a linear scale to avoid the plot going to infinity near 0.
117
118        :getter: Returns the current value of symLogScale
119        :setter: Sets the value of symLogScale
120        :type: tuple of bool, length 2
121        :raises ValueError: when setting an invalid value
122
123        .. notes::
124
125           This is a simplified example of how SymLog works::
126
127             if x >= thresh:
128                 x = Log10(x)
129             elif x =< thresh:
130                 x = -Log10(Abs(x))
131             else:
132                 x = x
133
134        .. seealso::
135
136           + :attr:`~wx.lib.plot.PolyPoints.symLogThresh`
137           + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html
138             for an example.
139        """
140        return self._symlogscale
141
142    # TODO: Implement symmetric log scale
143    @symLogScale.setter
144    def symLogScale(self, symlogscale, thresh):
145        raise NotImplementedError("Symmetric Log Scale not yet implemented")
146
147        if not isinstance(symlogscale, tuple) or len(symlogscale) != 2:
148            raise ValueError("`symlogscale` must be a 2-tuple of bools")
149        self._symlogscale = symlogscale
150
151    @property
152    def symLogThresh(self):
153        """
154        .. warning::
155
156           Not yet implemented.
157
158        A tuple of ``(x_thresh, y_thresh)`` floats that define where the plot
159        changes to linear scale when using a symmetric log scale.
160
161        :getter: Returns the current value of symLogThresh
162        :setter: Sets the value of symLogThresh
163        :type: tuple of float, length 2
164        :raises ValueError: when setting an invalid value
165
166        .. notes::
167
168           This is a simplified example of how SymLog works::
169
170             if x >= thresh:
171                 x = Log10(x)
172             elif x =< thresh:
173                 x = -Log10(Abs(x))
174             else:
175                 x = x
176
177        .. seealso::
178
179           + :attr:`~wx.lib.plot.PolyPoints.symLogScale`
180           + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html
181             for an example.
182        """
183        return self._symlogscale
184
185    # TODO: Implement symmetric log scale threshold
186    @symLogThresh.setter
187    def symLogThresh(self, symlogscale, thresh):
188        raise NotImplementedError("Symmetric Log Scale not yet implemented")
189
190        if not isinstance(symlogscale, tuple) or len(symlogscale) != 2:
191            raise ValueError("`symlogscale` must be a 2-tuple of bools")
192        self._symlogscale = symlogscale
193
194    @property
195    def absScale(self):
196        """
197        A tuple of ``(x_axis_is_abs, y_axis_is_abs)`` booleans. If a value
198        is ``True``, then that axis is plotted on an absolute value scale.
199
200        :getter: Returns the current value of absScale
201        :setter: Sets the value of absScale
202        :type: tuple of bool, length 2
203        :raises ValueError: when setting an invalid value
204        """
205        return self._absScale
206
207    @absScale.setter
208    def absScale(self, absscale):
209
210        if not isinstance(absscale, tuple) and len(absscale) == 2:
211            raise ValueError("`absscale` must be a 2-tuple of bools")
212        self._absScale = absscale
213
214    @property
215    def points(self):
216        """
217        Get or set the plotted points.
218
219        :getter: Returns the current value of points, adjusting for the
220                 various scale options such as Log, Abs, or SymLog.
221        :setter: Sets the value of points.
222        :type: list of `(x, y)` pairs
223
224        .. Note::
225
226           Only set unscaled points - do not perform the log, abs, or symlog
227           adjustments yourself.
228        """
229        data = np.array(self._points, copy=True)    # need the copy
230                                                    # TODO: get rid of the
231                                                    # need for copy
232
233        # work on X:
234        if self.absScale[0]:
235            data = self._abs(data, 0)
236        if self.logScale[0]:
237            data = self._log10(data, 0)
238
239        if self.symLogScale[0]:
240            # TODO: implement symLogScale
241            # Should symLogScale override absScale? My vote is no.
242            # Should symLogScale override logScale? My vote is yes.
243            #   - symLogScale could be a parameter passed to logScale...
244            pass
245
246        # work on Y:
247        if self.absScale[1]:
248            data = self._abs(data, 1)
249        if self.logScale[1]:
250            data = self._log10(data, 1)
251
252        if self.symLogScale[1]:
253            # TODO: implement symLogScale
254            pass
255
256        return data
257
258    @points.setter
259    def points(self, points):
260        self._points = points
261
262    def _log10(self, data, index):
263        """ Take the Log10 of the data, dropping any negative values """
264        data = np.compress(data[:, index] > 0, data, 0)
265        data[:, index] = np.log10(data[:, index])
266        return data
267
268    def _abs(self, data, index):
269        """ Take the Abs of the data """
270        data[:, index] = np.abs(data[:, index])
271        return data
272
273    def boundingBox(self):
274        """
275        Returns the bouding box for the entire dataset as a tuple with this
276        format::
277
278            ((minX, minY), (maxX, maxY))
279
280        :returns: boundingbox
281        :rtype: numpy array of ``[[minX, minY], [maxX, maxY]]``
282        """
283        if len(self.points) == 0:
284            # no curves to draw
285            # defaults to (-1,-1) and (1,1) but axis can be set in Draw
286            minXY = np.array([-1.0, -1.0])
287            maxXY = np.array([1.0, 1.0])
288        else:
289            minXY = np.minimum.reduce(self.points)
290            maxXY = np.maximum.reduce(self.points)
291        return minXY, maxXY
292
293    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
294        """
295        Scales and shifts the data for plotting.
296
297        :param scale: The values to scale the data by.
298        :type scale: list of floats: ``[x_scale, y_scale]``
299        :param shift: The value to shift the data by. This should be in scaled
300                      units
301        :type shift: list of floats: ``[x_shift, y_shift]``
302        :returns: None
303        """
304        if len(self.points) == 0:
305            # no curves to draw
306            return
307
308        # TODO: Can we remove the if statement alltogether? Does
309        #       scaleAndShift ever get called when the current value equals
310        #       the new value?
311
312        # cast everything to list: some might be np.ndarray objects
313        if (list(scale) != list(self.currentScale)
314                or list(shift) != list(self.currentShift)):
315            # update point scaling
316            self.scaled = scale * self.points + shift
317            self.currentScale = scale
318            self.currentShift = shift
319        # else unchanged use the current scaling
320
321    def getLegend(self):
322        return self.attributes['legend']
323
324    def getClosestPoint(self, pntXY, pointScaled=True):
325        """
326        Returns the index of closest point on the curve, pointXY,
327        scaledXY, distance x, y in user coords.
328
329        if pointScaled == True, then based on screen coords
330        if pointScaled == False, then based on user coords
331        """
332        if pointScaled:
333            # Using screen coords
334            p = self.scaled
335            pxy = self.currentScale * np.array(pntXY) + self.currentShift
336        else:
337            # Using user coords
338            p = self.points
339            pxy = np.array(pntXY)
340        # determine distance for each point
341        d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1))  # sqrt(dx^2+dy^2)
342        pntIndex = np.argmin(d)
343        dist = d[pntIndex]
344        return [pntIndex,
345                self.points[pntIndex],
346                self.scaled[pntIndex] / self._pointSize,
347                dist]
348
349
350class PolyLine(PolyPoints):
351    """
352    Creates PolyLine object
353
354    :param points: The points that make up the line
355    :type points: list of ``[x, y]`` values
356    :param **attr: keyword attributes
357
358    ===========================  =============  ====================
359    Keyword and Default          Description    Type
360    ===========================  =============  ====================
361    ``colour='black'``           Line color     :class:`wx.Colour`
362    ``width=1``                  Line width     float
363    ``style=wx.PENSTYLE_SOLID``  Line style     :class:`wx.PenStyle`
364    ``legend=''``                Legend string  str
365    ``drawstyle='line'``         see below      str
366    ===========================  =============  ====================
367
368    ==================  ==================================================
369    Draw style          Description
370    ==================  ==================================================
371    ``'line'``          Draws an straight line between consecutive points
372    ``'steps-pre'``     Draws a line down from point A and then right to
373                        point B
374    ``'steps-post'``    Draws a line right from point A and then down
375                        to point B
376    ``'steps-mid-x'``   Draws a line horizontally to half way between A
377                        and B, then draws a line vertically, then again
378                        horizontally to point B.
379    ``'steps-mid-y'``   Draws a line vertically to half way between A
380                        and B, then draws a line horizonatally, then
381                        again vertically to point B.
382                        *Note: This typically does not look very good*
383    ==================  ==================================================
384
385    .. warning::
386
387       All methods except ``__init__`` are private.
388    """
389
390    _attributes = {'colour': 'black',
391                   'width': 1,
392                   'style': wx.PENSTYLE_SOLID,
393                   'legend': '',
394                   'drawstyle': 'line',
395                   }
396    _drawstyles = ("line", "steps-pre", "steps-post",
397                   "steps-mid-x", "steps-mid-y")
398
399    def __init__(self, points, **attr):
400        PolyPoints.__init__(self, points, attr)
401
402    def draw(self, dc, printerScale, coord=None):
403        """
404        Draw the lines.
405
406        :param dc: The DC to draw on.
407        :type dc: :class:`wx.DC`
408        :param printerScale:
409        :type printerScale: float
410        :param coord: The legend coordinate?
411        :type coord: ???
412        """
413        colour = self.attributes['colour']
414        width = self.attributes['width'] * printerScale * self._pointSize[0]
415        style = self.attributes['style']
416        drawstyle = self.attributes['drawstyle']
417
418        if not isinstance(colour, wx.Colour):
419            colour = wx.Colour(colour)
420        pen = wx.Pen(colour, width, style)
421        pen.SetCap(wx.CAP_BUTT)
422        dc.SetPen(pen)
423        if coord is None:
424            if len(self.scaled):  # bugfix for Mac OS X
425                for c1, c2 in zip(self.scaled, self.scaled[1:]):
426                    self._path(dc, c1, c2, drawstyle)
427        else:
428            dc.DrawLines(coord)  # draw legend line
429
430    def getSymExtent(self, printerScale):
431        """
432        Get the Width and Height of the symbol.
433
434        :param printerScale:
435        :type printerScale: float
436        """
437        h = self.attributes['width'] * printerScale * self._pointSize[0]
438        w = 5 * h
439        return (w, h)
440
441    def _path(self, dc, coord1, coord2, drawstyle):
442        """
443        Calculates the path from coord1 to coord 2 along X and Y
444
445        :param dc: The DC to draw on.
446        :type dc: :class:`wx.DC`
447        :param coord1: The first coordinate in the coord pair
448        :type coord1: list, length 2: ``[x, y]``
449        :param coord2: The second coordinate in the coord pair
450        :type coord2: list, length 2: ``[x, y]``
451        :param drawstyle: The type of connector to use
452        :type drawstyle: str
453        """
454        if drawstyle == 'line':
455            # Straight line between points.
456            line = [coord1, coord2]
457        elif drawstyle == 'steps-pre':
458            # Up/down to next Y, then right to next X
459            intermediate = [coord1[0], coord2[1]]
460            line = [coord1, intermediate, coord2]
461        elif drawstyle == 'steps-post':
462            # Right to next X, then up/down to Y
463            intermediate = [coord2[0], coord1[1]]
464            line = [coord1, intermediate, coord2]
465        elif drawstyle == 'steps-mid-x':
466            # need 3 lines between points: right -> up/down -> right
467            mid_x = ((coord2[0] - coord1[0]) / 2) + coord1[0]
468            intermediate1 = [mid_x, coord1[1]]
469            intermediate2 = [mid_x, coord2[1]]
470            line = [coord1, intermediate1, intermediate2, coord2]
471        elif drawstyle == 'steps-mid-y':
472            # need 3 lines between points: up/down -> right -> up/down
473            mid_y = ((coord2[1] - coord1[1]) / 2) + coord1[1]
474            intermediate1 = [coord1[0], mid_y]
475            intermediate2 = [coord2[0], mid_y]
476            line = [coord1, intermediate1, intermediate2, coord2]
477        else:
478            err_txt = "Invalid drawstyle '{}'. Must be one of {}."
479            raise ValueError(err_txt.format(drawstyle, self._drawstyles))
480
481        dc.DrawLines(line)
482
483
484class PolySpline(PolyLine):
485    """
486    Creates PolySpline object
487
488    :param points: The points that make up the spline
489    :type points: list of ``[x, y]`` values
490    :param **attr: keyword attributes
491
492    ===========================  =============  ====================
493    Keyword and Default          Description    Type
494    ===========================  =============  ====================
495    ``colour='black'``           Line color     :class:`wx.Colour`
496    ``width=1``                  Line width     float
497    ``style=wx.PENSTYLE_SOLID``  Line style     :class:`wx.PenStyle`
498    ``legend=''``                Legend string  str
499    ===========================  =============  ====================
500
501    .. warning::
502
503       All methods except ``__init__`` are private.
504    """
505
506    _attributes = {'colour': 'black',
507                   'width': 1,
508                   'style': wx.PENSTYLE_SOLID,
509                   'legend': ''}
510
511    def __init__(self, points, **attr):
512        PolyLine.__init__(self, points, **attr)
513
514    def draw(self, dc, printerScale, coord=None):
515        """ Draw the spline """
516        colour = self.attributes['colour']
517        width = self.attributes['width'] * printerScale * self._pointSize[0]
518        style = self.attributes['style']
519        if not isinstance(colour, wx.Colour):
520            colour = wx.Colour(colour)
521        pen = wx.Pen(colour, width, style)
522        pen.SetCap(wx.CAP_ROUND)
523        dc.SetPen(pen)
524        if coord is None:
525            if len(self.scaled) >= 3:
526                dc.DrawSpline(self.scaled)
527        else:
528            dc.DrawLines(coord)  # draw legend line
529
530
531class PolyMarker(PolyPoints):
532    """
533    Creates a PolyMarker object.
534
535    :param points: The marker coordinates.
536    :type points: list of ``[x, y]`` values
537    :param **attr: keyword attributes
538
539    =================================  =============  ====================
540    Keyword and Default                Description    Type
541    =================================  =============  ====================
542    ``marker='circle'``                see below      str
543    ``size=2``                         Marker size    float
544    ``colour='black'``                 Outline color  :class:`wx.Colour`
545    ``width=1``                        Outline width  float
546    ``style=wx.PENSTYLE_SOLID``        Outline style  :class:`wx.PenStyle`
547    ``fillcolour=colour``              fill color     :class:`wx.Colour`
548    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
549    ``legend=''``                      Legend string  str
550    =================================  =============  ====================
551
552    ===================  ==================================
553    Marker               Description
554    ===================  ==================================
555    ``'circle'``         A circle of diameter ``size``
556    ``'dot'``            A dot. Does not have a size.
557    ``'square'``         A square with side length ``size``
558    ``'triangle'``       An upward-pointed triangle
559    ``'triangle_down'``  A downward-pointed triangle
560    ``'cross'``          An "X" shape
561    ``'plus'``           A "+" shape
562    ===================  ==================================
563
564    .. warning::
565
566       All methods except ``__init__`` are private.
567    """
568    _attributes = {'colour': 'black',
569                   'width': 1,
570                   'size': 2,
571                   'fillcolour': None,
572                   'fillstyle': wx.BRUSHSTYLE_SOLID,
573                   'marker': 'circle',
574                   'legend': ''}
575
576    def __init__(self, points, **attr):
577        PolyPoints.__init__(self, points, attr)
578
579    def draw(self, dc, printerScale, coord=None):
580        """ Draw the points """
581        colour = self.attributes['colour']
582        width = self.attributes['width'] * printerScale * self._pointSize[0]
583        size = self.attributes['size'] * printerScale * self._pointSize[0]
584        fillcolour = self.attributes['fillcolour']
585        fillstyle = self.attributes['fillstyle']
586        marker = self.attributes['marker']
587
588        if colour and not isinstance(colour, wx.Colour):
589            colour = wx.Colour(colour)
590        if fillcolour and not isinstance(fillcolour, wx.Colour):
591            fillcolour = wx.Colour(fillcolour)
592
593        dc.SetPen(wx.Pen(colour, width))
594        if fillcolour:
595            dc.SetBrush(wx.Brush(fillcolour, fillstyle))
596        else:
597            dc.SetBrush(wx.Brush(colour, fillstyle))
598        if coord is None:
599            if len(self.scaled):  # bugfix for Mac OS X
600                self._drawmarkers(dc, self.scaled, marker, size)
601        else:
602            self._drawmarkers(dc, coord, marker, size)  # draw legend marker
603
604    def getSymExtent(self, printerScale):
605        """Width and Height of Marker"""
606        s = 5 * self.attributes['size'] * printerScale * self._pointSize[0]
607        return (s, s)
608
609    def _drawmarkers(self, dc, coords, marker, size=1):
610        f = getattr(self, "_{}".format(marker))
611        f(dc, coords, size)
612
613    def _circle(self, dc, coords, size=1):
614        fact = 2.5 * size
615        wh = 5.0 * size
616        rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh]
617        rect[:, 0:2] = coords - [fact, fact]
618        dc.DrawEllipseList(rect.astype(np.int32))
619
620    def _dot(self, dc, coords, size=1):
621        dc.DrawPointList(coords)
622
623    def _square(self, dc, coords, size=1):
624        fact = 2.5 * size
625        wh = 5.0 * size
626        rect = np.zeros((len(coords), 4), np.float) + [0.0, 0.0, wh, wh]
627        rect[:, 0:2] = coords - [fact, fact]
628        dc.DrawRectangleList(rect.astype(np.int32))
629
630    def _triangle(self, dc, coords, size=1):
631        shape = [(-2.5 * size, 1.44 * size),
632                 (2.5 * size, 1.44 * size), (0.0, -2.88 * size)]
633        poly = np.repeat(coords, 3, 0)
634        poly.shape = (len(coords), 3, 2)
635        poly += shape
636        dc.DrawPolygonList(poly.astype(np.int32))
637
638    def _triangle_down(self, dc, coords, size=1):
639        shape = [(-2.5 * size, -1.44 * size),
640                 (2.5 * size, -1.44 * size), (0.0, 2.88 * size)]
641        poly = np.repeat(coords, 3, 0)
642        poly.shape = (len(coords), 3, 2)
643        poly += shape
644        dc.DrawPolygonList(poly.astype(np.int32))
645
646    def _cross(self, dc, coords, size=1):
647        fact = 2.5 * size
648        for f in [[-fact, -fact, fact, fact], [-fact, fact, fact, -fact]]:
649            lines = np.concatenate((coords, coords), axis=1) + f
650            dc.DrawLineList(lines.astype(np.int32))
651
652    def _plus(self, dc, coords, size=1):
653        fact = 2.5 * size
654        for f in [[-fact, 0, fact, 0], [0, -fact, 0, fact]]:
655            lines = np.concatenate((coords, coords), axis=1) + f
656            dc.DrawLineList(lines.astype(np.int32))
657
658
659class PolyBarsBase(PolyPoints):
660    """
661    Base class for PolyBars and PolyHistogram.
662
663    .. warning::
664
665       All methods are private.
666    """
667    _attributes = {'edgecolour': 'black',
668                   'edgewidth': 2,
669                   'edgestyle': wx.PENSTYLE_SOLID,
670                   'legend': '',
671                   'fillcolour': 'red',
672                   'fillstyle': wx.BRUSHSTYLE_SOLID,
673                   'barwidth': 1.0
674                   }
675
676    def __init__(self, points, attr):
677        """
678        """
679        PolyPoints.__init__(self, points, attr)
680
681    def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)):
682        """same as override method, but retuns a value."""
683        scaled = scale * data + shift
684        return scaled
685
686    def getSymExtent(self, printerScale):
687        """Width and Height of Marker"""
688        h = self.attributes['edgewidth'] * printerScale * self._pointSize[0]
689        w = 5 * h
690        return (w, h)
691
692    def set_pen_and_brush(self, dc, printerScale):
693        pencolour = self.attributes['edgecolour']
694        penwidth = (self.attributes['edgewidth']
695                    * printerScale * self._pointSize[0])
696        penstyle = self.attributes['edgestyle']
697        fillcolour = self.attributes['fillcolour']
698        fillstyle = self.attributes['fillstyle']
699
700        if not isinstance(pencolour, wx.Colour):
701            pencolour = wx.Colour(pencolour)
702        pen = wx.Pen(pencolour, penwidth, penstyle)
703        pen.SetCap(wx.CAP_BUTT)
704
705        if not isinstance(fillcolour, wx.Colour):
706            fillcolour = wx.Colour(fillcolour)
707        brush = wx.Brush(fillcolour, fillstyle)
708
709        dc.SetPen(pen)
710        dc.SetBrush(brush)
711
712    def scale_rect(self, rect):
713        # Scale the points to the plot area
714        scaled_rect = self._scaleAndShift(rect,
715                                          self.currentScale,
716                                          self.currentShift)
717
718        # Convert to (left, top, width, height) for drawing
719        wx_rect = [scaled_rect[0][0],                       # X (left)
720                   scaled_rect[0][1],                       # Y (top)
721                   scaled_rect[1][0] - scaled_rect[0][0],   # Width
722                   scaled_rect[1][1] - scaled_rect[0][1]]   # Height
723
724        return wx_rect
725
726    def draw(self, dc, printerScale, coord=None):
727        pass
728
729
730class PolyBars(PolyBarsBase):
731    """
732    Creates a PolyBars object.
733
734    :param points: The data to plot.
735    :type points: sequence of ``(center, height)`` points
736    :param **attr: keyword attributes
737
738    =================================  =============  =======================
739    Keyword and Default                Description    Type
740    =================================  =============  =======================
741    ``barwidth=1.0``                   bar width      float or list of floats
742    ``edgecolour='black'``             edge color     :class:`wx.Colour`
743    ``edgewidth=1``                    edge width     float
744    ``edgestyle=wx.PENSTYLE_SOLID``    edge style     :class:`wx.PenStyle`
745    ``fillcolour='red'``               fill color     :class:`wx.Colour`
746    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
747    ``legend=''``                      legend string  str
748    =================================  =============  =======================
749
750    .. important::
751
752       If ``barwidth`` is a list of floats:
753
754       + each bar will have a separate width
755       + ``len(barwidth)`` must equal ``len(points)``.
756
757    .. warning::
758
759       All methods except ``__init__`` are private.
760    """
761    def __init__(self, points, **attr):
762        PolyBarsBase.__init__(self, points, attr)
763
764    def calc_rect(self, x, y, w):
765        """ Calculate the rectangle for plotting. """
766        return self.scale_rect([[x - w / 2, y],      # left, top
767                                [x + w / 2, 0]])     # right, bottom
768
769    def draw(self, dc, printerScale, coord=None):
770        """ Draw the bars """
771        self.set_pen_and_brush(dc, printerScale)
772        barwidth = self.attributes['barwidth']
773
774        if coord is None:
775            if isinstance(barwidth, (int, float)):
776                # use a single width for all bars
777                pts = ((x, y, barwidth) for x, y in self.points)
778            elif isinstance(barwidth, (list, tuple)):
779                # use a separate width for each bar
780                if len(barwidth) != len(self.points):
781                    err_str = ("Barwidth ({} items) and Points ({} items) do "
782                               "not have the same length!")
783                    err_str = err_str.format(len(barwidth), len(self.points))
784                    raise ValueError(err_str)
785                pts = ((x, y, w) for (x, y), w in zip(self.points, barwidth))
786            else:
787                # invalid attribute type
788                err_str = ("Invalid type for 'barwidth'. Expected float, "
789                           "int, or list or tuple of (int or float). Got {}.")
790                raise TypeError(err_str.format(type(barwidth)))
791
792            rects = [self.calc_rect(x, y, w) for x, y, w in pts]
793            dc.DrawRectangleList(rects)
794        else:
795            dc.DrawLines(coord)  # draw legend line
796
797
798class PolyHistogram(PolyBarsBase):
799    """
800    Creates a PolyHistogram object.
801
802    :param hist: The histogram data.
803    :type hist: sequence of ``y`` values that define the heights of the bars
804    :param binspec: The bin specification.
805    :type binspec: sequence of ``x`` values that define the edges of the bins
806    :param **attr: keyword attributes
807
808    =================================  =============  =======================
809    Keyword and Default                Description    Type
810    =================================  =============  =======================
811    ``edgecolour='black'``             edge color     :class:`wx.Colour`
812    ``edgewidth=3``                    edge width     float
813    ``edgestyle=wx.PENSTYLE_SOLID``    edge style     :class:`wx.PenStyle`
814    ``fillcolour='blue'``              fill color     :class:`wx.Colour`
815    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
816    ``legend=''``                      legend string  str
817    =================================  =============  =======================
818
819    .. tip::
820
821       Use ``np.histogram()`` to easily create your histogram parameters::
822
823         hist_data, binspec = np.histogram(data)
824         hist_plot = PolyHistogram(hist_data, binspec)
825
826    .. important::
827
828       ``len(binspec)`` must equal ``len(hist) + 1``.
829
830    .. warning::
831
832       All methods except ``__init__`` are private.
833    """
834    def __init__(self, hist, binspec, **attr):
835        if len(binspec) != len(hist) + 1:
836            raise ValueError("Len(binspec) must equal len(hist) + 1")
837
838        self.hist = hist
839        self.binspec = binspec
840
841        # define the bins and center x locations
842        self.bins = list(pairwise(self.binspec))
843        bar_center_x = (pair[0] + (pair[1] - pair[0])/2 for pair in self.bins)
844
845        points = list(zip(bar_center_x, self.hist))
846        PolyBarsBase.__init__(self, points, attr)
847
848    def calc_rect(self, y, low, high):
849        """ Calculate the rectangle for plotting. """
850        return self.scale_rect([[low, y],       # left, top
851                                [high, 0]])     # right, bottom
852
853    def draw(self, dc, printerScale, coord=None):
854        """ Draw the bars """
855        self.set_pen_and_brush(dc, printerScale)
856
857        if coord is None:
858            rects = [self.calc_rect(y, low, high)
859                     for y, (low, high)
860                     in zip(self.hist, self.bins)]
861
862            dc.DrawRectangleList(rects)
863        else:
864            dc.DrawLines(coord)  # draw legend line
865
866
867class PolyBoxPlot(PolyPoints):
868    """
869    Creates a PolyBoxPlot object.
870
871    :param data: Raw data to create a box plot from.
872    :type data: sequence of int or float
873    :param **attr: keyword attributes
874
875    =================================  =============  =======================
876    Keyword and Default                Description    Type
877    =================================  =============  =======================
878    ``colour='black'``                 edge color     :class:`wx.Colour`
879    ``width=1``                        edge width     float
880    ``style=wx.PENSTYLE_SOLID``        edge style     :class:`wx.PenStyle`
881    ``legend=''``                      legend string  str
882    =================================  =============  =======================
883
884    .. note::
885
886       ``np.NaN`` and ``np.inf`` values are ignored.
887
888    .. admonition:: TODO
889
890       + [ ] Figure out a better way to get multiple box plots side-by-side
891         (current method is a hack).
892       + [ ] change the X axis to some labels.
893       + [ ] Change getClosestPoint to only grab box plot items and outlers?
894         Currently grabs every data point.
895       + [ ] Add more customization such as Pens/Brushes, outlier shapes/size,
896         and box width.
897       + [ ] Figure out how I want to handle log-y: log data then calcBP? Or
898         should I calc the BP first then the plot it on a log scale?
899    """
900    _attributes = {'colour': 'black',
901                   'width': 1,
902                   'style': wx.PENSTYLE_SOLID,
903                   'legend': '',
904                   }
905
906    def __init__(self, points, **attr):
907        # Set various attributes
908        self.box_width = 0.5
909
910        # Determine the X position and create a 1d dataset.
911        self.xpos = points[0, 0]
912        points = points[:, 1]
913
914        # Calculate the box plot points and the outliers
915        self._bpdata = self.calcBpData(points)
916        self._outliers = self.calcOutliers(points)
917        points = np.concatenate((self._bpdata, self._outliers))
918        points = np.array([(self.xpos, x) for x in points])
919
920        # Create a jitter for the outliers
921        self.jitter = (0.05 * np.random.random_sample(len(self._outliers))
922                       + self.xpos - 0.025)
923
924        # Init the parent class
925        PolyPoints.__init__(self, points, attr)
926
927    def _clean_data(self, data=None):
928        """
929        Removes NaN and Inf from the data.
930        """
931        if data is None:
932            data = self.points
933
934        # clean out NaN and infinity values.
935        data = data[~np.isnan(data)]
936        data = data[~np.isinf(data)]
937
938        return data
939
940    def boundingBox(self):
941        """
942        Returns bounding box for the plot.
943
944        Override method.
945        """
946        xpos = self.xpos
947
948        minXY = np.array([xpos - self.box_width / 2, self._bpdata.min * 0.95])
949        maxXY = np.array([xpos + self.box_width / 2, self._bpdata.max * 1.05])
950        return minXY, maxXY
951
952    def getClosestPoint(self, pntXY, pointScaled=True):
953        """
954        Returns the index of closest point on the curve, pointXY,
955        scaledXY, distance x, y in user coords.
956
957        Override method.
958
959        if pointScaled == True, then based on screen coords
960        if pointScaled == False, then based on user coords
961        """
962
963        xpos = self.xpos
964
965        # combine the outliers with the box plot data
966        data_to_use = np.concatenate((self._bpdata, self._outliers))
967        data_to_use = np.array([(xpos, x) for x in data_to_use])
968
969        if pointScaled:
970            # Use screen coords
971            p = self.scaled
972            pxy = self.currentScale * np.array(pntXY) + self.currentShift
973        else:
974            # Using user coords
975            p = self._points
976            pxy = np.array(pntXY)
977
978        # determine distnace for each point
979        d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1))  # sqrt(dx^2+dy^2)
980        pntIndex = np.argmin(d)
981        dist = d[pntIndex]
982        return [pntIndex,
983                self.points[pntIndex],
984                self.scaled[pntIndex] / self._pointSize,
985                dist]
986
987    def getSymExtent(self, printerScale):
988        """Width and Height of Marker"""
989        # TODO: does this need to be updated?
990        h = self.attributes['width'] * printerScale * self._pointSize[0]
991        w = 5 * h
992        return (w, h)
993
994    def calcBpData(self, data=None):
995        """
996        Box plot points:
997
998        Median (50%)
999        75%
1000        25%
1001        low_whisker = lowest value that's >= (25% - (IQR * 1.5))
1002        high_whisker = highest value that's <= 75% + (IQR * 1.5)
1003
1004        outliers are outside of 1.5 * IQR
1005
1006        Parameters
1007        ----------
1008        data : array-like
1009            The data to plot
1010
1011        Returns
1012        -------
1013        bpdata : collections.namedtuple
1014            Descriptive statistics for data:
1015            (min_data, low_whisker, q25, median, q75, high_whisker, max_data)
1016
1017        """
1018        data = self._clean_data(data)
1019
1020        min_data = float(np.min(data))
1021        max_data = float(np.max(data))
1022        q25 = float(np.percentile(data, 25))
1023        q75 = float(np.percentile(data, 75))
1024
1025        iqr = q75 - q25
1026
1027        low_whisker = float(data[data >= q25 - 1.5 * iqr].min())
1028        high_whisker = float(data[data <= q75 + 1.5 * iqr].max())
1029
1030        median = float(np.median(data))
1031
1032        BPData = namedtuple("bpdata", ("min", "low_whisker", "q25", "median",
1033                                       "q75", "high_whisker", "max"))
1034
1035        bpdata = BPData(min_data, low_whisker, q25, median,
1036                        q75, high_whisker, max_data)
1037
1038        return bpdata
1039
1040    def calcOutliers(self, data=None):
1041        """
1042        Calculates the outliers. Must be called after calcBpData.
1043        """
1044        data = self._clean_data(data)
1045
1046        outliers = data
1047        outlier_bool = np.logical_or(outliers > self._bpdata.high_whisker,
1048                                     outliers < self._bpdata.low_whisker)
1049        outliers = outliers[outlier_bool]
1050        return outliers
1051
1052    def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)):
1053        """same as override method, but retuns a value."""
1054        scaled = scale * data + shift
1055        return scaled
1056
1057    @TempStyle('pen')
1058    def draw(self, dc, printerScale, coord=None):
1059        """
1060        Draws a box plot on the DC.
1061
1062        Notes
1063        -----
1064        The following draw order is required:
1065
1066        1. First the whisker line
1067        2. Then the IQR box
1068        3. Lasly the median line.
1069
1070        This is because
1071
1072        + The whiskers are drawn as single line rather than two lines
1073        + The median line must be visable over the box if the box has a fill.
1074
1075        Other than that, the draw order can be changed.
1076        """
1077        self._draw_whisker(dc, printerScale)
1078        self._draw_iqr_box(dc, printerScale)
1079        self._draw_median(dc, printerScale)     # median after box
1080        self._draw_whisker_ends(dc, printerScale)
1081        self._draw_outliers(dc, printerScale)
1082
1083    @TempStyle('pen')
1084    def _draw_whisker(self, dc, printerScale):
1085        """Draws the whiskers as a single line"""
1086        xpos = self.xpos
1087
1088        # We draw it as one line and then hide the middle part with
1089        # the IQR rectangle
1090        whisker_line = np.array([[xpos, self._bpdata.low_whisker],
1091                                 [xpos, self._bpdata.high_whisker]])
1092
1093        whisker_line = self._scaleAndShift(whisker_line,
1094                                           self.currentScale,
1095                                           self.currentShift)
1096
1097        whisker_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID)
1098        whisker_pen.SetCap(wx.CAP_BUTT)
1099        dc.SetPen(whisker_pen)
1100        dc.DrawLines(whisker_line)
1101
1102    @TempStyle('pen')
1103    def _draw_iqr_box(self, dc, printerScale):
1104        """Draws the Inner Quartile Range box"""
1105        xpos = self.xpos
1106        box_w = self.box_width
1107
1108        iqr_box = [[xpos - box_w / 2, self._bpdata.q75],  # left, top
1109                   [xpos + box_w / 2, self._bpdata.q25]]  # right, bottom
1110
1111        # Scale it to the plot area
1112        iqr_box = self._scaleAndShift(iqr_box,
1113                                      self.currentScale,
1114                                      self.currentShift)
1115
1116        # rectangles are drawn (left, top, width, height) so adjust
1117        iqr_box = [iqr_box[0][0],                   # X (left)
1118                   iqr_box[0][1],                   # Y (top)
1119                   iqr_box[1][0] - iqr_box[0][0],   # Width
1120                   iqr_box[1][1] - iqr_box[0][1]]   # Height
1121
1122        box_pen = wx.Pen(wx.BLACK, 3, wx.PENSTYLE_SOLID)
1123        box_brush = wx.Brush(wx.GREEN, wx.BRUSHSTYLE_SOLID)
1124        dc.SetPen(box_pen)
1125        dc.SetBrush(box_brush)
1126
1127        dc.DrawRectangleList([iqr_box])
1128
1129    @TempStyle('pen')
1130    def _draw_median(self, dc, printerScale, coord=None):
1131        """Draws the median line"""
1132        xpos = self.xpos
1133
1134        median_line = np.array(
1135            [[xpos - self.box_width / 2, self._bpdata.median],
1136             [xpos + self.box_width / 2, self._bpdata.median]]
1137        )
1138
1139        median_line = self._scaleAndShift(median_line,
1140                                          self.currentScale,
1141                                          self.currentShift)
1142
1143        median_pen = wx.Pen(wx.BLACK, 4, wx.PENSTYLE_SOLID)
1144        median_pen.SetCap(wx.CAP_BUTT)
1145        dc.SetPen(median_pen)
1146        dc.DrawLines(median_line)
1147
1148    @TempStyle('pen')
1149    def _draw_whisker_ends(self, dc, printerScale):
1150        """Draws the end caps of the whiskers"""
1151        xpos = self.xpos
1152        fence_top = np.array(
1153            [[xpos - self.box_width * 0.2, self._bpdata.high_whisker],
1154             [xpos + self.box_width * 0.2, self._bpdata.high_whisker]]
1155        )
1156
1157        fence_top = self._scaleAndShift(fence_top,
1158                                        self.currentScale,
1159                                        self.currentShift)
1160
1161        fence_bottom = np.array(
1162            [[xpos - self.box_width * 0.2, self._bpdata.low_whisker],
1163             [xpos + self.box_width * 0.2, self._bpdata.low_whisker]]
1164        )
1165
1166        fence_bottom = self._scaleAndShift(fence_bottom,
1167                                           self.currentScale,
1168                                           self.currentShift)
1169
1170        fence_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID)
1171        fence_pen.SetCap(wx.CAP_BUTT)
1172        dc.SetPen(fence_pen)
1173        dc.DrawLines(fence_top)
1174        dc.DrawLines(fence_bottom)
1175
1176    @TempStyle('pen')
1177    def _draw_outliers(self, dc, printerScale):
1178        """Draws dots for the outliers"""
1179        # Set the pen
1180        outlier_pen = wx.Pen(wx.BLUE, 5, wx.PENSTYLE_SOLID)
1181        dc.SetPen(outlier_pen)
1182
1183        outliers = self._outliers
1184
1185        # Scale the data for plotting
1186        pt_data = np.array([self.jitter, outliers]).T
1187        pt_data = self._scaleAndShift(pt_data,
1188                                      self.currentScale,
1189                                      self.currentShift)
1190
1191        # Draw the outliers
1192        size = 0.5
1193        fact = 2.5 * size
1194        wh = 5.0 * size
1195        rect = np.zeros((len(pt_data), 4), np.float) + [0.0, 0.0, wh, wh]
1196        rect[:, 0:2] = pt_data - [fact, fact]
1197        dc.DrawRectangleList(rect.astype(np.int32))
1198
1199
1200class PlotGraphics(object):
1201    """
1202    Creates a PlotGraphics object.
1203
1204    :param objects: The Poly objects to plot.
1205    :type objects: list of :class:`~wx.lib.plot.PolyPoints` objects
1206    :param title: The title shown at the top of the graph.
1207    :type title: str
1208    :param xLabel: The x-axis label.
1209    :type xLabel: str
1210    :param yLabel: The y-axis label.
1211    :type yLabel: str
1212
1213    .. warning::
1214
1215       All methods except ``__init__`` are private.
1216    """
1217    def __init__(self, objects, title='', xLabel='', yLabel=''):
1218        if type(objects) not in [list, tuple]:
1219            raise TypeError("objects argument should be list or tuple")
1220        self.objects = objects
1221        self._title = title
1222        self._xLabel = xLabel
1223        self._yLabel = yLabel
1224        self._pointSize = (1.0, 1.0)
1225
1226    @property
1227    def logScale(self):
1228        if len(self.objects) == 0:
1229            return
1230        return [obj.logScale for obj in self.objects]
1231
1232    @logScale.setter
1233    def logScale(self, logscale):
1234        # XXX: error checking done by PolyPoints class
1235#        if not isinstance(logscale, tuple) and len(logscale) != 2:
1236#            raise TypeError("logscale must be a 2-tuple of bools")
1237        if len(self.objects) == 0:
1238            return
1239        for obj in self.objects:
1240            obj.logScale = logscale
1241
1242    def setLogScale(self, logscale):
1243        """
1244        Set the log scale boolean value.
1245
1246        .. deprecated:: Feb 27, 2016
1247
1248           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.logScale`
1249           property instead.
1250        """
1251        pendingDeprecation("self.logScale property")
1252        self.logScale = logscale
1253
1254    @property
1255    def absScale(self):
1256        if len(self.objects) == 0:
1257            return
1258        return [obj.absScale for obj in self.objects]
1259
1260    @absScale.setter
1261    def absScale(self, absscale):
1262        # XXX: error checking done by PolyPoints class
1263#        if not isinstance(absscale, tuple) and len(absscale) != 2:
1264#            raise TypeError("absscale must be a 2-tuple of bools")
1265        if len(self.objects) == 0:
1266            return
1267        for obj in self.objects:
1268            obj.absScale = absscale
1269
1270    def boundingBox(self):
1271        p1, p2 = self.objects[0].boundingBox()
1272        for o in self.objects[1:]:
1273            p1o, p2o = o.boundingBox()
1274            p1 = np.minimum(p1, p1o)
1275            p2 = np.maximum(p2, p2o)
1276        return p1, p2
1277
1278    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
1279        for o in self.objects:
1280            o.scaleAndShift(scale, shift)
1281
1282    def setPrinterScale(self, scale):
1283        """
1284        Thickens up lines and markers only for printing
1285
1286        .. deprecated:: Feb 27, 2016
1287
1288           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.printerScale`
1289           property instead.
1290        """
1291        pendingDeprecation("self.printerScale property")
1292        self.printerScale = scale
1293
1294    def setXLabel(self, xLabel=''):
1295        """
1296        Set the X axis label on the graph
1297
1298        .. deprecated:: Feb 27, 2016
1299
1300           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel`
1301           property instead.
1302        """
1303        pendingDeprecation("self.xLabel property")
1304        self.xLabel = xLabel
1305
1306    def setYLabel(self, yLabel=''):
1307        """
1308        Set the Y axis label on the graph
1309
1310        .. deprecated:: Feb 27, 2016
1311
1312           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel`
1313           property instead.
1314        """
1315        pendingDeprecation("self.yLabel property")
1316        self.yLabel = yLabel
1317
1318    def setTitle(self, title=''):
1319        """
1320        Set the title at the top of graph
1321
1322        .. deprecated:: Feb 27, 2016
1323
1324           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title`
1325           property instead.
1326        """
1327        pendingDeprecation("self.title property")
1328        self.title = title
1329
1330    def getXLabel(self):
1331        """
1332        Get X axis label string
1333
1334        .. deprecated:: Feb 27, 2016
1335
1336           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel`
1337           property instead.
1338        """
1339        pendingDeprecation("self.xLabel property")
1340        return self.xLabel
1341
1342    def getYLabel(self):
1343        """
1344        Get Y axis label string
1345
1346        .. deprecated:: Feb 27, 2016
1347
1348           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel`
1349           property instead.
1350        """
1351        pendingDeprecation("self.yLabel property")
1352        return self.yLabel
1353
1354    def getTitle(self, title=''):
1355        """
1356        Get the title at the top of graph
1357
1358        .. deprecated:: Feb 27, 2016
1359
1360           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title`
1361           property instead.
1362        """
1363        pendingDeprecation("self.title property")
1364        return self.title
1365
1366    @property
1367    def printerScale(self):
1368        return self._printerScale
1369
1370    @printerScale.setter
1371    def printerScale(self, scale):
1372        """Thickens up lines and markers only for printing"""
1373        self._printerScale = scale
1374
1375    @property
1376    def xLabel(self):
1377        """Get the X axis label on the graph"""
1378        return self._xLabel
1379
1380    @xLabel.setter
1381    def xLabel(self, text):
1382        self._xLabel = text
1383
1384    @property
1385    def yLabel(self):
1386        """Get the Y axis label on the graph"""
1387        return self._yLabel
1388
1389    @yLabel.setter
1390    def yLabel(self, text):
1391        self._yLabel = text
1392
1393    @property
1394    def title(self):
1395        """Get the title at the top of graph"""
1396        return self._title
1397
1398    @title.setter
1399    def title(self, text):
1400        self._title = text
1401
1402    def draw(self, dc):
1403        for o in self.objects:
1404#            t=_time.perf_counter()          # profile info
1405            o._pointSize = self._pointSize
1406            o.draw(dc, self._printerScale)
1407#            print(o, "time=", _time.perf_counter()-t)
1408
1409    def getSymExtent(self, printerScale):
1410        """Get max width and height of lines and markers symbols for legend"""
1411        self.objects[0]._pointSize = self._pointSize
1412        symExt = self.objects[0].getSymExtent(printerScale)
1413        for o in self.objects[1:]:
1414            o._pointSize = self._pointSize
1415            oSymExt = o.getSymExtent(printerScale)
1416            symExt = np.maximum(symExt, oSymExt)
1417        return symExt
1418
1419    def getLegendNames(self):
1420        """Returns list of legend names"""
1421        lst = [None] * len(self)
1422        for i in range(len(self)):
1423            lst[i] = self.objects[i].getLegend()
1424        return lst
1425
1426    def __len__(self):
1427        return len(self.objects)
1428
1429    def __getitem__(self, item):
1430        return self.objects[item]
1431
1432
1433# -------------------------------------------------------------------------
1434# Used to layout the printer page
1435
1436
1437class PlotPrintout(wx.Printout):
1438    """Controls how the plot is made in printing and previewing"""
1439    # Do not change method names in this class,
1440    # we have to override wx.Printout methods here!
1441
1442    def __init__(self, graph):
1443        """graph is instance of plotCanvas to be printed or previewed"""
1444        wx.Printout.__init__(self)
1445        self.graph = graph
1446
1447    def HasPage(self, page):
1448        if page == 1:
1449            return True
1450        else:
1451            return False
1452
1453    def GetPageInfo(self):
1454        return (1, 1, 1, 1)  # disable page numbers
1455
1456    def OnPrintPage(self, page):
1457        dc = self.GetDC()  # allows using floats for certain functions
1458#        print("PPI Printer",self.GetPPIPrinter())
1459#        print("PPI Screen", self.GetPPIScreen())
1460#        print("DC GetSize", dc.GetSize())
1461#        print("GetPageSizePixels", self.GetPageSizePixels())
1462        # Note PPIScreen does not give the correct number
1463        # Calulate everything for printer and then scale for preview
1464        PPIPrinter = self.GetPPIPrinter()        # printer dots/inch (w,h)
1465        # PPIScreen= self.GetPPIScreen()          # screen dots/inch (w,h)
1466        dcSize = dc.GetSize()                    # DC size
1467        if self.graph._antiAliasingEnabled and not isinstance(dc, wx.GCDC):
1468            try:
1469                dc = wx.GCDC(dc)
1470            except Exception:
1471                pass
1472            else:
1473                if self.graph._hiResEnabled:
1474                    # high precision - each logical unit is 1/20 of a point
1475                    dc.SetMapMode(wx.MM_TWIPS)
1476        pageSize = self.GetPageSizePixels()  # page size in terms of pixcels
1477        clientDcSize = self.graph.GetClientSize()
1478
1479        # find what the margins are (mm)
1480        pgSetupData = self.graph.pageSetupData
1481        margLeftSize, margTopSize = pgSetupData.GetMarginTopLeft()
1482        margRightSize, margBottomSize = pgSetupData.GetMarginBottomRight()
1483
1484        # calculate offset and scale for dc
1485        pixLeft = margLeftSize * PPIPrinter[0] / 25.4  # mm*(dots/in)/(mm/in)
1486        pixRight = margRightSize * PPIPrinter[0] / 25.4
1487        pixTop = margTopSize * PPIPrinter[1] / 25.4
1488        pixBottom = margBottomSize * PPIPrinter[1] / 25.4
1489
1490        plotAreaW = pageSize[0] - (pixLeft + pixRight)
1491        plotAreaH = pageSize[1] - (pixTop + pixBottom)
1492
1493        # ratio offset and scale to screen size if preview
1494        if self.IsPreview():
1495            ratioW = float(dcSize[0]) / pageSize[0]
1496            ratioH = float(dcSize[1]) / pageSize[1]
1497            pixLeft *= ratioW
1498            pixTop *= ratioH
1499            plotAreaW *= ratioW
1500            plotAreaH *= ratioH
1501
1502        # rescale plot to page or preview plot area
1503        self.graph._setSize(plotAreaW, plotAreaH)
1504
1505        # Set offset and scale
1506        dc.SetDeviceOrigin(pixLeft, pixTop)
1507
1508        # Thicken up pens and increase marker size for printing
1509        ratioW = float(plotAreaW) / clientDcSize[0]
1510        ratioH = float(plotAreaH) / clientDcSize[1]
1511        aveScale = (ratioW + ratioH) / 2
1512        if self.graph._antiAliasingEnabled and not self.IsPreview():
1513            scale = dc.GetUserScale()
1514            dc.SetUserScale(scale[0] / self.graph._pointSize[0],
1515                            scale[1] / self.graph._pointSize[1])
1516        self.graph._setPrinterScale(aveScale)  # tickens up pens for printing
1517
1518        self.graph._printDraw(dc)
1519        # rescale back to original
1520        self.graph._setSize()
1521        self.graph._setPrinterScale(1)
1522        self.graph.Redraw()  # to get point label scale and shift correct
1523
1524        return True
1525