#!/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 . ## ############################################################################# """ taurusplot.py: Generic graphical plotting widget for Taurus """ from __future__ import print_function from __future__ import absolute_import from future import standard_library standard_library.install_aliases() from builtins import next from builtins import str from builtins import range from builtins import object import os import copy from datetime import datetime import time import numpy from future.utils import string_types from functools import partial from taurus.external.qt import Qt, Qwt5, compat import taurus import taurus.core from taurus.core.taurusmanager import getSchemeFromName from taurus.core.taurusbasetypes import DataFormat # TODO: Tango-centric from taurus.core.util.containers import LoopList, CaselessDict, CaselessList from taurus.core.util.safeeval import SafeEvaluator from taurus.qt.qtcore.util.signal import baseSignal from taurus.qt.qtcore.mimetypes import TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE from taurus.qt.qtgui.base import TaurusBaseComponent, TaurusBaseWidget from taurus.qt.qtgui.qwt5 import TaurusPlotConfigDialog, FancyScaleDraw,\ DateTimeScaleEngine, FixedLabelsScaleEngine, FixedLabelsScaleDraw from .curvesAppearanceChooserDlg import CurveAppearanceProperties __all__ = ["TaurusCurve", "TaurusCurveMarker", "TaurusXValues", "TaurusPlot", "isodatestr2float"] def isodatestr2float(s, sep='_'): """ converts a date string in iso format to a timestamp (seconds since epoch) with microseconds precision if available """ try: # with microseconds d = datetime.strptime(s, '%Y-%m-%d' + sep + '%H:%M:%S.%f') except: # without microseconds d = datetime.strptime(s, '%Y-%m-%d' + sep + '%H:%M:%S') return time.mktime(d.timetuple()) + d.microsecond * 1e-6 #import threading class DummyLock(object): def acquire(self): pass def release(self): pass # for debugging. Comment out in production #from taurus.core.util.log import TraceIt, DebugIt, InfoIt, WarnIt DFT_CURVE_PENS = [Qt.QPen(Qt.Qt.red), Qt.QPen(Qt.Qt.blue), Qt.QPen(Qt.Qt.green), Qt.QPen(Qt.Qt.magenta), Qt.QPen(Qt.Qt.cyan), Qt.QPen(Qt.Qt.yellow), Qt.QPen(Qt.Qt.black)] for __p in DFT_CURVE_PENS: __p.setWidth(1) # TODO: we would like this to be 2, but bug #171 forces 1 class TaurusZoomer(Qwt5.QwtPlotZoomer): '''A QwtPlotZoomer that displays the label assuming that X values are timestamps''' def __init__(self, *args): Qwt5.QwtPlotZoomer.__init__(self, *args) self._xIsTime = False def setXIsTime(self, xistime): '''If xistime is True, the x values will be interpreted as timestamps :param xistime: (bool) ''' self._xIsTime = xistime def trackerText(self, pos): '''reimplemented from :meth:`Qwt5.QwtPicker.trackerText`''' pos = self.invTransform(pos) if self._xIsTime: x = datetime.fromtimestamp(pos.x()).isoformat(' ') else: x = '%g' % pos.x() y = '%g' % pos.y() return Qwt5.QwtText(', '.join((x, y))) class TaurusCurveMarker(Qwt5.QwtPlotMarker, TaurusBaseComponent): '''Taurus-enabled custom version of QwtPlotMarker ''' def __init__(self, name, parent=None, labelOpacity=0.7): self.call__init__wo_kw(Qwt5.QwtPlotMarker) self.call__init__(TaurusBaseComponent, self.__class__.__name__) self.labelOpacity = labelOpacity self.setLineStyle(Qwt5.QwtPlotMarker.NoLine) self.setLabelAlignment(Qt.Qt.AlignRight | Qt.Qt.AlignBottom) text = Qwt5.QwtText('') text.setColor(Qt.Qt.black) # a semi-transparent green background for the label text.setBackgroundBrush( Qt.QBrush(Qt.QColor(0, 255, 0, int(255 * labelOpacity)))) self.setLabel(text) self.setSymbol(Qwt5.QwtSymbol(Qwt5.QwtSymbol.Diamond, Qt.QBrush(Qt.Qt.yellow), Qt.QPen(Qt.Qt.green), Qt.QSize(7, 7))) def alignLabel(self): '''Sets the label alignment in a "smart" way (depending on the current marker's position in the canvas). ''' xmap = self.plot().canvasMap(self.xAxis()) ymap = self.plot().canvasMap(self.yAxis()) xmiddlepoint = xmap.p1() + xmap.pDist() / 2 # p1,p2 are left,right here # p1,p2 are bottom,top here (and pixel coords start from top!) ymiddlepoint = ymap.p2() + ymap.pDist() / 2 xPaintPos = xmap.transform(self.xValue()) yPaintPos = ymap.transform(self.yValue()) if xPaintPos > xmiddlepoint: # the point in the right side hAlign = Qt.Qt.AlignLeft else: hAlign = Qt.Qt.AlignRight if yPaintPos > ymiddlepoint: # the point is in the bottom side vAlign = Qt.Qt.AlignTop else: vAlign = Qt.Qt.AlignBottom self.setLabelAlignment(hAlign | vAlign) class TaurusXValues(TaurusBaseComponent): ''' Class for managing abscissas values in a TaurusCurve ''' def __init__(self, name, parent=None): self._xValues = None self.call__init__(TaurusBaseComponent, self.__class__.__name__) self._listeners = [] self.setModel(name) def getModelClass(self): '''see :meth:`TaurusBaseComponent.getModelClass`''' return taurus.core.taurusattribute.TaurusAttribute def eventHandle(self, src, evt_type, val): '''see :meth:`TaurusBaseComponent.eventHandle`''' model = src if src is not None else self.getModelObj() if model is None: self._xValues = numpy.zeros(0) for l in self._listeners: l.fireEvent(model, evt_type, val) return format = getattr(val, 'data_format', model.getDataFormat()) if format == DataFormat._1D: value = val if val is not None else self.getModelValueObj() if value: self._xValues = numpy.array(value.value) else: self._xValues = numpy.zeros(0) for l in self._listeners: # all listeners are notified via fireEvent when the X changes l.fireEvent(src, evt_type, val) def registerDataChanged(self, listener): '''see :meth:`TaurusBaseComponent.registerDataChanged`''' self._listeners.append(listener) def unregisterDataChanged(self, listener): '''see :meth:`TaurusBaseComponent.unregisterDataChanged`''' self._listeners.remove(listener) def isReadOnly(self): '''see :meth:`TaurusBaseComponent.isReadOnly`''' return True def getValues(self): ''' returns the X values. :return: (numpy.array) ''' model = self.getModelObj() if model is None: self._xValues = numpy.zeros(0) else: value = self.getModelValueObj() if value: self._xValues = numpy.array(value.value) else: self._xValues = numpy.zeros(0) return self._xValues class TaurusCurve(Qwt5.QwtPlotCurve, TaurusBaseComponent): ''' Taurus-enabled custom version of QwtPlotCurve. TaurusCurves are attached to :class:`TaurusPlot` objects for displaying 1D data sets. A TaurusCurve is more complex than simple QwtPlotCurve in that: - It is taurus-aware (i.e., it is associated to a taurus model (an attribute) and listens to Taurus events to update its data - They may have an associated :class:`TaurusXValues` object that controls the values for its abscissas. - It uses a :class:`CurveAppearanceProperties` object to manage how it looks **Important**: The TaurusPlot is in charge of attaching and detaching its TaurusCurves, and keeps information about which TaurusCurves are attached. Therefore the programmer should never attach/detach a TaurusCurve manually. ''' consecutiveDroppedEventsWarning = 3 # number consecutive of dropped events before issuing a warning (-1 for disabling) # absolute number of dropped events before issuing a warning (-1 for # disabling) droppedEventsWarning = -1 dataChanged = baseSignal('dataChanged', 'QString') def __init__(self, name, xname=None, parent=None, rawData=None, optimized=False): Qwt5.QwtPlotCurve.__init__(self) TaurusBaseComponent.__init__(self, 'TaurusCurve') self._rawData = rawData self._xValues = None self._yValues = None self._showMaxPeak = False self._showMinPeak = False #self._markerFormatter = self.defaultMarkerFormatter self._filteredWhenLog = True self._history = [] self._titleText = '