Source code for qwt.scale_engine

# -*- 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 setTransformation(self, transform): """ Assign a transformation :param qwt.transform.QwtTransform transform: Transformation The transformation object is used as factory for clones that are returned by `transformation()` The scale engine takes ownership of the transformation. .. seealso:: :py:meth:`QwtTransform.copy()`, :py:meth:`transformation()` """ assert transform is None or isinstance(transform, QwtTransform) if transform != self.__data.transform: self.__data.transform = transform
[docs] def transformation(self): """ Create and return a clone of the transformation of the engine. When the engine has no special transformation None is returned, indicating no transformation. :return: A clone of the transfomation .. seealso:: :py:meth:`setTransformation()` """ if self.__data.transform: return self.__data.transform.copy()
[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))