mirror of
https://github.com/wxWidgets/Phoenix.git
synced 2026-01-09 21:40:34 +01:00
In Python 3.10, a change[1] was implemented where extension functions
that take integer arguments will no longer silently accept non-integer
arguments (e.g., floats) that can only be converted to integers with a
loss of precision. This PR fixes most of these issues in the pure-Python
classes and demos by explicitly converting the parameters to int before
passing them to wxWidgets. There is loss of precision, but this was
happening before (automatically) anyway as most wxWidgets DeviceContext
functions operate using integers.
Additionally, the PR fixes a few sizing issues, mostly with SpinCtrls being
too small on GTK3.
This is an example of the relevant exception:
Traceback (most recent call last):
File "/usr/lib64/python3.10/site-packages/wx/lib/agw/pygauge.py", line 355, in OnPaint
r.width = w
TypeError: 'float' object cannot be interpreted as an integer
Fixes #2038.
[1] https://bugs.python.org/issue37999
1529 lines
52 KiB
Python
1529 lines
52 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:
|
|
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, 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:
|
|
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, int(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, 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), 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):
|
|
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), 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, 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 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 = [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), 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(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
|