1525 lines
51 KiB
Python
1525 lines
51 KiB
Python
# -*- 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.keys():
|
|
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 bouding 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 alltogether? 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, 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:
|
|
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))
|
|
|
|
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, width, style)
|
|
pen.SetCap(wx.CAP_ROUND)
|
|
dc.SetPen(pen)
|
|
if coord is None:
|
|
if len(self.scaled) >= 3:
|
|
dc.DrawSpline(self.scaled)
|
|
else:
|
|
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, 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), np.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):
|
|
dc.DrawPointList(coords)
|
|
|
|
def _square(self, dc, coords, size=1):
|
|
fact = 2.5 * size
|
|
wh = 5.0 * size
|
|
rect = np.zeros((len(coords), 4), np.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 retuns 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, 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]
|
|
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)]
|
|
|
|
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 distnace 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 retuns 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 visable 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 = [iqr_box[0][0], # X (left)
|
|
iqr_box[0][1], # Y (top)
|
|
iqr_box[1][0] - iqr_box[0][0], # Width
|
|
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), np.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
|
|
# Calulate 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(pixLeft, 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
|