Source code for taurus.qt.qtgui.extra_guiqwt.scales

#!/usr/bin/env python

# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################

"""
scales.py: Custom scales used by taurus.qt.qtgui.plot module
"""

import numpy
from datetime import datetime, timedelta
from time import mktime
from taurus.external.qt import Qt

import qwt
import guiqwt


__guiqwt_version = list(map(int, guiqwt.__version__.split(".")[:3]))


__all__ = [
    "DateTimeScaleEngine",
    "DeltaTimeScaleEngine",
    "FixedLabelsScaleEngine",
    "FancyScaleDraw",
    "TaurusTimeScaleDraw",
    "DeltaTimeScaleDraw",
    "FixedLabelsScaleDraw",
]


def _getDefaultAxisLabelsAlignment(axis, rotation):
    """return a "smart" alignment for the axis labels depending on the axis
    and the label rotation

    :param axis: the axis
    :type axis: qwt.QwtPlot.Axis
    :param rotation: The rotation (in degrees, clockwise-positive)
    :type rotation: float
    :return: an alignment
    :rtype: Qt.Alignment
    """
    if axis == qwt.QwtPlot.xBottom:
        if rotation == 0:
            return Qt.Qt.AlignHCenter | Qt.Qt.AlignBottom
        elif rotation < 0:
            return Qt.Qt.AlignLeft | Qt.Qt.AlignBottom
        else:
            return Qt.Qt.AlignRight | Qt.Qt.AlignBottom
    elif axis == qwt.QwtPlot.yLeft:
        if rotation == 0:
            return Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter
        elif rotation < 0:
            return Qt.Qt.AlignLeft | Qt.Qt.AlignBottom
        else:
            return Qt.Qt.AlignLeft | Qt.Qt.AlignTop
    elif axis == qwt.QwtPlot.yRight:
        if rotation == 0:
            return Qt.Qt.AlignRight | Qt.Qt.AlignVCenter
        elif rotation < 0:
            return Qt.Qt.AlignRight | Qt.Qt.AlignTop
        else:
            return Qt.Qt.AlignRight | Qt.Qt.AlignBottom
    elif axis == qwt.QwtPlot.xTop:
        if rotation == 0:
            return Qt.Qt.AlignHCenter | Qt.Qt.AlignTop
        elif rotation < 0:
            return Qt.Qt.AlignLeft | Qt.Qt.AlignTop
        else:
            return Qt.Qt.AlignRight | Qt.Qt.AlignTop


[docs]class FancyScaleDraw(qwt.QwtScaleDraw): """This is a scaleDraw with a tuneable palette and label formats""" def __init__(self, format=None, palette=None): qwt.QwtScaleDraw.__init__(self) self._labelFormat = format self._palette = palette
[docs] def setPalette(self, palette): """pass a QPalette or None to use default""" self._palette = palette
[docs] def getPalette(self): return self._palette
[docs] def setLabelFormat(self, format): """pass a format string (e.g. "%g") or None to use default (it uses the locale)""" self._labelFormat = format self.invalidateCache() # to force repainting of the labels
[docs] def getLabelFormat(self): """pass a format string (e.g. "%g") or None to use default (it uses the locale)""" return self._labelFormat
[docs] def label(self, val): if str(self._labelFormat) == "": return qwt.QwtText() if self._labelFormat is None: return qwt.QwtScaleDraw.label(self, val) else: return qwt.QwtText(self._labelFormat % val)
[docs] def draw(self, painter, palette): if self._palette is None: qwt.QwtScaleDraw.draw(self, painter, palette) else: qwt.QwtScaleDraw.draw(self, painter, self._palette)
[docs]class DateTimeScaleEngine(qwt.QwtLinearScaleEngine): def __init__(self, scaleDraw=None): qwt.QwtLinearScaleEngine.__init__(self) self.setScaleDraw(scaleDraw)
[docs] def setScaleDraw(self, scaleDraw): self._scaleDraw = scaleDraw
[docs] def scaleDraw(self): return self._scaleDraw
[docs] def divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize): """Reimplements qwt.QwtLinearScaleEngine.divideScale **Important**: The stepSize parameter is **ignored**. :return: a scale division whose ticks are aligned with the natural time units :rtype: qwt.QwtScaleDiv """ interval = qwt.QwtInterval(x1, x2).normalized() if interval.width() <= 0: return qwt.QwtScaleDiv() dt1 = datetime.fromtimestamp(interval.minValue()) dt2 = datetime.fromtimestamp(interval.maxValue()) if ( dt1.year < 1900 or dt2.year > 9999 ): # limits in time.mktime and datetime return qwt.QwtScaleDiv() majticks = [] medticks = [] minticks = [] dx = interval.width() # = 3600s*24*(365+366) = 2 years (counting a leap year) if dx > 63072001: format = "%Y" for y in range(dt1.year + 1, dt2.year): dt = datetime(year=y, month=1, day=1) majticks.append(mktime(dt.timetuple())) elif dx > 5270400: # = 3600s*24*61 = 61 days format = "%Y %b" d = timedelta(days=31) dt = ( dt1.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + d ) while dt < dt2: # make sure that we are on day 1 (even if always sum 31 days) dt = dt.replace(day=1) majticks.append(mktime(dt.timetuple())) dt += d elif dx > 172800: # 3600s24*2 = 2 days format = "%b/%d" d = timedelta(days=1) dt = dt1.replace(hour=0, minute=0, second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 7200: # 3600s*2 = 2hours format = "%b/%d-%Hh" d = timedelta(hours=1) dt = dt1.replace(minute=0, second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 1200: # 60s*20 =20 minutes format = "%H:%M" d = timedelta(minutes=10) dt = ( dt1.replace( minute=(dt1.minute // 10) * 10, second=0, microsecond=0 ) + d ) while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 120: # =60s*2 = 2 minutes format = "%H:%M" d = timedelta(minutes=1) dt = dt1.replace(second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 20: # 20 s format = "%H:%M:%S" d = timedelta(seconds=10) dt = dt1.replace(second=(dt1.second // 10) * 10, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 2: # 2s format = "%H:%M:%S" majticks = list(range(int(x1) + 1, int(x2))) else: # less than 2s (show microseconds) scaleDiv = qwt.QwtLinearScaleEngine.divideScale( self, x1, x2, maxMajSteps, maxMinSteps, stepSize ) self.scaleDraw().setDatetimeLabelFormat("%S.%f") return scaleDiv # make sure to comply with maxMajTicks L = len(majticks) if L > maxMajSteps: majticks = majticks[:: int(numpy.ceil(float(L) / maxMajSteps))] scaleDiv = qwt.QwtScaleDiv(interval, minticks, medticks, majticks) self.scaleDraw().setDatetimeLabelFormat(format) if x1 > x2: scaleDiv.invert() return scaleDiv
[docs] @staticmethod def getDefaultAxisLabelsAlignment(axis, rotation): """return a "smart" alignment for the axis labels depending on the axis and the label rotation :param axis: the axis :type axis: qwt.QwtPlot.Axis :param rotation: The rotation (in degrees, clockwise-positive) :type rotation: float :return: an alignment :rtype: Qt.Alignment """ return _getDefaultAxisLabelsAlignment(axis, rotation)
[docs] @staticmethod def enableInAxis(plot, axis, scaleDraw=None, rotation=None): """convenience method that will enable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, the current ScaleDraw for the plot will be used if possible, and a :class:`TaurusTimeScaleDraw` will be set if not :type scaleDraw: qwt.QwtScaleDraw :param rotation: The rotation of the labels (in degrees, clockwise- positive) :type rotation: float or None """ if scaleDraw is None: scaleDraw = plot.axisScaleDraw(axis) if not isinstance(scaleDraw, TaurusTimeScaleDraw): scaleDraw = TaurusTimeScaleDraw() plot.setAxisScaleDraw(axis, scaleDraw) plot.setAxisScaleEngine(axis, DateTimeScaleEngine(scaleDraw)) if rotation is not None: alignment = DateTimeScaleEngine.getDefaultAxisLabelsAlignment( axis, rotation ) plot.setAxisLabelRotation(axis, rotation) plot.setAxisLabelAlignment(axis, alignment)
[docs] @staticmethod def disableInAxis(plot, axis, scaleDraw=None, scaleEngine=None): """convenience method that will disable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :type scaleDraw: qwt.QwtScaleDraw :param scaleEngine: Scale draw to use. If None given, a :class:`qwt.QwtLinearScaleEngine` will be set :type scaleEngine: qwt.QwtScaleEngine """ if scaleDraw is None: scaleDraw = FancyScaleDraw() if scaleEngine is None: scaleEngine = qwt.QwtLinearScaleEngine() plot.setAxisScaleEngine(axis, scaleEngine) plot.setAxisScaleDraw(axis, scaleDraw)
[docs]class TaurusTimeScaleDraw(FancyScaleDraw): def __init__(self, *args): FancyScaleDraw.__init__(self, *args)
[docs] def setDatetimeLabelFormat(self, format): self._datetimeLabelFormat = format
[docs] def datetimeLabelFormat(self): return self._datetimeLabelFormat
[docs] def label(self, val): if str(self._labelFormat) == "": return qwt.QwtText() # From val to a string with time t = datetime.fromtimestamp(val) try: # If the scaleDiv was created by a DateTimeScaleEngine, # then it has a _datetimeLabelFormat s = t.strftime(self._datetimeLabelFormat) except AttributeError: print( "Warning: cannot get the datetime label format " + "(Are you using a DateTimeScaleEngine?)" ) s = t.isoformat(" ") return qwt.QwtText(s)
[docs]class DeltaTimeScaleEngine(qwt.QwtLinearScaleEngine): def __init__(self, scaleDraw=None): qwt.QwtLinearScaleEngine.__init__(self) self.setScaleDraw(scaleDraw)
[docs] def setScaleDraw(self, scaleDraw): self._scaleDraw = scaleDraw
[docs] def scaleDraw(self): return self._scaleDraw
[docs] def divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize): """Reimplements qwt.QwtLinearScaleEngine.divideScale :return: a scale division whose ticks are aligned with the natural delta time units :rtype: qwt.QwtScaleDiv """ interval = qwt.QwtInterval(x1, x2).normalized() if interval.width() <= 0: return qwt.QwtScaleDiv() d_range = interval.width() if d_range < 2: # 2s return qwt.QwtLinearScaleEngine.divideScale( self, x1, x2, maxMajSteps, maxMinSteps, stepSize ) elif d_range < 20: # 20 s s = 1 elif d_range < 120: # =60s*2 = 2 minutes s = 10 elif d_range < 1200: # 60s*20 =20 minutes s = 60 elif d_range < 7200: # 3600s*2 = 2 hours s = 600 elif d_range < 172800: # 3600s24*2 = 2 days s = 3600 else: s = 86400 # 1 day # calculate a step size that respects the base step (s) and also # enforces the maxMajSteps stepSize = s * int(numpy.ceil(float(d_range // s) / maxMajSteps)) return qwt.QwtLinearScaleEngine.divideScale( self, x1, x2, maxMajSteps, maxMinSteps, stepSize )
[docs] @staticmethod def getDefaultAxisLabelsAlignment(axis, rotation): """return a "smart" alignment for the axis labels depending on the axis and the label rotation :param axis: the axis :type axis: qwt.QwtPlot.Axis :param rotation: The rotation (in degrees, clockwise-positive) :type rotation: float :return: an alignment :rtype: Qt.Alignment """ return _getDefaultAxisLabelsAlignment(axis, rotation)
[docs] @staticmethod def enableInAxis(plot, axis, scaleDraw=None, rotation=None): """convenience method that will enable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, the current ScaleDraw for the plot will be used if possible, and a :class:`TaurusTimeScaleDraw` will be set if not :type scaleDraw: qwt.QwtScaleDraw :param rotation: The rotation of the labels (in degrees, clockwise- positive) :type rotation: float or None """ if scaleDraw is None: scaleDraw = plot.axisScaleDraw(axis) if not isinstance(scaleDraw, DeltaTimeScaleDraw): scaleDraw = DeltaTimeScaleDraw() plot.setAxisScaleDraw(axis, scaleDraw) plot.setAxisScaleEngine(axis, DeltaTimeScaleEngine(scaleDraw)) if rotation is not None: alignment = DeltaTimeScaleEngine.getDefaultAxisLabelsAlignment( axis, rotation ) plot.setAxisLabelRotation(axis, rotation) plot.setAxisLabelAlignment(axis, alignment)
[docs] @staticmethod def disableInAxis(plot, axis, scaleDraw=None, scaleEngine=None): """convenience method that will disable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :type scaleDraw: qwt.QwtScaleDraw :param scaleEngine: Scale draw to use. If None given, a :class:`qwt.QwtLinearScaleEngine` will be set :type scaleEngine: qwt.QwtScaleEngine """ if scaleDraw is None: scaleDraw = FancyScaleDraw() if scaleEngine is None: scaleEngine = qwt.QwtLinearScaleEngine() plot.setAxisScaleEngine(axis, scaleEngine) plot.setAxisScaleDraw(axis, scaleDraw)
[docs]class DeltaTimeScaleDraw(FancyScaleDraw): def __init__(self, *args): FancyScaleDraw.__init__(self, *args)
[docs] def label(self, val): if val >= 0: s = "+%s" % str(timedelta(seconds=val)) else: s = "-%s" % str(timedelta(seconds=-val)) return qwt.QwtText(s)
[docs]class FixedLabelsScaleEngine(qwt.QwtLinearScaleEngine): def __init__(self, positions): """labels is a sequence of (pos,label) tuples where pos is the point at wich to draw the label and label is given as a python string (or QwtText)""" qwt.QwtScaleEngine.__init__(self) self._positions = positions # self.setAttribute(self.Floating,True)
[docs] def divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize=0.0): div = qwt.QwtScaleDiv(x1, x2, self._positions, [], []) div.setTicks(qwt.QwtScaleDiv.MajorTick, self._positions) return div
[docs] @staticmethod def enableInAxis(plot, axis, scaleDraw=None): """convenience method that will enable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, the current ScaleDraw for the plot will be used if possible, and a :class:`FixedLabelsScaleDraw` will be set if not :type scaleDraw: qwt.QwtScaleDraw """ if scaleDraw is None: scaleDraw = plot.axisScaleDraw(axis) if not isinstance(scaleDraw, FixedLabelsScaleDraw): scaleDraw = FixedLabelsScaleDraw() plot.setAxisScaleDraw(axis, scaleDraw) plot.setAxisScaleEngine(axis, FixedLabelsScaleEngine(scaleDraw))
[docs] @staticmethod def disableInAxis(plot, axis, scaleDraw=None, scaleEngine=None): """convenience method that will disable this engine in the given axis. Note that it changes the ScaleDraw as well. :param plot: the plot to change :type plot: qwt.QwtPlot :param axis: the id of the axis :type axis: qwt.QwtPlot.Axis :param scaleDraw: Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :type scaleDraw: qwt.QwtScaleDraw :param scaleEngine: Scale draw to use. If None given, a :class:`qwt.QwtLinearScaleEngine` will be set :type scaleEngine: qwt.QwtScaleEngine """ if scaleDraw is None: scaleDraw = FancyScaleDraw() if scaleEngine is None: scaleEngine = qwt.QwtLinearScaleEngine() plot.setAxisScaleEngine(axis, scaleEngine) plot.setAxisScaleDraw(axis, scaleDraw)
[docs]class FixedLabelsScaleDraw(FancyScaleDraw): def __init__(self, positions, labels): """This is a custom ScaleDraw that shows labels at given positions (and nowhere else) positions is a sequence of points for which labels are defined. labels is a sequence strings (or QwtText) Note that the lengths of positions and labels must match """ if len(positions) != len(labels): raise ValueError("lengths of positions and labels do not match") FancyScaleDraw.__init__(self) self._positions = positions self._labels = labels # self._positionsarray = numpy.array(self._positions) # this is stored # just in case
[docs] def label(self, val): try: index = self._positions.index(val) # try to find an exact match except Exception: index = None # It won't show any label # use the index of the closest position # index = (numpy.abs(self._positionsarray - val)).argmin() if index is not None: return qwt.QwtText(self._labels[index]) else: qwt.QwtText()