# -*- 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:
"""
import math
import sys
import numpy as np
from qtpy.QtCore import qFuzzyCompare
from qwt._math import qwtFuzzyCompare
from qwt.interval import QwtInterval
from qwt.scale_div import QwtScaleDiv
from qwt.transform import QwtLogTransform, QwtTransform
DBL_MAX = sys.float_info.max
LOG_MIN = 1.0e-100
LOG_MAX = 1.0e100
def qwtLogInterval(base, interval):
return QwtInterval(
math.log(interval.minValue(), base), math.log(interval.maxValue(), base)
)
def qwtPowInterval(base, interval):
return QwtInterval(
math.pow(base, interval.minValue()), math.pow(base, interval.maxValue())
)
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.0:
# # ticks per interval
numTicks = math.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 0.5 * intervalSize
return minStep
EPS = 1.0e-6
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 math.ceil(value) * intervalSize
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 math.floor(value) * intervalSize
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.0 or intervalSize == 0.0:
return 0.0
return (intervalSize - (EPS * intervalSize)) / numSteps
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.0
v = divideEps(intervalSize, numSteps)
if v == 0.0:
return 0.0
lx = math.log(abs(v), base)
p = math.floor(lx)
fraction = math.pow(base, lx - p)
n = base
while n > 1 and fraction <= n // 2:
n //= 2
stepSize = n * math.pow(base, p)
if v < 0:
stepSize = -stepSize
return stepSize
class QwtScaleEngine_PrivateData(object):
def __init__(self):
self.attributes = QwtScaleEngine.NoAttribute
self.lowerMargin = 0.0
self.upperMargin = 0.0
self.referenceValue = 0.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()`.
`PythonQwt` 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.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.0])
self.__data.upperMargin = max([upper, 0.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
eps = abs(1.0e-6 * interval.width())
if interval.minValue() - value > eps or value - interval.maxValue() > eps:
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.0:
delta = 0.5
else:
delta = abs(0.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):
r"""
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.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.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.0:
if maxMajorSteps < 1:
maxMajorSteps = 1
stepSize = divideInterval(interval.width(), maxMajorSteps, self.base())
scaleDiv = QwtScaleDiv()
if stepSize != 0.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.0, stepSize) == 0:
ticks[i][j] = 0.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])
if np.isnan(numTicks):
numTicks = 0
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.0:
return
numTicks = int(math.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.0, stepSize) == 0:
alignedValue = 0.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()
eps = 0.000000000001
if -DBL_MAX + stepSize <= x1:
x = floorEps(x1, stepSize)
if abs(x) <= eps or not qFuzzyCompare(x1, x):
x1 = x
if DBL_MAX - stepSize >= x2:
x = ceilEps(x2, stepSize)
if abs(x) <= eps or not qFuzzyCompare(x2, x):
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 / math.pow(logBase, self.lowerMargin()),
x2 * math.pow(logBase, self.upperMargin()),
)
interval = interval.limited(LOG_MIN, LOG_MAX)
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.0:
stepSize = -math.log(abs(stepSize), logBase)
else:
stepSize = math.log(stepSize, logBase)
return x1, x2, stepSize
logRef = 1.0
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.0:
interval = self.buildInterval(interval.minValue())
stepSize = self.divideInterval(
qwtLogInterval(logBase, interval).width(), max([maxNumSteps, 1])
)
if stepSize < 1.0:
stepSize = 1.0
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.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())
return linearScaler.divideScale(
x1, x2, maxMajorSteps, maxMinorSteps, stepSize
)
stepSize = abs(stepSize)
if stepSize == 0.0:
if maxMajorSteps < 1:
maxMajorSteps = 1
stepSize = self.divideInterval(
qwtLogInterval(logBase, interval).width(), maxMajorSteps
)
if stepSize < 1.0:
stepSize = 1.0
scaleDiv = QwtScaleDiv()
if stepSize != 0.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 = math.log(interval.minValue())
lxmax = math.log(interval.maxValue())
lstep = (lxmax - lxmin) / float(numTicks - 1)
ticks = [interval.minValue()]
for i in range(1, numTicks - 1):
ticks += [math.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.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.0:
if not qFuzzyCompare(s, 1.0):
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.0:
return
if minStep < 1.0:
minStep = 1.0
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([math.pow(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))