# -*- coding: utf-8 -*-
# pylint: disable=E1101, C0330, C0103
#   E1101: Module X has no Y member
#   C0330: Wrong continued indentation
#   C0103: Invalid attribute/variable/method name
"""
polyobjects.py
==============

This contains all of the PolyXXX objects used by :mod:`wx.lib.plot`.

"""
__docformat__ = "restructuredtext en"

# Standard Library
import time as _time
import wx
import warnings
from collections import namedtuple

# Third-Party
try:
    import numpy as np
except:
    msg = """
    This module requires the NumPy module, which could not be
    imported.  It probably is not installed (it's not part of the
    standard Python distribution). See the Numeric Python site
    (http://numpy.scipy.org) for information on downloading source or
    binaries."""
    raise ImportError("NumPy not found.\n" + msg)

# Package
from .utils import pendingDeprecation
from .utils import TempStyle
from .utils import pairwise


class PolyPoints(object):
    """
    Base Class for lines and markers.

    :param points: The points to plot
    :type points: list of ``(x, y)`` pairs
    :param attr: Additional attributes
    :type attr: dict

    .. warning::
       All methods are private.
    """

    def __init__(self, points, attr):
        self._points = np.array(points).astype(np.float64)
        self._logscale = (False, False)
        self._absScale = (False, False)
        self._symlogscale = (False, False)
        self._pointSize = (1.0, 1.0)
        self.currentScale = (1, 1)
        self.currentShift = (0, 0)
        self.scaled = self.points
        self.attributes = {}
        self.attributes.update(self._attributes)
        for name, value in attr.items():
            if name not in self._attributes:
                err_txt = "Style attribute incorrect. Should be one of {}"
                raise KeyError(err_txt.format(self._attributes.keys()))
            self.attributes[name] = value

    @property
    def logScale(self):
        """
        A tuple of ``(x_axis_is_log10, y_axis_is_log10)`` booleans. If a value
        is ``True``, then that axis is plotted on a logarithmic base 10 scale.

        :getter: Returns the current value of logScale
        :setter: Sets the value of logScale
        :type: tuple of bool, length 2
        :raises ValueError: when setting an invalid value
        """
        return self._logscale

    @logScale.setter
    def logScale(self, logscale):
        if not isinstance(logscale, tuple) or len(logscale) != 2:
            raise ValueError("`logscale` must be a 2-tuple of bools")
        self._logscale = logscale

    def setLogScale(self, logscale):
        """
        Set to change the axes to plot Log10(values)

        Value must be a tuple of booleans (x_axis_bool, y_axis_bool)

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PolyPoints.logScale`
           property instead.
        """
        pendingDeprecation("self.logScale property")
        self._logscale = logscale

    @property
    def symLogScale(self):
        """
        .. warning::

           Not yet implemented.

        A tuple of ``(x_axis_is_SymLog10, y_axis_is_SymLog10)`` booleans.
        If a value is ``True``, then that axis is plotted on a symmetric
        logarithmic base 10 scale.

        A Symmetric Log10 scale means that values can be positive and
        negative. Any values less than
        :attr:`~wx.lig.plot.PolyPoints.symLogThresh` will be plotted on
        a linear scale to avoid the plot going to infinity near 0.

        :getter: Returns the current value of symLogScale
        :setter: Sets the value of symLogScale
        :type: tuple of bool, length 2
        :raises ValueError: when setting an invalid value

        .. notes::

           This is a simplified example of how SymLog works::

             if x >= thresh:
                 x = Log10(x)
             elif x =< thresh:
                 x = -Log10(Abs(x))
             else:
                 x = x

        .. seealso::

           + :attr:`~wx.lib.plot.PolyPoints.symLogThresh`
           + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html
             for an example.
        """
        return self._symlogscale

    # TODO: Implement symmetric log scale
    @symLogScale.setter
    def symLogScale(self, symlogscale, thresh):
        raise NotImplementedError("Symmetric Log Scale not yet implemented")

        if not isinstance(symlogscale, tuple) or len(symlogscale) != 2:
            raise ValueError("`symlogscale` must be a 2-tuple of bools")
        self._symlogscale = symlogscale

    @property
    def symLogThresh(self):
        """
        .. warning::

           Not yet implemented.

        A tuple of ``(x_thresh, y_thresh)`` floats that define where the plot
        changes to linear scale when using a symmetric log scale.

        :getter: Returns the current value of symLogThresh
        :setter: Sets the value of symLogThresh
        :type: tuple of float, length 2
        :raises ValueError: when setting an invalid value

        .. notes::

           This is a simplified example of how SymLog works::

             if x >= thresh:
                 x = Log10(x)
             elif x =< thresh:
                 x = -Log10(Abs(x))
             else:
                 x = x

        .. seealso::

           + :attr:`~wx.lib.plot.PolyPoints.symLogScale`
           + See http://matplotlib.org/examples/pylab_examples/symlog_demo.html
             for an example.
        """
        return self._symlogscale

    # TODO: Implement symmetric log scale threshold
    @symLogThresh.setter
    def symLogThresh(self, symlogscale, thresh):
        raise NotImplementedError("Symmetric Log Scale not yet implemented")

        if not isinstance(symlogscale, tuple) or len(symlogscale) != 2:
            raise ValueError("`symlogscale` must be a 2-tuple of bools")
        self._symlogscale = symlogscale

    @property
    def absScale(self):
        """
        A tuple of ``(x_axis_is_abs, y_axis_is_abs)`` booleans. If a value
        is ``True``, then that axis is plotted on an absolute value scale.

        :getter: Returns the current value of absScale
        :setter: Sets the value of absScale
        :type: tuple of bool, length 2
        :raises ValueError: when setting an invalid value
        """
        return self._absScale

    @absScale.setter
    def absScale(self, absscale):

        if not isinstance(absscale, tuple) and len(absscale) == 2:
            raise ValueError("`absscale` must be a 2-tuple of bools")
        self._absScale = absscale

    @property
    def points(self):
        """
        Get or set the plotted points.

        :getter: Returns the current value of points, adjusting for the
                 various scale options such as Log, Abs, or SymLog.
        :setter: Sets the value of points.
        :type: list of `(x, y)` pairs

        .. Note::

           Only set unscaled points - do not perform the log, abs, or symlog
           adjustments yourself.
        """
        data = np.array(self._points, copy=True)    # need the copy
                                                    # TODO: get rid of the
                                                    # need for copy

        # work on X:
        if self.absScale[0]:
            data = self._abs(data, 0)
        if self.logScale[0]:
            data = self._log10(data, 0)

        if self.symLogScale[0]:
            # TODO: implement symLogScale
            # Should symLogScale override absScale? My vote is no.
            # Should symLogScale override logScale? My vote is yes.
            #   - symLogScale could be a parameter passed to logScale...
            pass

        # work on Y:
        if self.absScale[1]:
            data = self._abs(data, 1)
        if self.logScale[1]:
            data = self._log10(data, 1)

        if self.symLogScale[1]:
            # TODO: implement symLogScale
            pass

        return data

    @points.setter
    def points(self, points):
        self._points = points

    def _log10(self, data, index):
        """ Take the Log10 of the data, dropping any negative values """
        data = np.compress(data[:, index] > 0, data, 0)
        data[:, index] = np.log10(data[:, index])
        return data

    def _abs(self, data, index):
        """ Take the Abs of the data """
        data[:, index] = np.abs(data[:, index])
        return data

    def boundingBox(self):
        """
        Returns the bounding box for the entire dataset as a tuple with this
        format::

            ((minX, minY), (maxX, maxY))

        :returns: boundingbox
        :rtype: numpy array of ``[[minX, minY], [maxX, maxY]]``
        """
        if len(self.points) == 0:
            # no curves to draw
            # defaults to (-1,-1) and (1,1) but axis can be set in Draw
            minXY = np.array([-1.0, -1.0])
            maxXY = np.array([1.0, 1.0])
        else:
            minXY = np.minimum.reduce(self.points)
            maxXY = np.maximum.reduce(self.points)
        return minXY, maxXY

    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
        """
        Scales and shifts the data for plotting.

        :param scale: The values to scale the data by.
        :type scale: list of floats: ``[x_scale, y_scale]``
        :param shift: The value to shift the data by. This should be in scaled
                      units
        :type shift: list of floats: ``[x_shift, y_shift]``
        :returns: None
        """
        if len(self.points) == 0:
            # no curves to draw
            return

        # TODO: Can we remove the if statement altogether? Does
        #       scaleAndShift ever get called when the current value equals
        #       the new value?

        # cast everything to list: some might be np.ndarray objects
        if (list(scale) != list(self.currentScale)
                or list(shift) != list(self.currentShift)):
            # update point scaling
            self.scaled = scale * self.points + shift
            self.currentScale = scale
            self.currentShift = shift
        # else unchanged use the current scaling

    def getLegend(self):
        return self.attributes['legend']

    def getClosestPoint(self, pntXY, pointScaled=True):
        """
        Returns the index of closest point on the curve, pointXY,
        scaledXY, distance x, y in user coords.

        if pointScaled == True, then based on screen coords
        if pointScaled == False, then based on user coords
        """
        if pointScaled:
            # Using screen coords
            p = self.scaled
            pxy = self.currentScale * np.array(pntXY) + self.currentShift
        else:
            # Using user coords
            p = self.points
            pxy = np.array(pntXY)
        # determine distance for each point
        d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1))  # sqrt(dx^2+dy^2)
        pntIndex = np.argmin(d)
        dist = d[pntIndex]
        return [pntIndex,
                self.points[pntIndex],
                self.scaled[pntIndex] / self._pointSize,
                dist]


class PolyLine(PolyPoints):
    """
    Creates PolyLine object

    :param points: The points that make up the line
    :type points: list of ``[x, y]`` values
    :param **attr: keyword attributes

    ===========================  =============  ====================
    Keyword and Default          Description    Type
    ===========================  =============  ====================
    ``colour='black'``           Line color     :class:`wx.Colour`
    ``width=1``                  Line width     float
    ``style=wx.PENSTYLE_SOLID``  Line style     :class:`wx.PenStyle`
    ``legend=''``                Legend string  str
    ``drawstyle='line'``         see below      str
    ===========================  =============  ====================

    ==================  ==================================================
    Draw style          Description
    ==================  ==================================================
    ``'line'``          Draws an straight line between consecutive points
    ``'steps-pre'``     Draws a line down from point A and then right to
                        point B
    ``'steps-post'``    Draws a line right from point A and then down
                        to point B
    ``'steps-mid-x'``   Draws a line horizontally to half way between A
                        and B, then draws a line vertically, then again
                        horizontally to point B.
    ``'steps-mid-y'``   Draws a line vertically to half way between A
                        and B, then draws a line horizonatally, then
                        again vertically to point B.
                        *Note: This typically does not look very good*
    ==================  ==================================================

    .. warning::

       All methods except ``__init__`` are private.
    """

    _attributes = {'colour': 'black',
                   'width': 1,
                   'style': wx.PENSTYLE_SOLID,
                   'legend': '',
                   'drawstyle': 'line',
                   }
    _drawstyles = ("line", "steps-pre", "steps-post",
                   "steps-mid-x", "steps-mid-y")

    def __init__(self, points, **attr):
        PolyPoints.__init__(self, points, attr)

    def draw(self, dc, printerScale, coord=None):
        """
        Draw the lines.

        :param dc: The DC to draw on.
        :type dc: :class:`wx.DC`
        :param printerScale:
        :type printerScale: float
        :param coord: The legend coordinate?
        :type coord: ???
        """
        colour = self.attributes['colour']
        width = self.attributes['width'] * printerScale * self._pointSize[0]
        style = self.attributes['style']
        drawstyle = self.attributes['drawstyle']

        if not isinstance(colour, wx.Colour):
            colour = wx.Colour(colour)
        pen = wx.Pen(colour, int(width), style)
        pen.SetCap(wx.CAP_BUTT)
        dc.SetPen(pen)
        if coord is None:
            if len(self.scaled):  # bugfix for Mac OS X
                for c1, c2 in zip(self.scaled, self.scaled[1:]):
                    self._path(dc, c1, c2, drawstyle)
        else:
            coord = [(int(c[0]), int(c[1])) for c in coord]
            dc.DrawLines(coord)  # draw legend line

    def getSymExtent(self, printerScale):
        """
        Get the Width and Height of the symbol.

        :param printerScale:
        :type printerScale: float
        """
        h = self.attributes['width'] * printerScale * self._pointSize[0]
        w = 5 * h
        return (w, h)

    def _path(self, dc, coord1, coord2, drawstyle):
        """
        Calculates the path from coord1 to coord 2 along X and Y

        :param dc: The DC to draw on.
        :type dc: :class:`wx.DC`
        :param coord1: The first coordinate in the coord pair
        :type coord1: list, length 2: ``[x, y]``
        :param coord2: The second coordinate in the coord pair
        :type coord2: list, length 2: ``[x, y]``
        :param drawstyle: The type of connector to use
        :type drawstyle: str
        """
        if drawstyle == 'line':
            # Straight line between points.
            line = [coord1, coord2]
        elif drawstyle == 'steps-pre':
            # Up/down to next Y, then right to next X
            intermediate = [coord1[0], coord2[1]]
            line = [coord1, intermediate, coord2]
        elif drawstyle == 'steps-post':
            # Right to next X, then up/down to Y
            intermediate = [coord2[0], coord1[1]]
            line = [coord1, intermediate, coord2]
        elif drawstyle == 'steps-mid-x':
            # need 3 lines between points: right -> up/down -> right
            mid_x = ((coord2[0] - coord1[0]) / 2) + coord1[0]
            intermediate1 = [mid_x, coord1[1]]
            intermediate2 = [mid_x, coord2[1]]
            line = [coord1, intermediate1, intermediate2, coord2]
        elif drawstyle == 'steps-mid-y':
            # need 3 lines between points: up/down -> right -> up/down
            mid_y = ((coord2[1] - coord1[1]) / 2) + coord1[1]
            intermediate1 = [coord1[0], mid_y]
            intermediate2 = [coord2[0], mid_y]
            line = [coord1, intermediate1, intermediate2, coord2]
        else:
            err_txt = "Invalid drawstyle '{}'. Must be one of {}."
            raise ValueError(err_txt.format(drawstyle, self._drawstyles))

        line = [(int(p[0]), int(p[1])) for p in line]
        dc.DrawLines(line)


class PolySpline(PolyLine):
    """
    Creates PolySpline object

    :param points: The points that make up the spline
    :type points: list of ``[x, y]`` values
    :param **attr: keyword attributes

    ===========================  =============  ====================
    Keyword and Default          Description    Type
    ===========================  =============  ====================
    ``colour='black'``           Line color     :class:`wx.Colour`
    ``width=1``                  Line width     float
    ``style=wx.PENSTYLE_SOLID``  Line style     :class:`wx.PenStyle`
    ``legend=''``                Legend string  str
    ===========================  =============  ====================

    .. warning::

       All methods except ``__init__`` are private.
    """

    _attributes = {'colour': 'black',
                   'width': 1,
                   'style': wx.PENSTYLE_SOLID,
                   'legend': ''}

    def __init__(self, points, **attr):
        PolyLine.__init__(self, points, **attr)

    def draw(self, dc, printerScale, coord=None):
        """ Draw the spline """
        colour = self.attributes['colour']
        width = self.attributes['width'] * printerScale * self._pointSize[0]
        style = self.attributes['style']
        if not isinstance(colour, wx.Colour):
            colour = wx.Colour(colour)
        pen = wx.Pen(colour, int(width), style)
        pen.SetCap(wx.CAP_ROUND)
        dc.SetPen(pen)
        if coord is None:
            if len(self.scaled) >= 3:
                dc.DrawSpline(self.scaled.astype(np.int32))
        else:
            coord = [(int(c[0]), int(c[1])) for c in coord]
            dc.DrawLines(coord)  # draw legend line


class PolyMarker(PolyPoints):
    """
    Creates a PolyMarker object.

    :param points: The marker coordinates.
    :type points: list of ``[x, y]`` values
    :param **attr: keyword attributes

    =================================  =============  ====================
    Keyword and Default                Description    Type
    =================================  =============  ====================
    ``marker='circle'``                see below      str
    ``size=2``                         Marker size    float
    ``colour='black'``                 Outline color  :class:`wx.Colour`
    ``width=1``                        Outline width  float
    ``style=wx.PENSTYLE_SOLID``        Outline style  :class:`wx.PenStyle`
    ``fillcolour=colour``              fill color     :class:`wx.Colour`
    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
    ``legend=''``                      Legend string  str
    =================================  =============  ====================

    ===================  ==================================
    Marker               Description
    ===================  ==================================
    ``'circle'``         A circle of diameter ``size``
    ``'dot'``            A dot. Does not have a size.
    ``'square'``         A square with side length ``size``
    ``'triangle'``       An upward-pointed triangle
    ``'triangle_down'``  A downward-pointed triangle
    ``'cross'``          An "X" shape
    ``'plus'``           A "+" shape
    ===================  ==================================

    .. warning::

       All methods except ``__init__`` are private.
    """
    _attributes = {'colour': 'black',
                   'width': 1,
                   'size': 2,
                   'fillcolour': None,
                   'fillstyle': wx.BRUSHSTYLE_SOLID,
                   'marker': 'circle',
                   'legend': ''}

    def __init__(self, points, **attr):
        PolyPoints.__init__(self, points, attr)

    def draw(self, dc, printerScale, coord=None):
        """ Draw the points """
        colour = self.attributes['colour']
        width = self.attributes['width'] * printerScale * self._pointSize[0]
        size = self.attributes['size'] * printerScale * self._pointSize[0]
        fillcolour = self.attributes['fillcolour']
        fillstyle = self.attributes['fillstyle']
        marker = self.attributes['marker']

        if colour and not isinstance(colour, wx.Colour):
            colour = wx.Colour(colour)
        if fillcolour and not isinstance(fillcolour, wx.Colour):
            fillcolour = wx.Colour(fillcolour)

        dc.SetPen(wx.Pen(colour, int(width)))
        if fillcolour:
            dc.SetBrush(wx.Brush(fillcolour, fillstyle))
        else:
            dc.SetBrush(wx.Brush(colour, fillstyle))
        if coord is None:
            if len(self.scaled):  # bugfix for Mac OS X
                self._drawmarkers(dc, self.scaled, marker, size)
        else:
            self._drawmarkers(dc, coord, marker, size)  # draw legend marker

    def getSymExtent(self, printerScale):
        """Width and Height of Marker"""
        s = 5 * self.attributes['size'] * printerScale * self._pointSize[0]
        return (s, s)

    def _drawmarkers(self, dc, coords, marker, size=1):
        f = getattr(self, "_{}".format(marker))
        f(dc, coords, size)

    def _circle(self, dc, coords, size=1):
        fact = 2.5 * size
        wh = 5.0 * size
        rect = np.zeros((len(coords), 4), float) + [0.0, 0.0, wh, wh]
        rect[:, 0:2] = coords - [fact, fact]
        dc.DrawEllipseList(rect.astype(np.int32))

    def _dot(self, dc, coords, size=1):
        coords = [(int(c[0]), int(c[1])) for c in coords]
        dc.DrawPointList(coords)

    def _square(self, dc, coords, size=1):
        fact = 2.5 * size
        wh = 5.0 * size
        rect = np.zeros((len(coords), 4), float) + [0.0, 0.0, wh, wh]
        rect[:, 0:2] = coords - [fact, fact]
        dc.DrawRectangleList(rect.astype(np.int32))

    def _triangle(self, dc, coords, size=1):
        shape = [(-2.5 * size, 1.44 * size),
                 (2.5 * size, 1.44 * size), (0.0, -2.88 * size)]
        poly = np.repeat(coords, 3, 0)
        poly.shape = (len(coords), 3, 2)
        poly += shape
        dc.DrawPolygonList(poly.astype(np.int32))

    def _triangle_down(self, dc, coords, size=1):
        shape = [(-2.5 * size, -1.44 * size),
                 (2.5 * size, -1.44 * size), (0.0, 2.88 * size)]
        poly = np.repeat(coords, 3, 0)
        poly.shape = (len(coords), 3, 2)
        poly += shape
        dc.DrawPolygonList(poly.astype(np.int32))

    def _cross(self, dc, coords, size=1):
        fact = 2.5 * size
        for f in [[-fact, -fact, fact, fact], [-fact, fact, fact, -fact]]:
            lines = np.concatenate((coords, coords), axis=1) + f
            dc.DrawLineList(lines.astype(np.int32))

    def _plus(self, dc, coords, size=1):
        fact = 2.5 * size
        for f in [[-fact, 0, fact, 0], [0, -fact, 0, fact]]:
            lines = np.concatenate((coords, coords), axis=1) + f
            dc.DrawLineList(lines.astype(np.int32))


class PolyBarsBase(PolyPoints):
    """
    Base class for PolyBars and PolyHistogram.

    .. warning::

       All methods are private.
    """
    _attributes = {'edgecolour': 'black',
                   'edgewidth': 2,
                   'edgestyle': wx.PENSTYLE_SOLID,
                   'legend': '',
                   'fillcolour': 'red',
                   'fillstyle': wx.BRUSHSTYLE_SOLID,
                   'barwidth': 1.0
                   }

    def __init__(self, points, attr):
        """
        """
        PolyPoints.__init__(self, points, attr)

    def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)):
        """same as override method, but returns a value."""
        scaled = scale * data + shift
        return scaled

    def getSymExtent(self, printerScale):
        """Width and Height of Marker"""
        h = self.attributes['edgewidth'] * printerScale * self._pointSize[0]
        w = 5 * h
        return (w, h)

    def set_pen_and_brush(self, dc, printerScale):
        pencolour = self.attributes['edgecolour']
        penwidth = (self.attributes['edgewidth']
                    * printerScale * self._pointSize[0])
        penstyle = self.attributes['edgestyle']
        fillcolour = self.attributes['fillcolour']
        fillstyle = self.attributes['fillstyle']

        if not isinstance(pencolour, wx.Colour):
            pencolour = wx.Colour(pencolour)
        pen = wx.Pen(pencolour, int(penwidth), penstyle)
        pen.SetCap(wx.CAP_BUTT)

        if not isinstance(fillcolour, wx.Colour):
            fillcolour = wx.Colour(fillcolour)
        brush = wx.Brush(fillcolour, fillstyle)

        dc.SetPen(pen)
        dc.SetBrush(brush)

    def scale_rect(self, rect):
        # Scale the points to the plot area
        scaled_rect = self._scaleAndShift(rect,
                                          self.currentScale,
                                          self.currentShift)

        # Convert to (left, top, width, height) for drawing
        wx_rect = [scaled_rect[0][0],                       # X (left)
                   scaled_rect[0][1],                       # Y (top)
                   scaled_rect[1][0] - scaled_rect[0][0],   # Width
                   scaled_rect[1][1] - scaled_rect[0][1]]   # Height

        return wx_rect

    def draw(self, dc, printerScale, coord=None):
        pass


class PolyBars(PolyBarsBase):
    """
    Creates a PolyBars object.

    :param points: The data to plot.
    :type points: sequence of ``(center, height)`` points
    :param **attr: keyword attributes

    =================================  =============  =======================
    Keyword and Default                Description    Type
    =================================  =============  =======================
    ``barwidth=1.0``                   bar width      float or list of floats
    ``edgecolour='black'``             edge color     :class:`wx.Colour`
    ``edgewidth=1``                    edge width     float
    ``edgestyle=wx.PENSTYLE_SOLID``    edge style     :class:`wx.PenStyle`
    ``fillcolour='red'``               fill color     :class:`wx.Colour`
    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
    ``legend=''``                      legend string  str
    =================================  =============  =======================

    .. important::

       If ``barwidth`` is a list of floats:

       + each bar will have a separate width
       + ``len(barwidth)`` must equal ``len(points)``.

    .. warning::

       All methods except ``__init__`` are private.
    """
    def __init__(self, points, **attr):
        PolyBarsBase.__init__(self, points, attr)

    def calc_rect(self, x, y, w):
        """ Calculate the rectangle for plotting. """
        return self.scale_rect([[x - w / 2, y],      # left, top
                                [x + w / 2, 0]])     # right, bottom

    def draw(self, dc, printerScale, coord=None):
        """ Draw the bars """
        self.set_pen_and_brush(dc, printerScale)
        barwidth = self.attributes['barwidth']

        if coord is None:
            if isinstance(barwidth, (int, float)):
                # use a single width for all bars
                pts = ((x, y, barwidth) for x, y in self.points)
            elif isinstance(barwidth, (list, tuple)):
                # use a separate width for each bar
                if len(barwidth) != len(self.points):
                    err_str = ("Barwidth ({} items) and Points ({} items) do "
                               "not have the same length!")
                    err_str = err_str.format(len(barwidth), len(self.points))
                    raise ValueError(err_str)
                pts = ((x, y, w) for (x, y), w in zip(self.points, barwidth))
            else:
                # invalid attribute type
                err_str = ("Invalid type for 'barwidth'. Expected float, "
                           "int, or list or tuple of (int or float). Got {}.")
                raise TypeError(err_str.format(type(barwidth)))

            rects = [self.calc_rect(x, y, w) for x, y, w in pts]
            rects = [(int(r[0]), int(r[1]), int(r[2]), int(r[3])) for r in rects]
            dc.DrawRectangleList(rects)
        else:
            dc.DrawLines(coord)  # draw legend line


class PolyHistogram(PolyBarsBase):
    """
    Creates a PolyHistogram object.

    :param hist: The histogram data.
    :type hist: sequence of ``y`` values that define the heights of the bars
    :param binspec: The bin specification.
    :type binspec: sequence of ``x`` values that define the edges of the bins
    :param **attr: keyword attributes

    =================================  =============  =======================
    Keyword and Default                Description    Type
    =================================  =============  =======================
    ``edgecolour='black'``             edge color     :class:`wx.Colour`
    ``edgewidth=3``                    edge width     float
    ``edgestyle=wx.PENSTYLE_SOLID``    edge style     :class:`wx.PenStyle`
    ``fillcolour='blue'``              fill color     :class:`wx.Colour`
    ``fillstyle=wx.BRUSHSTYLE_SOLID``  fill style     :class:`wx.BrushStyle`
    ``legend=''``                      legend string  str
    =================================  =============  =======================

    .. tip::

       Use ``np.histogram()`` to easily create your histogram parameters::

         hist_data, binspec = np.histogram(data)
         hist_plot = PolyHistogram(hist_data, binspec)

    .. important::

       ``len(binspec)`` must equal ``len(hist) + 1``.

    .. warning::

       All methods except ``__init__`` are private.
    """
    def __init__(self, hist, binspec, **attr):
        if len(binspec) != len(hist) + 1:
            raise ValueError("Len(binspec) must equal len(hist) + 1")

        self.hist = hist
        self.binspec = binspec

        # define the bins and center x locations
        self.bins = list(pairwise(self.binspec))
        bar_center_x = (pair[0] + (pair[1] - pair[0])/2 for pair in self.bins)

        points = list(zip(bar_center_x, self.hist))
        PolyBarsBase.__init__(self, points, attr)

    def calc_rect(self, y, low, high):
        """ Calculate the rectangle for plotting. """
        return self.scale_rect([[low, y],       # left, top
                                [high, 0]])     # right, bottom

    def draw(self, dc, printerScale, coord=None):
        """ Draw the bars """
        self.set_pen_and_brush(dc, printerScale)

        if coord is None:
            rects = [self.calc_rect(y, low, high)
                     for y, (low, high)
                     in zip(self.hist, self.bins)]
            rects = [(int(r[0]), int(r[1]), int(r[2]), int(r[3]))
                     for r in rects]

            dc.DrawRectangleList(rects)
        else:
            dc.DrawLines(coord)  # draw legend line


class PolyBoxPlot(PolyPoints):
    """
    Creates a PolyBoxPlot object.

    :param data: Raw data to create a box plot from.
    :type data: sequence of int or float
    :param **attr: keyword attributes

    =================================  =============  =======================
    Keyword and Default                Description    Type
    =================================  =============  =======================
    ``colour='black'``                 edge color     :class:`wx.Colour`
    ``width=1``                        edge width     float
    ``style=wx.PENSTYLE_SOLID``        edge style     :class:`wx.PenStyle`
    ``legend=''``                      legend string  str
    =================================  =============  =======================

    .. note::

       ``np.NaN`` and ``np.inf`` values are ignored.

    .. admonition:: TODO

       + [ ] Figure out a better way to get multiple box plots side-by-side
         (current method is a hack).
       + [ ] change the X axis to some labels.
       + [ ] Change getClosestPoint to only grab box plot items and outlers?
         Currently grabs every data point.
       + [ ] Add more customization such as Pens/Brushes, outlier shapes/size,
         and box width.
       + [ ] Figure out how I want to handle log-y: log data then calcBP? Or
         should I calc the BP first then the plot it on a log scale?
    """
    _attributes = {'colour': 'black',
                   'width': 1,
                   'style': wx.PENSTYLE_SOLID,
                   'legend': '',
                   }

    def __init__(self, points, **attr):
        # Set various attributes
        self.box_width = 0.5

        # Determine the X position and create a 1d dataset.
        self.xpos = points[0, 0]
        points = points[:, 1]

        # Calculate the box plot points and the outliers
        self._bpdata = self.calcBpData(points)
        self._outliers = self.calcOutliers(points)
        points = np.concatenate((self._bpdata, self._outliers))
        points = np.array([(self.xpos, x) for x in points])

        # Create a jitter for the outliers
        self.jitter = (0.05 * np.random.random_sample(len(self._outliers))
                       + self.xpos - 0.025)

        # Init the parent class
        PolyPoints.__init__(self, points, attr)

    def _clean_data(self, data=None):
        """
        Removes NaN and Inf from the data.
        """
        if data is None:
            data = self.points

        # clean out NaN and infinity values.
        data = data[~np.isnan(data)]
        data = data[~np.isinf(data)]

        return data

    def boundingBox(self):
        """
        Returns bounding box for the plot.

        Override method.
        """
        xpos = self.xpos

        minXY = np.array([xpos - self.box_width / 2, self._bpdata.min * 0.95])
        maxXY = np.array([xpos + self.box_width / 2, self._bpdata.max * 1.05])
        return minXY, maxXY

    def getClosestPoint(self, pntXY, pointScaled=True):
        """
        Returns the index of closest point on the curve, pointXY,
        scaledXY, distance x, y in user coords.

        Override method.

        if pointScaled == True, then based on screen coords
        if pointScaled == False, then based on user coords
        """

        xpos = self.xpos

        # combine the outliers with the box plot data
        data_to_use = np.concatenate((self._bpdata, self._outliers))
        data_to_use = np.array([(xpos, x) for x in data_to_use])

        if pointScaled:
            # Use screen coords
            p = self.scaled
            pxy = self.currentScale * np.array(pntXY) + self.currentShift
        else:
            # Using user coords
            p = self._points
            pxy = np.array(pntXY)

        # determine distance for each point
        d = np.sqrt(np.add.reduce((p - pxy) ** 2, 1))  # sqrt(dx^2+dy^2)
        pntIndex = np.argmin(d)
        dist = d[pntIndex]
        return [pntIndex,
                self.points[pntIndex],
                self.scaled[pntIndex] / self._pointSize,
                dist]

    def getSymExtent(self, printerScale):
        """Width and Height of Marker"""
        # TODO: does this need to be updated?
        h = self.attributes['width'] * printerScale * self._pointSize[0]
        w = 5 * h
        return (w, h)

    def calcBpData(self, data=None):
        """
        Box plot points:

        Median (50%)
        75%
        25%
        low_whisker = lowest value that's >= (25% - (IQR * 1.5))
        high_whisker = highest value that's <= 75% + (IQR * 1.5)

        outliers are outside of 1.5 * IQR

        Parameters
        ----------
        data : array-like
            The data to plot

        Returns
        -------
        bpdata : collections.namedtuple
            Descriptive statistics for data:
            (min_data, low_whisker, q25, median, q75, high_whisker, max_data)

        """
        data = self._clean_data(data)

        min_data = float(np.min(data))
        max_data = float(np.max(data))
        q25 = float(np.percentile(data, 25))
        q75 = float(np.percentile(data, 75))

        iqr = q75 - q25

        low_whisker = float(data[data >= q25 - 1.5 * iqr].min())
        high_whisker = float(data[data <= q75 + 1.5 * iqr].max())

        median = float(np.median(data))

        BPData = namedtuple("bpdata", ("min", "low_whisker", "q25", "median",
                                       "q75", "high_whisker", "max"))

        bpdata = BPData(min_data, low_whisker, q25, median,
                        q75, high_whisker, max_data)

        return bpdata

    def calcOutliers(self, data=None):
        """
        Calculates the outliers. Must be called after calcBpData.
        """
        data = self._clean_data(data)

        outliers = data
        outlier_bool = np.logical_or(outliers > self._bpdata.high_whisker,
                                     outliers < self._bpdata.low_whisker)
        outliers = outliers[outlier_bool]
        return outliers

    def _scaleAndShift(self, data, scale=(1, 1), shift=(0, 0)):
        """same as override method, but returns a value."""
        scaled = scale * data + shift
        return scaled

    @TempStyle('pen')
    def draw(self, dc, printerScale, coord=None):
        """
        Draws a box plot on the DC.

        Notes
        -----
        The following draw order is required:

        1. First the whisker line
        2. Then the IQR box
        3. Lasly the median line.

        This is because

        + The whiskers are drawn as single line rather than two lines
        + The median line must be visible over the box if the box has a fill.

        Other than that, the draw order can be changed.
        """
        self._draw_whisker(dc, printerScale)
        self._draw_iqr_box(dc, printerScale)
        self._draw_median(dc, printerScale)     # median after box
        self._draw_whisker_ends(dc, printerScale)
        self._draw_outliers(dc, printerScale)

    @TempStyle('pen')
    def _draw_whisker(self, dc, printerScale):
        """Draws the whiskers as a single line"""
        xpos = self.xpos

        # We draw it as one line and then hide the middle part with
        # the IQR rectangle
        whisker_line = np.array([[xpos, self._bpdata.low_whisker],
                                 [xpos, self._bpdata.high_whisker]])

        whisker_line = self._scaleAndShift(whisker_line,
                                           self.currentScale,
                                           self.currentShift)

        whisker_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID)
        whisker_pen.SetCap(wx.CAP_BUTT)
        dc.SetPen(whisker_pen)
        dc.DrawLines(whisker_line)

    @TempStyle('pen')
    def _draw_iqr_box(self, dc, printerScale):
        """Draws the Inner Quartile Range box"""
        xpos = self.xpos
        box_w = self.box_width

        iqr_box = [[xpos - box_w / 2, self._bpdata.q75],  # left, top
                   [xpos + box_w / 2, self._bpdata.q25]]  # right, bottom

        # Scale it to the plot area
        iqr_box = self._scaleAndShift(iqr_box,
                                      self.currentScale,
                                      self.currentShift)

        # rectangles are drawn (left, top, width, height) so adjust
        iqr_box = [int(iqr_box[0][0]),                   # X (left)
                   int(iqr_box[0][1]),                   # Y (top)
                   int(iqr_box[1][0] - iqr_box[0][0]),   # Width
                   int(iqr_box[1][1] - iqr_box[0][1])]   # Height

        box_pen = wx.Pen(wx.BLACK, 3, wx.PENSTYLE_SOLID)
        box_brush = wx.Brush(wx.GREEN, wx.BRUSHSTYLE_SOLID)
        dc.SetPen(box_pen)
        dc.SetBrush(box_brush)

        dc.DrawRectangleList([iqr_box])

    @TempStyle('pen')
    def _draw_median(self, dc, printerScale, coord=None):
        """Draws the median line"""
        xpos = self.xpos

        median_line = np.array(
            [[xpos - self.box_width / 2, self._bpdata.median],
             [xpos + self.box_width / 2, self._bpdata.median]]
        )

        median_line = self._scaleAndShift(median_line,
                                          self.currentScale,
                                          self.currentShift)

        median_pen = wx.Pen(wx.BLACK, 4, wx.PENSTYLE_SOLID)
        median_pen.SetCap(wx.CAP_BUTT)
        dc.SetPen(median_pen)
        dc.DrawLines(median_line)

    @TempStyle('pen')
    def _draw_whisker_ends(self, dc, printerScale):
        """Draws the end caps of the whiskers"""
        xpos = self.xpos
        fence_top = np.array(
            [[xpos - self.box_width * 0.2, self._bpdata.high_whisker],
             [xpos + self.box_width * 0.2, self._bpdata.high_whisker]]
        )

        fence_top = self._scaleAndShift(fence_top,
                                        self.currentScale,
                                        self.currentShift)

        fence_bottom = np.array(
            [[xpos - self.box_width * 0.2, self._bpdata.low_whisker],
             [xpos + self.box_width * 0.2, self._bpdata.low_whisker]]
        )

        fence_bottom = self._scaleAndShift(fence_bottom,
                                           self.currentScale,
                                           self.currentShift)

        fence_pen = wx.Pen(wx.BLACK, 2, wx.PENSTYLE_SOLID)
        fence_pen.SetCap(wx.CAP_BUTT)
        dc.SetPen(fence_pen)
        dc.DrawLines(fence_top)
        dc.DrawLines(fence_bottom)

    @TempStyle('pen')
    def _draw_outliers(self, dc, printerScale):
        """Draws dots for the outliers"""
        # Set the pen
        outlier_pen = wx.Pen(wx.BLUE, 5, wx.PENSTYLE_SOLID)
        dc.SetPen(outlier_pen)

        outliers = self._outliers

        # Scale the data for plotting
        pt_data = np.array([self.jitter, outliers]).T
        pt_data = self._scaleAndShift(pt_data,
                                      self.currentScale,
                                      self.currentShift)

        # Draw the outliers
        size = 0.5
        fact = 2.5 * size
        wh = 5.0 * size
        rect = np.zeros((len(pt_data), 4), float) + [0.0, 0.0, wh, wh]
        rect[:, 0:2] = pt_data - [fact, fact]
        dc.DrawRectangleList(rect.astype(np.int32))


class PlotGraphics(object):
    """
    Creates a PlotGraphics object.

    :param objects: The Poly objects to plot.
    :type objects: list of :class:`~wx.lib.plot.PolyPoints` objects
    :param title: The title shown at the top of the graph.
    :type title: str
    :param xLabel: The x-axis label.
    :type xLabel: str
    :param yLabel: The y-axis label.
    :type yLabel: str

    .. warning::

       All methods except ``__init__`` are private.
    """
    def __init__(self, objects, title='', xLabel='', yLabel=''):
        if type(objects) not in [list, tuple]:
            raise TypeError("objects argument should be list or tuple")
        self.objects = objects
        self._title = title
        self._xLabel = xLabel
        self._yLabel = yLabel
        self._pointSize = (1.0, 1.0)

    @property
    def logScale(self):
        if len(self.objects) == 0:
            return
        return [obj.logScale for obj in self.objects]

    @logScale.setter
    def logScale(self, logscale):
        # XXX: error checking done by PolyPoints class
#        if not isinstance(logscale, tuple) and len(logscale) != 2:
#            raise TypeError("logscale must be a 2-tuple of bools")
        if len(self.objects) == 0:
            return
        for obj in self.objects:
            obj.logScale = logscale

    def setLogScale(self, logscale):
        """
        Set the log scale boolean value.

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.logScale`
           property instead.
        """
        pendingDeprecation("self.logScale property")
        self.logScale = logscale

    @property
    def absScale(self):
        if len(self.objects) == 0:
            return
        return [obj.absScale for obj in self.objects]

    @absScale.setter
    def absScale(self, absscale):
        # XXX: error checking done by PolyPoints class
#        if not isinstance(absscale, tuple) and len(absscale) != 2:
#            raise TypeError("absscale must be a 2-tuple of bools")
        if len(self.objects) == 0:
            return
        for obj in self.objects:
            obj.absScale = absscale

    def boundingBox(self):
        p1, p2 = self.objects[0].boundingBox()
        for o in self.objects[1:]:
            p1o, p2o = o.boundingBox()
            p1 = np.minimum(p1, p1o)
            p2 = np.maximum(p2, p2o)
        return p1, p2

    def scaleAndShift(self, scale=(1, 1), shift=(0, 0)):
        for o in self.objects:
            o.scaleAndShift(scale, shift)

    def setPrinterScale(self, scale):
        """
        Thickens up lines and markers only for printing

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.printerScale`
           property instead.
        """
        pendingDeprecation("self.printerScale property")
        self.printerScale = scale

    def setXLabel(self, xLabel=''):
        """
        Set the X axis label on the graph

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel`
           property instead.
        """
        pendingDeprecation("self.xLabel property")
        self.xLabel = xLabel

    def setYLabel(self, yLabel=''):
        """
        Set the Y axis label on the graph

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel`
           property instead.
        """
        pendingDeprecation("self.yLabel property")
        self.yLabel = yLabel

    def setTitle(self, title=''):
        """
        Set the title at the top of graph

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title`
           property instead.
        """
        pendingDeprecation("self.title property")
        self.title = title

    def getXLabel(self):
        """
        Get X axis label string

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.xLabel`
           property instead.
        """
        pendingDeprecation("self.xLabel property")
        return self.xLabel

    def getYLabel(self):
        """
        Get Y axis label string

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.yLabel`
           property instead.
        """
        pendingDeprecation("self.yLabel property")
        return self.yLabel

    def getTitle(self, title=''):
        """
        Get the title at the top of graph

        .. deprecated:: Feb 27, 2016

           Use the :attr:`~wx.lib.plot.polyobjects.PlotGraphics.title`
           property instead.
        """
        pendingDeprecation("self.title property")
        return self.title

    @property
    def printerScale(self):
        return self._printerScale

    @printerScale.setter
    def printerScale(self, scale):
        """Thickens up lines and markers only for printing"""
        self._printerScale = scale

    @property
    def xLabel(self):
        """Get the X axis label on the graph"""
        return self._xLabel

    @xLabel.setter
    def xLabel(self, text):
        self._xLabel = text

    @property
    def yLabel(self):
        """Get the Y axis label on the graph"""
        return self._yLabel

    @yLabel.setter
    def yLabel(self, text):
        self._yLabel = text

    @property
    def title(self):
        """Get the title at the top of graph"""
        return self._title

    @title.setter
    def title(self, text):
        self._title = text

    def draw(self, dc):
        for o in self.objects:
#            t=_time.perf_counter()          # profile info
            o._pointSize = self._pointSize
            o.draw(dc, self._printerScale)
#            print(o, "time=", _time.perf_counter()-t)

    def getSymExtent(self, printerScale):
        """Get max width and height of lines and markers symbols for legend"""
        self.objects[0]._pointSize = self._pointSize
        symExt = self.objects[0].getSymExtent(printerScale)
        for o in self.objects[1:]:
            o._pointSize = self._pointSize
            oSymExt = o.getSymExtent(printerScale)
            symExt = np.maximum(symExt, oSymExt)
        return symExt

    def getLegendNames(self):
        """Returns list of legend names"""
        lst = [None] * len(self)
        for i in range(len(self)):
            lst[i] = self.objects[i].getLegend()
        return lst

    def __len__(self):
        return len(self.objects)

    def __getitem__(self, item):
        return self.objects[item]


# -------------------------------------------------------------------------
# Used to layout the printer page


class PlotPrintout(wx.Printout):
    """Controls how the plot is made in printing and previewing"""
    # Do not change method names in this class,
    # we have to override wx.Printout methods here!

    def __init__(self, graph):
        """graph is instance of plotCanvas to be printed or previewed"""
        wx.Printout.__init__(self)
        self.graph = graph

    def HasPage(self, page):
        if page == 1:
            return True
        else:
            return False

    def GetPageInfo(self):
        return (1, 1, 1, 1)  # disable page numbers

    def OnPrintPage(self, page):
        dc = self.GetDC()  # allows using floats for certain functions
#        print("PPI Printer",self.GetPPIPrinter())
#        print("PPI Screen", self.GetPPIScreen())
#        print("DC GetSize", dc.GetSize())
#        print("GetPageSizePixels", self.GetPageSizePixels())
        # Note PPIScreen does not give the correct number
        # Calculate everything for printer and then scale for preview
        PPIPrinter = self.GetPPIPrinter()        # printer dots/inch (w,h)
        # PPIScreen= self.GetPPIScreen()          # screen dots/inch (w,h)
        dcSize = dc.GetSize()                    # DC size
        if self.graph._antiAliasingEnabled and not isinstance(dc, wx.GCDC):
            try:
                dc = wx.GCDC(dc)
            except Exception:
                pass
            else:
                if self.graph._hiResEnabled:
                    # high precision - each logical unit is 1/20 of a point
                    dc.SetMapMode(wx.MM_TWIPS)
        pageSize = self.GetPageSizePixels()  # page size in terms of pixcels
        clientDcSize = self.graph.GetClientSize()

        # find what the margins are (mm)
        pgSetupData = self.graph.pageSetupData
        margLeftSize, margTopSize = pgSetupData.GetMarginTopLeft()
        margRightSize, margBottomSize = pgSetupData.GetMarginBottomRight()

        # calculate offset and scale for dc
        pixLeft = margLeftSize * PPIPrinter[0] / 25.4  # mm*(dots/in)/(mm/in)
        pixRight = margRightSize * PPIPrinter[0] / 25.4
        pixTop = margTopSize * PPIPrinter[1] / 25.4
        pixBottom = margBottomSize * PPIPrinter[1] / 25.4

        plotAreaW = pageSize[0] - (pixLeft + pixRight)
        plotAreaH = pageSize[1] - (pixTop + pixBottom)

        # ratio offset and scale to screen size if preview
        if self.IsPreview():
            ratioW = float(dcSize[0]) / pageSize[0]
            ratioH = float(dcSize[1]) / pageSize[1]
            pixLeft *= ratioW
            pixTop *= ratioH
            plotAreaW *= ratioW
            plotAreaH *= ratioH

        # rescale plot to page or preview plot area
        self.graph._setSize(plotAreaW, plotAreaH)

        # Set offset and scale
        dc.SetDeviceOrigin(int(pixLeft), int(pixTop))

        # Thicken up pens and increase marker size for printing
        ratioW = float(plotAreaW) / clientDcSize[0]
        ratioH = float(plotAreaH) / clientDcSize[1]
        aveScale = (ratioW + ratioH) / 2
        if self.graph._antiAliasingEnabled and not self.IsPreview():
            scale = dc.GetUserScale()
            dc.SetUserScale(scale[0] / self.graph._pointSize[0],
                            scale[1] / self.graph._pointSize[1])
        self.graph._setPrinterScale(aveScale)  # tickens up pens for printing

        self.graph._printDraw(dc)
        # rescale back to original
        self.graph._setSize()
        self.graph._setPrinterScale(1)
        self.graph.Redraw()  # to get point label scale and shift correct

        return True
