# -*- coding: utf-8 -*-
#
# Licensed under the terms of the Qwt License
# Copyright (c) 2002 Uwe Rathmann, for the original C++ code
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
"""
QwtScaleEngine
--------------
.. autoclass:: QwtScaleEngine
:members:
QwtLinearScaleEngine
--------------------
.. autoclass:: QwtLinearScaleEngine
:members:
QwtLogScaleEngine
-----------------
.. autoclass:: QwtLogScaleEngine
:members:
"""
from __future__ import division
from qwt.interval import QwtInterval
from qwt.scale_div import QwtScaleDiv
from qwt.transform import QwtLogTransform
from qwt.math import qwtFuzzyCompare
from qwt.transform import QwtTransform
from qwt.qt.QtCore import qFuzzyCompare
import numpy as np
DBL_MAX = np.finfo(float).max
LOG_MIN = 1.0e-100
LOG_MAX = 1.0e100
def qwtLog(base, value):
return np.log(value)/np.log(base)
def qwtLogInterval(base, interval):
return QwtInterval(qwtLog(base, interval.minValue()),
qwtLog(base, interval.maxValue()))
def qwtPowInterval(base, interval):
return QwtInterval(np.power(base, interval.minValue()),
np.power(base, interval.maxValue()))
[docs]def qwtStepSize(intervalSize, maxSteps, base):
"""this version often doesn't find the best ticks: f.e for 15: 5, 10"""
minStep = divideInterval(intervalSize, maxSteps, base)
if minStep != 0.:
# # ticks per interval
numTicks = np.ceil(abs(intervalSize/minStep))-1
# Do the minor steps fit into the interval?
if qwtFuzzyCompare((numTicks+1)*abs(minStep),
abs(intervalSize), intervalSize) > 0:
# The minor steps doesn't fit into the interval
return .5*intervalSize
return minStep
EPS = 1.0e-6
[docs]def ceilEps(value, intervalSize):
"""
Ceil a value, relative to an interval
:param float value: Value to be ceiled
:param float intervalSize: Interval size
:return: Rounded value
.. seealso::
:py:func:`qwt.scale_engine.floorEps()`
"""
eps = EPS*intervalSize
value = (value-eps)/intervalSize
return np.ceil(value)*intervalSize
[docs]def floorEps(value, intervalSize):
"""
Floor a value, relative to an interval
:param float value: Value to be floored
:param float intervalSize: Interval size
:return: Rounded value
.. seealso::
:py:func:`qwt.scale_engine.ceilEps()`
"""
eps = EPS*intervalSize
value = (value+eps)/intervalSize
return np.floor(value)*intervalSize
[docs]def divideEps(intervalSize, numSteps):
"""
Divide an interval into steps
`stepSize = (intervalSize - intervalSize * 10**-6) / numSteps`
:param float intervalSize: Interval size
:param float numSteps: Number of steps
:return: Step size
"""
if numSteps == 0. or intervalSize == 0.:
return 0.
return (intervalSize-(EPS*intervalSize))/numSteps
[docs]def divideInterval(intervalSize, numSteps, base):
"""
Calculate a step size for a given interval
:param float intervalSize: Interval size
:param float numSteps: Number of steps
:param int base: Base for the division (usually 10)
:return: Calculated step size
"""
if numSteps <= 0:
return 0.
v = divideEps(intervalSize, numSteps)
if v == 0.:
return 0.
lx = qwtLog(base, abs(v))
p = np.floor(lx)
fraction = np.power(base, lx-p)
n = base
while n > 1 and fraction <= n/2:
n /= 2
stepSize = n*np.power(base, p)
if v < 0:
stepSize = -stepSize
return stepSize
class QwtScaleEngine_PrivateData(object):
def __init__(self):
self.attributes = QwtScaleEngine.NoAttribute
self.lowerMargin = 0.
self.upperMargin = 0.
self.referenceValue = 0.
self.base = 10
self.transform = None # QwtTransform
[docs]class QwtScaleEngine(object):
"""
Base class for scale engines.
A scale engine tries to find "reasonable" ranges and step sizes
for scales.
The layout of the scale can be varied with `setAttribute()`.
`python-qwt` offers implementations for logarithmic and linear scales.
Layout attributes:
* `QwtScaleEngine.NoAttribute`: No attributes
* `QwtScaleEngine.IncludeReference`: Build a scale which includes the
`reference()` value
* `QwtScaleEngine.Symmetric`: Build a scale which is symmetric to the
`reference()` value
* `QwtScaleEngine.Floating`: The endpoints of the scale are supposed to
be equal the outmost included values plus the specified margins (see
`setMargins()`). If this attribute is *not* set, the endpoints of the
scale will be integer multiples of the step size.
* `QwtScaleEngine.Inverted`: Turn the scale upside down
"""
# enum Attribute
NoAttribute = 0x00
IncludeReference = 0x01
Symmetric = 0x02
Floating = 0x04
Inverted = 0x08
def __init__(self, base=10):
self.__data = QwtScaleEngine_PrivateData()
self.setBase(base)
[docs] def autoScale(self, maxNumSteps, x1, x2, stepSize):
"""
Align and divide an interval
:param int maxNumSteps: Max. number of steps
:param float x1: First limit of the interval (In/Out)
:param float x2: Second limit of the interval (In/Out)
:param float stepSize: Step size
:return: tuple (x1, x2, stepSize)
"""
pass
[docs] def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.):
"""
Calculate a scale division
:param float x1: First interval limit
:param float x2: Second interval limit
:param int maxMajorSteps: Maximum for the number of major steps
:param int maxMinorSteps: Maximum number of minor steps
:param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one
:return: Calculated scale division
"""
pass
[docs] def lowerMargin(self):
"""
:return: the margin at the lower end of the scale
The default margin is 0.
.. seealso::
:py:meth:`setMargins()`
"""
return self.__data.lowerMargin
[docs] def upperMargin(self):
"""
:return: the margin at the upper end of the scale
The default margin is 0.
.. seealso::
:py:meth:`setMargins()`
"""
return self.__data.upperMargin
[docs] def setMargins(self, lower, upper):
"""
Specify margins at the scale's endpoints
:param float lower: minimum distance between the scale's lower boundary and the smallest enclosed value
:param float upper: minimum distance between the scale's upper boundary and the greatest enclosed value
:return: A clone of the transfomation
Margins can be used to leave a minimum amount of space between
the enclosed intervals and the boundaries of the scale.
.. warning::
`QwtLogScaleEngine` measures the margins in decades.
.. seealso::
:py:meth:`upperMargin()`, :py:meth:`lowerMargin()`
"""
self.__data.lowerMargin = max([lower, 0.])
self.__data.upperMargin = max([upper, 0.])
[docs] def divideInterval(self, intervalSize, numSteps):
"""
Calculate a step size for a given interval
:param float intervalSize: Interval size
:param float numSteps: Number of steps
:return: Step size
"""
return divideInterval(intervalSize, numSteps, self.__data.base)
[docs] def contains(self, interval, value):
"""
Check if an interval "contains" a value
:param float intervalSize: Interval size
:param float value: Value
:return: True, when the value is inside the interval
"""
if not interval.isValid():
return False
elif qwtFuzzyCompare(value, interval.minValue(), interval.width()) < 0:
return False
elif qwtFuzzyCompare(value, interval.maxValue(), interval.width()) > 0:
return False
else:
return True
[docs] def strip(self, ticks, interval):
"""
Remove ticks from a list, that are not inside an interval
:param list ticks: Tick list
:param qwt.interval.QwtInterval interval: Interval
:return: Stripped tick list
"""
if not interval.isValid() or not ticks:
return []
if self.contains(interval, ticks[0]) and\
self.contains(interval, ticks[-1]):
return ticks
return [tick for tick in ticks
if self.contains(interval, tick)]
[docs] def buildInterval(self, value):
"""
Build an interval around a value
In case of v == 0.0 the interval is [-0.5, 0.5],
otherwide it is [0.5 * v, 1.5 * v]
:param float value: Initial value
:return: Calculated interval
"""
if value == 0.:
delta = .5
else:
delta = abs(.5*value)
if DBL_MAX-delta < value:
return QwtInterval(DBL_MAX-delta, DBL_MAX)
if -DBL_MAX+delta > value:
return QwtInterval(-DBL_MAX, -DBL_MAX+delta)
return QwtInterval(value-delta, value+delta)
[docs] def setAttribute(self, attribute, on=True):
"""
Change a scale attribute
:param int attribute: Attribute to change
:param bool on: On/Off
:return: Calculated interval
.. seealso::
:py:meth:`testAttribute()`
"""
if on:
self.__data.attributes |= attribute
else:
self.__data.attributes &= ~attribute
[docs] def testAttribute(self, attribute):
"""
:param int attribute: Attribute to be tested
:return: True, if attribute is enabled
.. seealso::
:py:meth:`setAttribute()`
"""
return self.__data.attributes & attribute
[docs] def setAttributes(self, attributes):
"""
Change the scale attribute
:param attributes: Set scale attributes
.. seealso::
:py:meth:`attributes()`
"""
self.__data.attributes = attributes
[docs] def attributes(self):
"""
:return: Scale attributes
.. seealso::
:py:meth:`setAttributes()`, :py:meth:`testAttribute()`
"""
return self.__data.attributes
[docs] def setReference(self, r):
"""
Specify a reference point
:param float r: new reference value
The reference point is needed if options `IncludeReference` or
`Symmetric` are active. Its default value is 0.0.
"""
self.__data.referenceValue = r
[docs] def reference(self):
"""
:return: the reference value
.. seealso::
:py:meth:`setReference()`, :py:meth:`setAttribute()`
"""
return self.__data.referenceValue
[docs] def setBase(self, base):
"""
Set the base of the scale engine
While a base of 10 is what 99.9% of all applications need
certain scales might need a different base: f.e 2
The default setting is 10
:param int base: Base of the engine
.. seealso::
:py:meth:`base()`
"""
self.__data.base = max([base, 2])
[docs] def base(self):
"""
:return: Base of the scale engine
.. seealso::
:py:meth:`setBase()`
"""
return self.__data.base
[docs]class QwtLinearScaleEngine(QwtScaleEngine):
"""
A scale engine for linear scales
The step size will fit into the pattern
\f$\left\{ 1,2,5\right\} \cdot 10^{n}\f$, where n is an integer.
"""
def __init__(self, base=10):
super(QwtLinearScaleEngine, self).__init__(base)
[docs] def autoScale(self, maxNumSteps, x1, x2, stepSize):
"""
Align and divide an interval
:param int maxNumSteps: Max. number of steps
:param float x1: First limit of the interval (In/Out)
:param float x2: Second limit of the interval (In/Out)
:param float stepSize: Step size
:return: tuple (x1, x2, stepSize)
.. seealso::
:py:meth:`setAttribute()`
"""
interval = QwtInterval(x1, x2)
interval = interval.normalized()
interval.setMinValue(interval.minValue()-self.lowerMargin())
interval.setMaxValue(interval.maxValue()+self.upperMargin())
if self.testAttribute(QwtScaleEngine.Symmetric):
interval = interval.symmetrize(self.reference())
if self.testAttribute(QwtScaleEngine.IncludeReference):
interval = interval.extend(self.reference())
if interval.width() == 0.:
interval = self.buildInterval(interval.minValue())
stepSize = divideInterval(interval.width(),
max([maxNumSteps, 1]), self.base())
if not self.testAttribute(QwtScaleEngine.Floating):
interval = self.align(interval, stepSize)
x1 = interval.minValue()
x2 = interval.maxValue()
if self.testAttribute(QwtScaleEngine.Inverted):
x1, x2 = x2, x1
stepSize = -stepSize
return x1, x2, stepSize
[docs] def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.):
"""
Calculate a scale division for an interval
:param float x1: First interval limit
:param float x2: Second interval limit
:param int maxMajorSteps: Maximum for the number of major steps
:param int maxMinorSteps: Maximum number of minor steps
:param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one
:return: Calculated scale division
"""
interval = QwtInterval(x1, x2).normalized()
if interval.width() <= 0:
return QwtScaleDiv()
stepSize = abs(stepSize)
if stepSize == 0.:
if maxMajorSteps < 1:
maxMajorSteps = 1
stepSize = divideInterval(interval.width(), maxMajorSteps,
self.base())
scaleDiv = QwtScaleDiv()
if stepSize != 0.:
ticks = self.buildTicks(interval, stepSize, maxMinorSteps)
scaleDiv = QwtScaleDiv(interval, ticks)
if x1 > x2:
scaleDiv.invert()
return scaleDiv
[docs] def buildTicks(self, interval, stepSize, maxMinorSteps):
"""
Calculate ticks for an interval
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:param int maxMinorSteps: Maximum number of minor steps
:return: Calculated ticks
"""
ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)]
boundingInterval = self.align(interval, stepSize)
ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval,
stepSize)
if maxMinorSteps > 0:
self.buildMinorTicks(ticks, maxMinorSteps, stepSize)
for i in range(QwtScaleDiv.NTickTypes):
ticks[i] = self.strip(ticks[i], interval)
for j in range(len(ticks[i])):
if qwtFuzzyCompare(ticks[i][j], 0., stepSize) == 0:
ticks[i][j] = 0.
return ticks
[docs] def buildMajorTicks(self, interval, stepSize):
"""
Calculate major ticks for an interval
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:return: Calculated ticks
"""
numTicks = min([round(interval.width()/stepSize)+1, 10000])
ticks = [interval.minValue()]
for i in range(1, int(numTicks-1)):
ticks += [interval.minValue()+i*stepSize]
ticks += [interval.maxValue()]
return ticks
[docs] def buildMinorTicks(self, ticks, maxMinorSteps, stepSize):
"""
Calculate minor ticks for an interval
:param list ticks: Major ticks (returned)
:param int maxMinorSteps: Maximum number of minor steps
:param float stepSize: Step size
"""
minStep = qwtStepSize(stepSize, maxMinorSteps, self.base())
if minStep == 0.:
return
numTicks = int(np.ceil(abs(stepSize/minStep))-1)
medIndex = -1
if numTicks % 2:
medIndex = numTicks/2
for val in ticks[QwtScaleDiv.MajorTick]:
for k in range(numTicks):
val += minStep
alignedValue = val
if qwtFuzzyCompare(val, 0., stepSize) == 0:
alignedValue = 0.
if k == medIndex:
ticks[QwtScaleDiv.MediumTick] += [alignedValue]
else:
ticks[QwtScaleDiv.MinorTick] += [alignedValue]
[docs] def align(self, interval, stepSize):
"""
Align an interval to a step size
The limits of an interval are aligned that both are integer
multiples of the step size.
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:return: Aligned interval
"""
x1 = interval.minValue()
x2 = interval.maxValue()
if -DBL_MAX+stepSize <= x1:
x = floorEps(x1, stepSize)
if qwtFuzzyCompare(x1, x, stepSize) != 0:
x1 = x
if DBL_MAX-stepSize >= x2:
x = ceilEps(x2, stepSize)
if qwtFuzzyCompare(x2, x, stepSize) != 0:
x2 = x
return QwtInterval(x1, x2)
[docs]class QwtLogScaleEngine(QwtScaleEngine):
"""
A scale engine for logarithmic scales
The step size is measured in *decades* and the major step size will be
adjusted to fit the pattern {1,2,3,5}.10**n, where n is a natural number
including zero.
.. warning::
The step size as well as the margins are measured in *decades*.
"""
def __init__(self, base=10):
super(QwtLogScaleEngine, self).__init__(base)
self.setTransformation(QwtLogTransform())
[docs] def autoScale(self, maxNumSteps, x1, x2, stepSize):
"""
Align and divide an interval
:param int maxNumSteps: Max. number of steps
:param float x1: First limit of the interval (In/Out)
:param float x2: Second limit of the interval (In/Out)
:param float stepSize: Step size
:return: tuple (x1, x2, stepSize)
.. seealso::
:py:meth:`setAttribute()`
"""
if x1 > x2:
x1, x2 = x2, x1
logBase = self.base()
interval = QwtInterval(x1/np.power(logBase, self.lowerMargin()),
x2*np.power(logBase, self.upperMargin()))
if interval.maxValue()/interval.minValue() < logBase:
linearScaler = QwtLinearScaleEngine()
linearScaler.setAttributes(self.attributes())
linearScaler.setReference(self.reference())
linearScaler.setMargins(self.lowerMargin(), self.upperMargin())
x1, x2, stepSize = linearScaler.autoScale(maxNumSteps,
x1, x2, stepSize)
linearInterval = QwtInterval(x1, x2).normalized()
linearInterval = linearInterval.limited(LOG_MIN, LOG_MAX)
if linearInterval.maxValue()/linearInterval.minValue() < logBase:
if stepSize < 0.:
stepSize = -qwtLog(logBase, abs(stepSize))
else:
stepSize = qwtLog(logBase, stepSize)
return x1, x2, stepSize
logRef = 1.
if self.reference() > LOG_MIN/2:
logRef = min([self.reference(), LOG_MAX/2])
if self.testAttribute(QwtScaleEngine.Symmetric):
delta = max([interval.maxValue()/logRef,
logRef/interval.minValue()])
interval.setInterval(logRef/delta, logRef*delta)
if self.testAttribute(QwtScaleEngine.IncludeReference):
interval = interval.extend(logRef)
interval = interval.limited(LOG_MIN, LOG_MAX)
if interval.width() == 0.:
interval = self.buildInterval(interval.minValue())
stepSize = self.divideInterval(
qwtLogInterval(logBase, interval).width(), max([maxNumSteps, 1]))
if stepSize < 1.:
stepSize = 1.
if not self.testAttribute(QwtScaleEngine.Floating):
interval = self.align(interval, stepSize)
x1 = interval.minValue()
x2 = interval.maxValue()
if self.testAttribute(QwtScaleEngine.Inverted):
x1, x2 = x2, x1
stepSize = -stepSize
return x1, x2, stepSize
[docs] def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.):
"""
Calculate a scale division for an interval
:param float x1: First interval limit
:param float x2: Second interval limit
:param int maxMajorSteps: Maximum for the number of major steps
:param int maxMinorSteps: Maximum number of minor steps
:param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one
:return: Calculated scale division
"""
interval = QwtInterval(x1, x2).normalized()
interval = interval.limited(LOG_MIN, LOG_MAX)
if interval.width() <= 0:
return QwtScaleDiv()
logBase = self.base()
if interval.maxValue()/interval.minValue() < logBase:
linearScaler = QwtLinearScaleEngine()
linearScaler.setAttributes(self.attributes())
linearScaler.setReference(self.reference())
linearScaler.setMargins(self.lowerMargin(), self.upperMargin())
if stepSize != 0.:
if stepSize < 0.:
stepSize = -np.power(logBase, -stepSize)
else:
stepSize = np.power(logBase, stepSize)
return linearScaler.divideScale(x1, x2, maxMajorSteps,
maxMinorSteps, stepSize)
stepSize = abs(stepSize)
if stepSize == 0.:
if maxMajorSteps < 1:
maxMajorSteps = 1
stepSize = self.divideInterval(
qwtLogInterval(logBase, interval).width(), maxMajorSteps)
if stepSize < 1.:
stepSize = 1.
scaleDiv = QwtScaleDiv()
if stepSize != 0.:
ticks = self.buildTicks(interval, stepSize, maxMinorSteps)
scaleDiv = QwtScaleDiv(interval, ticks)
if x1 > x2:
scaleDiv.invert()
return scaleDiv
[docs] def buildTicks(self, interval, stepSize, maxMinorSteps):
"""
Calculate ticks for an interval
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:param int maxMinorSteps: Maximum number of minor steps
:return: Calculated ticks
"""
ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)]
boundingInterval = self.align(interval, stepSize)
ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval,
stepSize)
if maxMinorSteps > 0:
self.buildMinorTicks(ticks, maxMinorSteps, stepSize)
for i in range(QwtScaleDiv.NTickTypes):
ticks[i] = self.strip(ticks[i], interval)
return ticks
[docs] def buildMajorTicks(self, interval, stepSize):
"""
Calculate major ticks for an interval
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:return: Calculated ticks
"""
width = qwtLogInterval(self.base(), interval).width()
numTicks = min([int(round(width/stepSize))+1, 10000])
lxmin = np.log(interval.minValue())
lxmax = np.log(interval.maxValue())
lstep = (lxmax-lxmin)/float(numTicks-1)
ticks = [interval.minValue()]
for i in range(1, numTicks-1):
ticks += [np.exp(lxmin+float(i)*lstep)]
ticks += [interval.maxValue()]
return ticks
[docs] def buildMinorTicks(self, ticks, maxMinorSteps, stepSize):
"""
Calculate minor ticks for an interval
:param list ticks: Major ticks (returned)
:param int maxMinorSteps: Maximum number of minor steps
:param float stepSize: Step size
"""
logBase = self.base()
if stepSize < 1.1:
minStep = self.divideInterval(stepSize, maxMinorSteps+1)
if minStep == 0.:
return
numSteps = int(round(stepSize/minStep))
mediumTickIndex = -1
if numSteps > 2 and numSteps % 2 == 0:
mediumTickIndex = numSteps/2
for v in ticks[QwtScaleDiv.MajorTick]:
s = logBase/numSteps
if s >= 1.:
if not qFuzzyCompare(s, 1.):
ticks[QwtScaleDiv.MinorTick] += [v*s]
for j in range(2, numSteps):
ticks[QwtScaleDiv.MinorTick] += [v*j*s]
else:
for j in range(1, numSteps):
tick = v + j*v*(logBase-1)/numSteps
if j == mediumTickIndex:
ticks[QwtScaleDiv.MediumTick] += [tick]
else:
ticks[QwtScaleDiv.MinorTick] += [tick]
else:
minStep = self.divideInterval(stepSize, maxMinorSteps)
if minStep == 0.:
return
if minStep < 1.:
minStep = 1.
numTicks = int(round(stepSize/minStep))-1
if qwtFuzzyCompare((numTicks+1)*minStep, stepSize, stepSize) > 0:
numTicks = 0
if numTicks < 1:
return
mediumTickIndex = -1
if numTicks > 2 and numTicks % 2:
mediumTickIndex = numTicks/2
minFactor = max([np.power(logBase, minStep), float(logBase)])
for tick in ticks[QwtScaleDiv.MajorTick]:
for j in range(numTicks):
tick *= minFactor
if j == mediumTickIndex:
ticks[QwtScaleDiv.MediumTick] += [tick]
else:
ticks[QwtScaleDiv.MinorTick] += [tick]
[docs] def align(self, interval, stepSize):
"""
Align an interval to a step size
The limits of an interval are aligned that both are integer
multiples of the step size.
:param qwt.interval.QwtInterval interval: Interval
:param float stepSize: Step size
:return: Aligned interval
"""
intv = qwtLogInterval(self.base(), interval)
x1 = floorEps(intv.minValue(), stepSize)
if qwtFuzzyCompare(interval.minValue(), x1, stepSize) == 0:
x1 = interval.minValue()
x2 = ceilEps(intv.maxValue(), stepSize)
if qwtFuzzyCompare(interval.maxValue(), x2, stepSize) == 0:
x2 = interval.maxValue()
return qwtPowInterval(self.base(), QwtInterval(x1, x2))