Source code for taurus.qt.qtgui.plot.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
"""
__all__=["DateTimeScaleEngine", "DeltaTimeScaleEngine", "FixedLabelsScaleEngine", 
         "FancyScaleDraw", "TaurusTimeScaleDraw", "DeltaTimeScaleDraw", 
         "FixedLabelsScaleDraw"]

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

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

    :param axis: (Qwt5.QwtPlot.Axis) the axis
    :param rotation: (float) The rotation (in degrees, clockwise-positive)

    :return: (Qt.Alignment) an alignment
    '''
    if axis == Qwt5.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 == Qwt5.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 == Qwt5.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 == Qwt5.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(Qwt5.QwtScaleDraw): '''This is a scaleDraw with a tuneable palette and label formats''' def __init__(self, format = None, palette = None): Qwt5.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 Qwt5.QwtText() if self._labelFormat is None: return Qwt5.QwtScaleDraw.label(self, val) else: return Qwt5.QwtText(self._labelFormat%val)
[docs] def draw(self, painter, palette): if self._palette is None: Qwt5.QwtScaleDraw.draw(self, painter, palette) else: Qwt5.QwtScaleDraw.draw(self, painter, self._palette)
[docs]class DateTimeScaleEngine(Qwt5.QwtLinearScaleEngine): def __init__(self, scaleDraw=None): Qwt5.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 Qwt5.QwtLinearScaleEngine.divideScale **Important**: The stepSize parameter is **ignored**. :return: (Qwt5.QwtScaleDiv) a scale division whose ticks are aligned with the natural time units ''' #if stepSize != 0: # scaleDiv = Qwt5.QwtLinearScaleEngine.divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize) # scaleDiv.datetimeLabelFormat = "%Y/%m/%d %H:%M%S.%f" # return scaleDiv interval = Qwt5.QwtDoubleInterval(x1, x2).normalized() if interval.width() <= 0: return Qwt5.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 Qwt5.QwtScaleDiv() majticks = [] medticks = [] minticks = [] dx=interval.width() if dx > 63072001: # = 3600s*24*(365+366) = 2 years (counting a leap year) 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): dt = dt.replace(day=1) #make sure that we are on day 1 (even if always sum 31 days) 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=range(int(x1)+1, int(x2)) else: #less than 2s (show microseconds) scaleDiv = Qwt5.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 = Qwt5.QwtScaleDiv(interval, minticks, medticks, majticks) self.scaleDraw().setDatetimeLabelFormat(format) if x1>x2: scaleDiv.invert() ##START DEBUG #print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" #for tk in scaleDiv.ticks(scaleDiv.MajorTick): # print datetime.fromtimestamp(tk).isoformat() #print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ##END DEBUG return scaleDiv
@staticmethod
[docs] def getDefaultAxisLabelsAlignment(axis, rotation): '''return a "smart" alignment for the axis labels depending on the axis and the label rotation :param axis: (Qwt5.QwtPlot.Axis) the axis :param rotation: (float) The rotation (in degrees, clockwise-positive) :return: (Qt.Alignment) an alignment ''' return _getDefaultAxisLabelsAlignment(axis, rotation)
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) 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 :param rotation: (float or None) The rotation of the labels (in degrees, clockwise-positive) ''' 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)
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :param scaleEngine: (Qwt5.QwtScaleEngine) Scale draw to use. If None given, a :class:`Qwt5.QwtLinearScaleEngine` will be set ''' if scaleDraw is None: scaleDraw=FancyScaleDraw() if scaleEngine is None: scaleEngine = Qwt5.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 Qwt5.QwtText() # From val to a string with time t = datetime.fromtimestamp(val) try: #If the scaleDiv was created by a DateTimeScaleEngine it has a _datetimeLabelFormat s = t.strftime(self._datetimeLabelFormat) except AttributeError, e: print "Warning: cannot get the datetime label format (Are you using a DateTimeScaleEngine?)" s = t.isoformat(' ') return Qwt5.QwtText(s)
[docs]class DeltaTimeScaleEngine(Qwt5.QwtLinearScaleEngine): def __init__(self, scaleDraw=None): Qwt5.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 Qwt5.QwtLinearScaleEngine.divideScale :return: (Qwt5.QwtScaleDiv) a scale division whose ticks are aligned with the natural delta time units ''' interval = Qwt5.QwtDoubleInterval(x1, x2).normalized() if interval.width() <= 0: return Qwt5.QwtScaleDiv() d_range = interval.width() if d_range < 2: # 2s return Qwt5.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 Qwt5.QwtLinearScaleEngine.divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize)
@staticmethod
[docs] def getDefaultAxisLabelsAlignment(axis, rotation): '''return a "smart" alignment for the axis labels depending on the axis and the label rotation :param axis: (Qwt5.QwtPlot.Axis) the axis :param rotation: (float) The rotation (in degrees, clockwise-positive) :return: (Qt.Alignment) an alignment ''' return _getDefaultAxisLabelsAlignment(axis, rotation)
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) 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 :param rotation: (float or None) The rotation of the labels (in degrees, clockwise-positive) ''' 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)
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :param scaleEngine: (Qwt5.QwtScaleEngine) Scale draw to use. If None given, a :class:`Qwt5.QwtLinearScaleEngine` will be set ''' if scaleDraw is None: scaleDraw=FancyScaleDraw() if scaleEngine is None: scaleEngine = Qwt5.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 Qwt5.QwtText(s)
[docs]class FixedLabelsScaleEngine(Qwt5.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)''' Qwt5.QwtScaleEngine.__init__(self) self._positions = positions #self.setAttribute(self.Floating,True)
[docs] def divideScale(self, x1, x2, maxMajSteps, maxMinSteps, stepSize=0.0): div = Qwt5.QwtScaleDiv(x1, x2, self._positions, [], []) div.setTicks(Qwt5.QwtScaleDiv.MajorTick, self._positions) return div
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) 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 ''' if scaleDraw is None: scaleDraw = plot.axisScaleDraw(axis) if not isinstance(scaleDraw, FixedLabelsScaleDraw): scaleDraw = FixedLabelsScaleDraw() plot.setAxisScaleDraw(axis, scaleDraw) plot.setAxisScaleEngine(axis, FixedLabelsScaleEngine(scaleDraw))
@staticmethod
[docs] 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: (Qwt5.QwtPlot) the plot to change :param axis: (Qwt5.QwtPlot.Axis) the id of the axis :param scaleDraw: (Qwt5.QwtScaleDraw) Scale draw to use. If None given, a :class:`FancyScaleDraw` will be set :param scaleEngine: (Qwt5.QwtScaleEngine) Scale draw to use. If None given, a :class:`Qwt5.QwtLinearScaleEngine` will be set ''' if scaleDraw is None: scaleDraw=FancyScaleDraw() if scaleEngine is None: scaleEngine = Qwt5.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: 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 Qwt5.QwtText(self._labels[index]) else: Qwt5.QwtText()