Source code for taurus.qt.qtgui.qwt5.taurusplot

#!/usr/bin/env python

# This file is part of Taurus
# 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
# 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 <>.

""" Generic graphical plotting widget for Taurus

from __future__ import print_function
from __future__ import absolute_import
from future import standard_library
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"]

[docs]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.QPen(, Qt.QPen(, Qt.QPen(Qt.Qt.magenta), Qt.QPen(Qt.Qt.cyan), Qt.QPen(Qt.Qt.yellow), Qt.QPen(] 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)))
[docs]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( # 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.QSize(7, 7)))
[docs] 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)
[docs]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)
[docs] def getModelClass(self): '''see :meth:`TaurusBaseComponent.getModelClass`''' return taurus.core.taurusattribute.TaurusAttribute
[docs] 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)
[docs] def registerDataChanged(self, listener): '''see :meth:`TaurusBaseComponent.registerDataChanged`''' self._listeners.append(listener)
[docs] def unregisterDataChanged(self, listener): '''see :meth:`TaurusBaseComponent.unregisterDataChanged`''' self._listeners.remove(listener)
[docs] def isReadOnly(self): '''see :meth:`TaurusBaseComponent.isReadOnly`''' return True
[docs] 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
[docs]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 = '<label>' self.setXValuesBuilder() self._maxPeakMarker = TaurusCurveMarker(name, self) self._minPeakMarker = TaurusCurveMarker(name, self) self.__curveName = name self.isRawData = not(rawData is None) self.droppedEventsCount = 0 self.consecutiveDroppedEventsCount = 0 if optimized: self.setPaintAttribute(self.PaintFiltered, True) self.setPaintAttribute(self.ClipPolygons, True) if xname is not None: self.__xFromAttr = TaurusXValues(xname, parent) self.__xFromAttr.registerDataChanged(self) self.setXValuesBuilder(lambda yVals: self.__xFromAttr.getValues()) else: self.__xFromAttr = None if name and not self.isRawData: try: self.setModel(name) except Exception: self.error("Problems when adding curve " + str(name)) self.traceback() # @staticmethod # def defaultMarkerFormatter(self,curve,label,i,x,y,xIsTime): # """ # Returns the text to be shown in plot tooltips/markers. # :param curve: the name of the curve # :param label: the label to be displayed # :param i: the index of the point in the curve # :param x: x axis position # :param y: y axis position # :param xIsTime: To adapt format to time if needed # :return: (str) # """ # #@todo: Check: is this method ever called??? It seems it is not since it is buggy and we don't see problems # if self.getXIsTime(): # infotxt = "'%s'[%i]:\n\t (t=%s, y=%.3g)"%(pickedCurveName,pickedIndex,datetime.fromtimestamp(picked.x()).ctime(),picked.y()) # else: # infotxt = "'%s'[%i]:\n\t (x=%.3g, y=%.3g)"%(pickedCurveName,pickedIndex,picked.x(),picked.y()) # return infotxt # # def setMarkerFormatter(self,formatter): # """ # Sets formatter method for plot tooltips/markers. # The method must have at least 4 arguments: # :param curve: the name of the curve # :param label: the label to be displayed # :param i: the index of the point in the curve # :param x: x axis position # :param y: y axis position # :param xIsTime: To adapt format to time if needed # """ # self._markerFormatter = formatter # # def markerFormatter(self): # """ # Returns the method used to format plot tooltips # # :return: (function) # """ # return self._formatter
[docs] def getCurveName(self): '''Returns the name of the curve (in the case of non RawDataCurves, it is the same as the model name) :return: (str) ''' return self.__curveName
[docs] def setTitleText(self, titletext): ''' Sets the title text for this curve. :param titletext: (str) A string which can contain predefined placeholders (which make sense in the case of non-rawdata curves) See Also : compileTitleText ''' self._titleText = titletext self.updateTitle()
[docs] def titleText(self, compiled=False): '''Returns the titleText string. If compiled == True, the returned string will be processed through compileTitleText :param compiled: (bool) Whether to process the return value or not (default is compiled=False) :return: (basestring) the title .. seealso:: :meth:`compileTitleText` ''' if compiled: return self.compileTitleText(self._titleText) else: return self._titleText
[docs] def updateTitle(self): '''Updates the title of the curve, according to the titleText property''' titleText = self.compileTitleText(self._titleText) title = self.title() title.setText(titleText) self.setTitle(title) self.itemChanged()
[docs] def compileTitleText(self, titletext): """Substitutes the known placeholders by the current equivalent values for a titleText. *Note*: Some placeholders may not make sense for certain curves (e.g. <label> for a RawData curve). In these cases, they are left unprocessed (without warning). :param titletext: (str) A string which can contain any of the following predefined placeholders: - <label> the attribute label (default) - <model> the model name - <attr_name> attribute name - <attr_fullname> full attribute name (for backwards compatibility, <attr_full_name> is also accepted) - <dev_alias> device alias - <dev_name> device name - <dev_fullname> full device name (for backwards compatibility, <dev_full_name> is also accepted) - <current_title> The current title :return: (str) a title string where the placeholders have been substituted by their corresponding values """ # All TaurusCurves can deal with at least these placeholders titletext = titletext.replace('<current_title>', str(self.title().text())) titletext = titletext.replace('<model>', self.getModel()) attr = self.getModelObj() if attr is None: return titletext # TaurusCurves for which we can get the Attribute... titletext = titletext.replace('<label>', attr.label or '---') titletext = titletext.replace('<attr_name>', or '---') titletext = titletext.replace('<attr_fullname>', attr.getFullName() or '---') titletext = titletext.replace('<attr_full_name>', attr.getFullName() or '---') dev = attr.getParentObj() if dev is not None: # TaurusCurves for which we can get the Device object... titletext = titletext.replace('<dev_alias>', dev.getSimpleName() or '---') titletext = titletext.replace('<dev_name>', dev.getNormalName() or '---') titletext = titletext.replace('<dev_fullname>', dev.getFullName() or '---') titletext = titletext.replace('<dev_full_name>', dev.getFullName() or '---') return titletext
# -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~ # Convenience attach/detach methods # -~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
[docs] def attachMarkers(self, plot): '''attach markers to the plot :param plot: the plot (typically, the TaurusPlot instance) ''' self._maxPeakMarker.attach(plot) self._minPeakMarker.attach(plot)
[docs] def attachMaxMarker(self, plot): '''attach marker of max value to the plot :param plot: the plot (typically, the TaurusPlot instance) ''' self._maxPeakMarker.attach(plot)
[docs] def attachMinMarker(self, plot): '''attach markers of min value to the plot :param plot: the plot (typically, the TaurusPlot instance) ''' self._minPeakMarker.attach(plot)
[docs] def detach(self): '''reimplemented from :class:`QwtPlotCurve`. In addition to dettaching the curve, it dettaches the associated min/max markers. ''' self.detachMarkers() self.resetModel() Qwt5.QwtPlotCurve.detach(self)
[docs] def detachMarkers(self): '''detaches the min/max markers of this curve''' self._maxPeakMarker.detach() self._minPeakMarker.detach()
[docs] def detachMaxMarker(self): '''detaches the max marker of this curve''' self._maxPeakMarker.detach()
[docs] def detachMinMarker(self): '''detaches the min marker of this curve''' self._minPeakMarker.detach()
#-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # Overwrite from TaurusBaseComponent #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
[docs] def getModelClass(self): '''See :meth:`TaurusBaseComponent.getModelClass`''' return taurus.core.taurusattribute.TaurusAttribute
[docs] def getRawData(self): '''Returns the rawData :return: (dict or None) a RawData dict or None if the curve is not RawData .. seealso:: :meth:`TaurusPlot.attachRawData` ''' return self._rawData
[docs] def setXValuesBuilder(self, fn=None): """ Sets the callback to be used for creating the 'X' array values for a curve. If None given, the default is that the abscissas are int indexes (from 0 to len(Y)). :param fn: (callable) a callable that gets the Y values as a parameter and returns X values E.g., the default:: curve.setXValuesBuilder() is equivalent to:: curve.setXValuesBuilder(lambda yVals: numpy.arange(len(yVals))) """ if fn is None: fn = lambda yVals: numpy.arange(len(yVals)) self._xValuesBuilder = fn
[docs] def getXValues(self): '''Returns X values using the XValuesBuilder. :return: (sequence) .. seealso:: :meth:`setXValuesBuilder` ''' xBuilder = self._xValuesBuilder if not hasattr(self, "_yValues"): return xBuilder([]) if self._yValues is None: return xBuilder([]) assert(isinstance(self._yValues, numpy.ndarray)) # ob=numpy.array(None) has len(ob.shape) = 0, but the worst part # is that len(ob) throws exception! if len(self._yValues.shape) == 0: return xBuilder([]) return xBuilder(self._yValues)
[docs] def handleEvent(self, src, evt_type, val): '''Handles Taurus Events for this curve See: :meth:`TaurusBaseComponent.handleEvent` ''' if self.isRawData: #self.warning('fireEvent of a RawData curve has been called by %s'%repr(self.sender())) raise Exception('called handleEvent of a RawData curve') return model = src if src is not None else self.getModelObj() if model is None: self._xValues = numpy.zeros(0) self._yValues = numpy.zeros(0) self.dataChanged.emit(str(self.getModel())) return if evt_type == taurus.core.taurusbasetypes.TaurusEventType.Config: self.updateTitle() value = val if isinstance( val, taurus.core.taurusbasetypes.TaurusAttrValue) else self.getModelValueObj() if not isinstance(value, taurus.core.taurusbasetypes.TaurusAttrValue): self._onDroppedEvent(reason="Could not get attribute value") return try: self.setXYFromModel(value) except Exception as e: self._onDroppedEvent(reason=str(e)) return self._updateMarkers() self.dataChanged.emit(str(self.getModel()))
def _onDroppedEvent(self, reason='Unknown'): '''inform the user about a dropped event :param reason: (str) The reason of the drop ''' self.debug("Droping event. Reason %s", reason) self.droppedEventsCount += 1 self.consecutiveDroppedEventsCount += 1 mustwarn = False if self.droppedEventsCount == self.droppedEventsWarning: mustwarn = True msg = ('At least %i events from model "%s" have being dropped. This attribute may have problems\n' + 'Future occurrences will be silently ignored') % (self.droppedEventsWarning, self.modelName) # disable the consecutive Dropped events warning (we do not want it # if we got this one) self.consecutiveDroppedEventsWarning = -1 if self.consecutiveDroppedEventsCount == self.consecutiveDroppedEventsWarning: mustwarn = True msg = ('At least %i consecutive events from model "%s" have being dropped. This attribute may have problems\n' + 'Future occurrences will be silently ignored') % (self.consecutiveDroppedEventsWarning, self.modelName) # disable the consecutive Dropped events warning self.consecutiveDroppedEventsWarning = -1 if mustwarn: self.warning(msg) p = self.plot() if p: c = p.canvas() msg2 = "Errors reading %s (%s)" % ( self.titleText(compiled=True), self.modelName) Qt.QToolTip.showText(c.mapToGlobal(c.pos()), msg2, c) #Qt.QMessageBox.warning(p, "Errors in curve %s"%self.titleText(compiled=True), msg, Qt.QMessageBox.Ok) if self.droppedEventsCount == self.droppedEventsWarning: mustwarn = True msg = ('At least %i events from model "%s" have being dropped. This attribute may have problems\n' + 'Future occurrences will be silently ignored') % (self.droppedEventsWarning, self.modelName) self.warning(msg) p = self.plot() if p: c = p.canvas() msg = '' Qt.QToolTip.showText(c.pos(), msg, c) #Qt.QMessageBox.warning(p, "Errors in curve %s"%self.titleText(compiled=True), msg, Qt.QMessageBox.Ok) def _updateMarkers(self): '''updates min & max markers if needed''' if self.isVisible(): if self._showMaxPeak: try: maxpoint = [self._xValues[ self._yValues.argmax()], self._yValues.max()] except: maxpoint = [0, 0] self._maxPeakMarker.setValue(*maxpoint) label = self._maxPeakMarker.label() if self.plot().getXIsTime(): label.setText("Max. " + str(self.title().text()) + " " + repr( maxpoint[1]) + ' at t = ' + datetime.fromtimestamp(maxpoint[0]).ctime()) else: label.setText("Max. " + str(self.title().text()) + " " + repr(maxpoint[1]) + ' at x = ' + repr(maxpoint[0])) self._maxPeakMarker.setLabel(label) if self._showMinPeak: try: minpoint = [self._xValues[ self._yValues.argmin()], self._yValues.min()] except: minpoint = [0, 0] self._minPeakMarker.setValue(*minpoint) label = self._minPeakMarker.label() if self.plot().getXIsTime(): label.setText("Min. " + str(self.title().text()) + " " + repr( minpoint[1]) + ' at t = ' + datetime.fromtimestamp(minpoint[0]).ctime()) else: label.setText("Min. " + str(self.title().text()) + " " + repr(minpoint[1]) + ' at x = ' + repr(minpoint[0])) self._minPeakMarker.setLabel(label)
[docs] def setXYFromModel(self, value): """ sets the X (self._xValues) and Y (self._yValues) values from the given model. This method can be reimplemented by subclasses of Taurusplot that behave differently (e.g. TaurusTrend) :param value: (TaurusAttrValue) the value object from the model """ attr = self.getModelObj() if attr.data_format == DataFormat._1D: # TODO: Adapt all values to be plotted to the same Unit if value: if attr.isNumeric(): self._yValues = numpy.array(value.rvalue.magnitude) else: self._yValues = numpy.array(value.rvalue) else: self._yValues = numpy.zeros(0) self._xValues = self.getXValues() else: raise ValueError('TaurusCurve only supports SPECTRUM attributes ' '(a %s was passed). \n' 'Note: if you want a trend plot, use ' 'TaurusTrendCurve instead of ' 'TaurusCurve.' % str(DataFormat[attr.data_format]))
[docs] def setPaused(self, paused=True): '''Pauses itself and other listeners depending on it .. seealso:: :meth:`TaurusBaseComponent.setPaused` ''' TaurusBaseComponent.setPaused(self, paused) for ob in (self.__xFromAttr, self._maxPeakMarker, self._minPeakMarker): try: ob.setPaused(paused) except AttributeError: pass
[docs] def setAppearanceProperties(self, prop): """Applies the given CurveAppearanceProperties object (prop) to the curve. If a given property is set to None, it is not applied :param prop: (CurveAppearanceProperties) """ prop = copy.deepcopy(prop) s = Qwt5.QwtSymbol(self.symbol()) if prop.sStyle is not None: s.setStyle(Qwt5.QwtSymbol.Style(prop.sStyle)) if prop.sSize is not None: s.setSize(prop.sSize) if prop.sColor is not None: b = s.brush() p = s.pen() color = Qt.QColor(prop.sColor) p.setColor(color) b.setColor(color) b.setStyle(Qt.Qt.NoBrush) s.setBrush(b) s.setPen(p) if prop.sFill is not None: b = s.brush() if prop.sFill: b.setStyle(Qt.Qt.SolidPattern) s.setBrush(b) else: s.brush().setStyle(Qt.Qt.NoBrush) s.setBrush(b) p = Qt.QPen(self.pen()) if prop.lStyle is not None: p.setStyle(prop.lStyle) if prop.lWidth is not None: p.setWidth(prop.lWidth) if prop.lColor is not None: p.setColor(Qt.QColor(prop.lColor)) if prop.cStyle is not None: self.setStyle(prop.cStyle) if prop.cFill is not None: b = Qt.QBrush(self.brush()) if prop.cFill: # The area under the curve is filled with the same color as the curve but with 50% transparency color = p.color() color.setAlphaF(0.5) b.setColor(color) b.setStyle(Qt.Qt.SolidPattern) else: b.setStyle(Qt.Qt.NoBrush) self.setBrush(b) if prop.yAxis is not None: self.setYAxis(prop.yAxis) if getattr(prop, "visible", None) is not None: self.setVisible(prop.visible) if prop.title is not None: self.setTitleText(prop.title) self.setSymbol(s) self.setPen(p)
[docs] def getAppearanceProperties(self): """Returns the appearance properties of the curve (color, symbol, width,...). :return: (CurveAppearanceProperties) """ prop = CurveAppearanceProperties() s = self.symbol() prop.sStyle = prop.sSize = s.size().width() # We are only supporting symbols with width==heigh prop.sColor = s.brush().color() prop.sFill = (s.brush().style() != Qt.Qt.NoBrush) p = self.pen() prop.lStyle = prop.lWidth = p.width() prop.lColor = p.color() prop.cStyle = prop.cFill = (self.brush().style() != Qt.Qt.NoBrush) prop.yAxis = self.yAxis() prop.visible = self.isVisible() # We are forced to save only the text (and not the QwtText) because # Pickle chokes with the QwtText prop.title = self.title().text() return copy.deepcopy(prop)
[docs] def setYAxis(self, axis): """changes the Y axis to which the curve is associated :param axis: (Qwt5.QwtPlot.Axis) the axis to which it should associate """ Qwt5.QwtPlotCurve.setYAxis(self, axis) # this way we make sure that the filtering is correct (in case of # change of scale type) self.safeSetData()
[docs] def setFilteredWhenLog(self, filtered=True): '''Set whether non-possitive values should be discarded or not when plotting in log mode. :param filtered: (bool) if True, filtering is done ''' self._filteredWhenLog = filtered
[docs] def isFilteredWhenLog(self): '''returns True if non-possitive values are being discarded when plotting in log mode. return: (bool) .. seealso:: :meth:`setFilteredWhenLog` ''' return self._filteredWhenLog
[docs] def setData(self, x, y): '''Sets the X and Y data for the curve (possibly filtering non-possitive values if in log mode). Reimplemented from Qwt5.QwtPlotCurve.setData. :param x: (sequence) X values :param y: (sequence) Y values .. seealso:: :meth:`safeSetData`, :meth:`setFilteredWhenLog` ''' if self.isFilteredWhenLog(): # filter out the nonpossitive elements if the scale is logarithmic if self.plot(): type_ = self.plot().getAxisTransformationType(self.xAxis()) if type_ == Qwt5.QwtScaleTransformation.Log10: x, y = numpy.array(x), numpy.array(y) valid = x > 0 # this is an array of bools representing valid entries x, y = x[valid], y[valid] type_ = self.plot().getAxisTransformationType(self.yAxis()) if type_ == Qwt5.QwtScaleTransformation.Log10: x, y = numpy.array(x), numpy.array(y) valid = y > 0 # this is an array of bools representing valid entries x, y = x[valid], y[valid] else: self.debug("Curve is not connected but still receiving data") if len(x) != len(y): self.warning( "setData(x[%d],y[%d]): array sizes don't match!" % (len(x), len(y))) # now proceed as usual Qwt5.QwtPlotCurve.setData(self, x, y)
[docs] def safeSetData(self): '''Calls setData with x= self._xValues and y=self._yValues .. seealso:: :meth:`setData` ''' if self._xValues is None: self.setData(numpy.zeros(0), numpy.zeros(0)) elif self._yValues is None or len(self._yValues.shape) == 0: self.setData(self._xValues, numpy.zeros(0)) else: self.setData(self._xValues, self._yValues)
[docs] def getParentTaurusComponent(self): '''Searches the closest ancestor (in the Qt parenting hyerarchy) that is which inherits from TaurusBaseComponent. It returns None if None found. :return: (widget or None) ''' p = self.plot() while p and not isinstance(p, TaurusBaseComponent): if isinstance(p, (Qt.QDialog, Qt.QMainWindow)): p = None break p = p.parentWidget() return p
[docs] def registerDataChanged(self, listener, meth): ''' registers a listener to the DataChangedSignal of this curve :param listener: (QWidget) listener object :param meth: (callable) callback method ''' self.dataChanged.connect(meth)
[docs] def unregisterDataChanged(self, listener, meth): '''unregisters the given listener and method from the DataChangedSignal of this curve :param listener: (QWidget) listener object :param meth: (callable) callback method ''' self.dataChanged.disconnect(meth)
[docs] def isReadOnly(self): '''see :meth:`TaurusBaseComponent.isReadOnly`''' return True
[docs] def getStats(self, limits=None, inclusive=(True, True), imin=None, imax=None, ignorenans=True): ''' returns a dict containing several descriptive statistics of a region of the curve defined by the limits given by the keyword arguments. It also contains a copy of the data in the considered region. The keys of the returned dictionary correspond to: -'x' : the abscissas for the considered points (numpy.array) -'y' : the ordinates for the considered points (numpy.array) -'points': number of considered points (int) -'min' : (x,y) pair of the minimum of the curve (float,float) -'max' : (x,y) pair of the maximum of the curve (float,float) -'mean' : arithmetic average of y (float) -'std' : (biased)standard deviation of y (float) -'rms' : root mean square of y (float) Note that some of the values may be None if that cannot be computed. Also, :param limits: (None or tuple<float,float>) tuple containing (min,max) limits. Points of the curve whose abscisa value is outside of these limits are ignored. If None is passed, the limit is not enforced :param inclusive: (tuple<bool,bool>). A tuple consisting of the (lower flag, upper flag). These flags determine whether values exactly equal to the lower or upper limits are included. The default value is (True, True). :param imin: (int) lowest index to be considered. If None is given, the limit is not enforced :param imax: (int) higest index to be considered. If None is given, the limit is not enforced :param ignorenans: (bool) if True (defaul), the points with NaN values are stripped before calculating the stats :return: (dict) A dict containing the stats. ''' data = if imin is None: imin = 0 if imax is None: imax = data.size() x = numpy.array([data.x(i) for i in range(imin, imax)]) y = numpy.array([data.y(i) for i in range(imin, imax)]) if limits is not None: xmin, xmax = limits if xmax is None: xmax = numpy.inf if inclusive: mask = (x >= xmin) * (x <= xmax) else: mask = (x > xmin) * (x < xmax) x = x[mask] y = y[mask] if ignorenans: # we remove points where either x or y are Nan mask = numpy.invert(numpy.isnan(x + y)) x = x[mask] y = y[mask] ret = {'x': x, 'y': y, 'points': x.size, 'min': None, 'max': None, 'mean': None, 'std': None, 'rms': None} if x.size > 0: argmin = y.argmin() argmax = y.argmax() ret.update({'min': (x[argmin], y[argmin]), 'max': (x[argmax], y[argmax]), 'mean': y.mean(), 'std': y.std(), 'rms': numpy.sqrt(numpy.mean(y ** 2))}) return ret
#-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # Methods necessary to show/hide peak values #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
[docs] def showMaxPeak(self, show): """Specififes if we want to show or not the max peak of the curve :param show: (bool) """ self._showMaxPeak = show
# self.fireEvent(taurus.core.taurusbasetypes.TaurusEventType.Change) # #force re-reading attribute to update peak values
[docs] def showMinPeak(self, show): """Specififes if we want to show or not the min peak of the curve. :param show: (bool) """ self._showMinPeak = show
# self.fireEvent(taurus.core.taurusbasetypes.TaurusEventType.Change) # #force re-reading attribute to update peak values
[docs] def getYAxisStatus(self): '''returns either None (if the curve is not visible) or its yAxis (if it is visible) :return: (Qwt5.QwtPlot.Axis or None) ''' if not self.isVisible(): return None return self.yAxis()
# class TaurusPlot(Qwt5.QwtPlot, Logger):
[docs]class TaurusPlot(Qwt5.QwtPlot, TaurusBaseWidget): ''' TaurusPlot is a general widget for plotting 1D data sets. It is an extended taurus-aware version of :class:`QwtPlot`. .. image:: /_static/taurusplot04.png :align: center TaurusPlot already incorporates by default many features that can be added to a regular QwtPlot: - Zoomming, panning, and magnifier are enabled by default - Autoscaling is enabled and associated to the ESC key - Methods are available to add new curves which can either be associated to taurus attributes or be "raw data" (i.e., values that are not directly aware of control system events) - Context menu offers access to many options - A plot configuration dialog, and save/restore configuration facilities - Date-time scales and linear/log scales support - Methods for importing/exporting curves from/to ASCII data - Methods for printing and exporting the plot to PDF - Methods for creating curves from arbitrary functions - Data inspection facilities - ... For an overview of the features from an user point of view, see the :ref:`TaurusPlot User's Interface Guide <taurusplot_ui>`. You can also see some code that exemplifies the use of TaurusPlot in :ref:`the TaurusPlot coding examples <examples_taurusplot>` **Important**: although TaurusPlot subclasses QwtPlot and therefore it is possible to use QwtPlot's lower level methods for attaching QwtPlotItems (such as QwtPlotCurves) to the plot, it is *highly* recommended to use the higher-level methods provided by TaurusPlot to interact with the datasets attached to a TaurusPlot (e.g., `addModels()`, `attachRawData()`). This is because TaurusPlot keeps records of the items attached via its own methods. .. seealso:: :class:`TaurusTrend`, :ref:`TaurusPlot User's Interface Guide <taurusplot_ui>`, :ref:`The TaurusPlot coding examples <examples_taurusplot>` ''' #: Override the default modelChanged('QString') signal modelChanged = Qt.pyqtSignal() dataChanged = Qt.pyqtSignal('QString') CurvesYAxisChanged = Qt.pyqtSignal('QStringList', int) def __init__(self, parent=None, designMode=False): name = "TaurusPlot" # book-keeping of attached tauruscurves self.curves = CaselessDict() # TODO: Tango-centric Qwt5.QwtPlot.__init__(self, parent) TaurusBaseWidget.__init__(self, name) self._designMode = designMode self._modelNames = [] self._useParentModel = False self._isPaused = False self._defaultCurvesTitle = '<label>' self._curvePens = LoopList(DFT_CURVE_PENS) self._gridPen = Qt.QPen(Qt.Qt.gray, 1) # the latest element of this list is considered the current version self._supportedConfigVersions = ["tpc-1", "tpc-1.1"] # Logger.__init__(self) # Qwt5.QwtPlot.__init__(self, parent) # dictionary for default axes naming self._axesnames = {Qwt5.QwtPlot.xBottom: 'X', Qwt5.QwtPlot.xTop: 'X2', Qwt5.QwtPlot.yLeft: 'Y1', Qwt5.QwtPlot.yRight: 'Y2'} # cache for the values of the axis transformation self.__transformations = {} # Data Import Dialog (it will only be initialised if required) self.DataImportDlg = None # Values to be managed with/from the TaurusPlotConfigDialog time # configuration self._xIsTime = False self._xMax = None self._xMin = None self._xDynScale = False self._xDynScaleSupported = False # enable dropping (see also dragEnterEvent and dropEvent methods) self.setAcceptDrops(True) #self.curves_lock = threading.RLock() self.curves_lock = DummyLock() # background # self.setCanvasBackground(Qt.Qt.white) # attach a grid self._grid = Qwt5.QwtPlotGrid() self._grid.setPen(self._gridPen) self._grid.attach(self) # configure axes for axis in [Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.yLeft, Qwt5.QwtPlot.yRight, Qwt5.QwtPlot.xTop]: self.setAxisScaleDraw(axis, FancyScaleDraw()) self.y2AxisPalette = Qt.QPalette(Qt.QColor("blue"), Qt.QColor("black"), Qt.QColor( "blue"), Qt.QColor("blue"), Qt.QColor("blue"), Qt.QColor("blue"), Qt.QColor("blue")) self.axisScaleDraw(Qwt5.QwtPlot.yRight).setPalette(self.y2AxisPalette) # set initial show/hide peaks configuration self._showMaxPeaks = False self._showMinPeaks = False # zoom. One zoomer for Y1 and another for Y2 (but only one will be # active at each time) self._max_zoom_stack = 15 self._zoomer1 = TaurusZoomer(self.canvas()) self._zoomer2 = TaurusZoomer(self.canvas()) self._zoomer2.setRubberBandPen( self._zoomer2.setTrackerPen( self._zoomer2.setAxis(Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.yRight) self._zoomer2.setEnabled(False) self._zoomer = self._zoomer1 self._allowZoomers = True for z in (self._zoomer1, self._zoomer2): z.setMaxStackDepth(self._max_zoom_stack) # this disables the escape key for going to the top of the zoom # stack (we use escape via an action for autoscaling) z.setKeyPattern(z.KeyHome, Qt.Qt.Key_unknown) # point picker self._pointPicker = Qwt5.QwtPicker(self.canvas()) self._pointPicker.setSelectionFlags(Qwt5.QwtPicker.PointSelection) self._pickedMarker = TaurusCurveMarker("Picked", labelOpacity=0.8) self._pickedCurveName = "" self._pointPicker.selected.connect(self.pickDataPoint) # xRegion picker self._xRegionPicker = Qwt5.QwtPlotPicker(Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.yLeft, Qwt5.QwtPicker.PointSelection, Qwt5.QwtPicker.VLineRubberBand, Qwt5.QwtPicker.AlwaysOn, self.canvas()) self._xRegionPicker.setEnabled(False) self._xRegionPicker.selected.connect(self._onXRegionEvent) # magnifier self._magnifier = Qwt5.QwtPlotMagnifier(self.canvas()) self._magnifier.setMouseButton(Qt.Qt.NoButton) # panner self._panner = Qwt5.QwtPlotPanner(self.canvas()) self._panner.setMouseButton(Qt.Qt.LeftButton, Qt.Qt.ControlModifier) # legend self._legendPos = Qwt5.QwtPlot.RightLegend self._showLegend = False self._legendDecissionIsForever = False self.updateLegend() self.legendClicked.connect(self.toggleCurveState) # datainspector mode self._inspectorMode = False self.toggleDataInspectorMode(False) # optimization self._optimizationEnabled = True # modifiable by user self.setModifiableByUser(True) # drag&drop self.setSupportedMimeTypes( [TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE]) # initialize actions self.__initActions() # final stuff self.setObjectName(name) # self.defineStyle() def __initActions(self): '''Create and attach TaurusPlot actions''' #====================================================================== # This action is for debug only. Comment out when not debugging #self._debugAction = Qt.QAction("Calculate statistics", None) # self._debugAction.setShortcut(Qt.Qt.Key_D) #self.connect(self._debugAction, Qt.SIGNAL("triggered()"), self.__debug) # self.canvas().addAction(self._debugAction) #====================================================================== self._dataInspectorAction = Qt.QAction("Data &Inspector mode", None) self._dataInspectorAction.setShortcut(Qt.Qt.Key_I) self._dataInspectorAction.setCheckable(True) self._dataInspectorAction.setChecked(self._pointPicker.isEnabled()) self._dataInspectorAction.toggled[bool].connect(self.toggleDataInspectorMode) self._setFormatterAction = Qt.QAction("Set Formatter...", None) self._setFormatterAction.triggered.connect(self.onSetFormatter) self._curveStatsAction = Qt.QAction("Calculate statistics", None) self._curveStatsAction.setShortcut(Qt.Qt.Key_S) self._curveStatsAction.triggered.connect(self.onCurveStatsAction) self._pauseAction = Qt.QAction("&Pause", None) self._pauseAction.setShortcuts([Qt.Qt.Key_P, Qt.Qt.Key_Pause]) self._pauseAction.setCheckable(True) self._pauseAction.setChecked(self.isPaused()) self._pauseAction.toggled[bool].connect(self.setPaused) self._autoscaleAllAxisAction = Qt.QAction("Autoscale all axes", None) self._autoscaleAllAxisAction.setShortcut(Qt.Qt.Key_Escape) self._autoscaleAllAxisAction.triggered.connect(self.autoScaleAllAxes) self._toggleZoomAxisAction = Qt.QAction("Toggle Zoom-aware axis", None) self._toggleZoomAxisAction.setShortcut(Qt.Qt.Key_Z) self._toggleZoomAxisAction.triggered.connect( partial(self.toggleZoomer, axis=None)) self._configDialogAction = Qt.QAction("Plot configuration...", None) self._configDialogAction.setShortcut(Qt.QKeySequence("Alt+C")) self._configDialogAction.triggered.connect(self.showConfigDialog) self._inputDataAction = Qt.QAction("Input data selection...", None) self._inputDataAction.setShortcut(Qt.QKeySequence.New) self._inputDataAction.triggered.connect(self.showDataImportDlg) self._saveConfigAction = Qt.QAction("Save current settings...", None) self._saveConfigAction.setShortcut(Qt.QKeySequence.Save) self._saveConfigAction.triggered.connect( partial(self.saveConfig, ofile=None, curvenames=None)) self._loadConfigAction = Qt.QAction( "&Retrieve saved settings...", None) self._loadConfigAction.setShortcut(Qt.QKeySequence.Open) self._loadConfigAction.triggered.connect( partial(self.loadConfig, ifile=None)) self._showLegendAction = Qt.QAction("Show &Legend", None) self._showLegendAction.setShortcut(Qt.QKeySequence("Ctrl+L")) self._showLegendAction.setCheckable(True) self._showLegendAction.setChecked(self._showLegend) self._showLegendAction.triggered[bool].connect(self.showLegend) self.canvas().addAction(self._showLegendAction) self._showMaxAction = Qt.QAction("Show Max", None) self._showMaxAction.setCheckable(True) self._showMaxAction.setChecked(self._showMaxPeaks) self._showMaxAction.toggled.connect(self.showMaxPeaks) self._showMinAction = Qt.QAction("Show Min", None) self._showMinAction.setCheckable(True) self._showMinAction.setChecked(self._showMinPeaks) self._showMinAction.toggled[bool].connect(self.showMinPeaks) self._printAction = Qt.QAction("&Print plot...", None) self._printAction.triggered.connect(self.exportPrint) self._exportPdfAction = Qt.QAction("Export plot to PD&F...", None) self._exportPdfAction.triggered.connect( partial(self.exportPdf, fileName=None)) self._exportAsciiAction = Qt.QAction("Export data to &ASCII...", None) self._exportAsciiAction.triggered.connect( partial(self.exportAscii, curves=None)) self._setCurvesTitleAction = Qt.QAction( "Change Curves Titles...", None) self._setCurvesTitleAction.triggered.connect( partial(self.changeCurvesTitlesDialog, curveNamesList=None)) self._closeWindowAction = Qt.QAction( Qt.QIcon.fromTheme("process-stop"), 'Close Plot', self) self._closeWindowAction.triggered.connect(self.close) # add all actions and limit the scope of the key shortcuts to the # widget (default is Window) for action in (self._dataInspectorAction, self._pauseAction, self._autoscaleAllAxisAction, self._toggleZoomAxisAction, self._configDialogAction, self._inputDataAction, self._saveConfigAction, self._loadConfigAction, self._showLegendAction, self._showMaxAction, self._showMinAction, self._printAction, self._exportPdfAction, self._exportAsciiAction, self._setCurvesTitleAction, self._curveStatsAction, self._setFormatterAction): # this is needed to avoid ambiguity when more than one TaurusPlot # is used in the same window action.setShortcutContext(Qt.Qt.WidgetShortcut) # because of the line above, we must add the actions to the widget # that gets the focus (the canvas instead of self) self.canvas().addAction(action)
[docs] def setFormat(self, format): """Reimplemented from TaurusBaseComponent""" for name in self.curves: curve = self.curves.get(name, None) w = getattr(curve, 'owner', curve) w.setFormat(format) TaurusBaseComponent.setFormat(self, format)
[docs] def dropEvent(self, event): '''reimplemented to support dropping of modelnames in taurusplots''' mtype = self.handleMimeData(event.mimeData(), self.addModels) if mtype is None:'Invalid model') else: event.acceptProposedAction()
# def dropEvent(self, event): # '''reimplemented to support dropping of modelnames in taurusplots''' # supported = self.getSupportedMimeTypes() # formats = event.mimeData().formats() # for mtype in supported: # if mtype in formats: # modelname = str(event.mimeData().data(mtype)) # if modelname is None: # return # try: # self.addModels([modelname]) # event.acceptProposedAction() # except: #'Dropped data is invalid (%s)'%repr(modelname)) # return
[docs] def getAxisTransformationType(self, axis): """Retrieve the transformation type for a given axis (cached) :param axis: (Qwt5.QwtPlot.Axis) the axis :return: (Qwt5.QwtScaleTransformation.Type) .. note:: this method helps to avoid a memory leak in Qwt (see ) """ try: return self.__transformations[axis] except KeyError: t = self.axisScaleEngine(axis).transformation().type() self.__transformations[axis] = t return t
[docs] def setAxisScaleEngine(self, axis, scaleEngine): """ reimplemented from :meth:`Qwt5.QwtPlot.setAxisScaleEngine` to store a cache of the transformation type """ self.__transformations[axis] = scaleEngine.transformation().type() return Qwt5.QwtPlot.setAxisScaleEngine(self, axis, scaleEngine)
[docs] def getCurveTitle(self, curvename): '''return the current title associated to a given curve name :param curvename: (str) the name of the curve :return:(str) ''' self.curves_lock.acquire() try: curve = self.getCurve(curvename) if curve is None: title = None else: title = str(curve.title().text()) finally: self.curves_lock.release() return title
[docs] def getCurveNames(self): '''returns the names of all TaurusCurves attached to the plot (in arbitrary order, if you need a sorted list, see :meth:`getCurveNamesSorted`). :return: (list<str>) a copy of self.curves.keys() .. seealso:: :meth:`getCurveNamesSorted` ''' self.curves_lock.acquire() try: ret = copy.deepcopy(list(self.curves.keys())) finally: self.curves_lock.release() return ret
[docs] def getCurveNamesSorted(self): '''returns the names of the curves in z order (which is the one used in the legend, and in showing the curves). :return: (list<str>) curve names .. seealso:: :meth:`getCurveNames` ''' self.curves_lock.acquire() try: names = [o.getCurveName() for o in self.itemList() if isinstance(o, TaurusCurve)] finally: self.curves_lock.release() return copy.deepcopy(names)
[docs] def sortCurves(self, ordered=None): '''Sorts the attached curves in a given z order. This affects both the ordering in the legend and the visibility order when curves overlap in the plotting area. The order is governed by the `ordered` parameter (or alphabetically if no parameter is passed). :param ordered: (list<str> or None) A list of curve names in the desired order. If None passed, the items will be ordered alphabetically according to their title. ''' self.curves_lock.acquire() try: if ordered is None: orderedObjs = sorted( self.curves.values(), key=lambda curve: curve.titleText(compiled=True) ) else: #current = self.curves.keys() # if len(ordered) != len(current) or set(map(str.lower,current)) - set(map(str.lower, ordered)): # raise ValueError('Invalid value for the "ordered" parameter') orderedObjs = [self.curves[n] for n in ordered] for curve in orderedObjs: Qwt5.QwtPlotCurve.detach(curve) Qwt5.QwtPlotCurve.attach(curve, self) finally: self.curves_lock.release()
[docs] def toggleZoomer(self, axis=None): '''changes the current zoomer to that associated to the given axis (zoomer1 is attached to Y1 and zoomer2 to Y2). If no axis is passed, the zoomers are toggled. :param axis: (Qwt.QwtPlot.Axis or None) axis to activate for zooming. If None passed, the zoomers are toggled. :return: (Qwt.QwtPlot.Axis) the Y axis of the enabled zoomer ''' if not self._allowZoomers: for z in (self._zoomer1, self._zoomer2): z.setEnabled(False) return if axis is None: # find the axis of the currently disabled zoomer for z in (self._zoomer1, self._zoomer2): if not z.isEnabled(): break axis = z.yAxis() # enable the zoomer corresponding to axis and disable the other one for z in (self._zoomer1, self._zoomer2): z.setEnabled(z.yAxis() == axis) self._zoomer = self.getZoomers(axis)[0] self.debug('Now Zooming on %s' % str(self.getAxisName(axis))) return self._zoomer.yAxis()
[docs] def getAxisName(self, axis): '''If set, it returns the axis title text, otherwise returns the default axis name :param axis: (Qwt.QwtPlot.Axis) :return: (unicode) ''' name = str(self.axisTitle(axis).text()) if name == '': name = self._axesnames[axis] return name
[docs] def setPaused(self, paused=True): '''delegates the pausing to the curves :param paused: (bool) if True, the plot will be paused ''' for c in self.curves.values(): c.setPaused(paused) self._isPaused = paused
[docs] def isPaused(self): '''Returns the pause state :return: (bool) ''' return self._isPaused
def __debug(self, *args, **kwargs): '''put code here that you want to debug''' print("!!!!!!!!!!!!!!!1", self.pos()) Qt.QToolTip.showText(self.mapToGlobal(self.pos()), "ASDASDASDASD DASDAS ASDA", self) return
[docs] def getDefaultAxisLabelsAlignment(self, 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 ''' # print "!!!!", {Qwt5.QwtPlot.xBottom:"B" , Qwt5.QwtPlot.yLeft:"L", # Qwt5.QwtPlot.yRight:"R", Qwt5.QwtPlot.xTop:"T"}[axis] 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] def setAxisCustomLabels(self, axis, pos_and_labels, rotation=0, alignment=None): '''By calling this method, the scale vaues can be substituted by custom labels at arbitrary positions. In general, it is a good idea to let the alignment to be autocalculated. :param axis: (Qwt5.QwtPlot.Axis) the axis :param pos_and_labels: (sequence<tuple>) a sequence of position(<float>),label(<str>) tuples :param rotation: (float) rotation value for the labels (in degrees, clockwise-positive, by default it is 0) :param alignment: (Qt.Alignment) an alignment for the labels. If None given, it will be autocalculated ''' positions, labels = list(zip(*pos_and_labels)) # "unzipping" positions = list(positions) self.setAxisScaleEngine(axis, FixedLabelsScaleEngine(positions)) sd = FixedLabelsScaleDraw(positions, labels) sd.setLabelRotation(rotation) if alignment is None: alignment = self.getDefaultAxisLabelsAlignment(axis, rotation) sd.setLabelAlignment(alignment) self.setAxisScaleDraw(axis, sd) self.replot()
[docs] def getPickedMarker(self): '''returns the marker for the picked points for this plot :return: (TaurusCurveMarker) ''' return self._pickedMarker
[docs] def getZoomers(self, axis=None): '''returns a list of the zoomer(s) associated to the given axis. If None is passed, it returns a list containing the current zoomer :param axis: (Qwt5.QwtPlot.Axis) the axis ''' if axis is None: return [self._zoomer] elif axis == Qwt5.QwtPlot.yLeft: return [self._zoomer1] elif axis == Qwt5.QwtPlot.yRight: return [self._zoomer2] elif axis == Qwt5.QwtPlot.xBottom: return [self._zoomer1, self._zoomer2] else: raise ValueError('Invalid axis for getZoomers()')
[docs] def getGrid(self): ''' returns the grid of the plot :return: (Qwt5.QwtPlotGrid) ''' return self._grid
[docs] def getPlot(self): '''deprecated method . Only here for backwards compatibility. It will be removed, eventually. Now you should use the TaurusPlot instance instead of TaurusPlot.getPlot()''' 'DEPRECATION WARNING!: Calling TaurusPlot.getPlot() is deprecated. Use the TaurusPlot object itself instead') print(self.sender()) return self
[docs] def getCurve(self, name): '''gets a curve object by name. **Important**: Note that the curve object is not thread safe. Therefore, if you access to the curve object you must do it protected by the TaurusPlot.curves_lock reentrant lock. :param name: (str) the curve name :return: (TaurusCurve) the curve object corresponding to name ''' self.curves_lock.acquire() try: ret = self.curves.get(str(name)) finally: self.curves_lock.release() return ret
[docs] def setAxesLabelFormat(self, format=None, xformat=None, y1format=None, y2format=None): '''Convenience method for setting the format of any or all axes if format=None, specific formats for x, y1 and y2 can be explicitly set, e.g:: setAxesLabelFormat("%6.2f") #<--sets the "%6.2f" format for all axes setAxesLabelFormat(xformat=None, y1format="%i") #<--sets the default format for x and an integer format fotr y1 :param format: (str) format string to be applied to all axes. If None, the default format is used :param xformat: (str) format string to be applied to the X axis. If None, the default format is used :param y1format: (str) format string to be applied to the Y1 axis. If None, the default format is used :param y2format: (str) format string to be applied to the Y2 axis. If None, the default format is used .. seealso:: :meth:`setAxisLabelFormat` ''' if format is None: formats = [xformat, y1format, y2format] else: formats = [format] * 3 axes = [Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.yLeft, Qwt5.QwtPlot.yRight] for axis, format in zip(axes, formats): self.setAxisLabelFormat(axis, format)
[docs] def setAxisLabelFormat(self, axis, format=None): '''changes the format of an axis label. format is a python format string (e.g., "%6.2f"), . If format=None, the default behaviour is set (which uses QLocale.system().toString(value)) :param axis: (Qwt5.QwtPlot.Axis) the axis :param format: (str) format string to be applied to all axes. If None, the default format is used ''' self.axisScaleDraw(axis).setLabelFormat(format)
[docs] def getAxisLabelFormat(self, axis): '''Returns the label format for the given axis :param axis: (Qwt5.QwtPlot.Axis) the axis :return: (str or None) .. seealso:: :meth:`setAxisLabelFormat` ''' try: return self.axisScaleDraw(axis).getLabelFormat() except AttributeError: return None
[docs] def resetAxisLabelFormat(self, axis): '''equivalent to setAxisLabelFormat(axis, None) :param axis: (Qwt5.QwtPlot.Axis) the axis See also:setAxisLabelFormat ''' self.setAxisLabelFormat(axis, None)
[docs] def showMaxPeaks(self, show): """This function will set the showMaxPeak flag of all the curves in the plot. :param show: (bool) if True, the max values of the displayed curve(s) will be shown on the plot. Otherwise, they will be hidden. """ self.curves_lock.acquire() try: self._showMaxPeaks = show for curveName in self.curves.keys(): curve = self.curves.get(str(curveName)) if show: curve.showMaxPeak(True) curve.attachMaxMarker(self) else: curve.showMaxPeak(False) curve.detachMaxMarker() finally: self.curves_lock.release() self.replot()
[docs] def showMinPeaks(self, show): """This function will set the showMinPeak flag of all the curves in the plot. :param show: (bool) if True, the min values of the displayed curve(s) will be shown on the plot. Otherwise, they will be hidden. """ self.curves_lock.acquire() try: self._showMinPeaks = show for curveName in self.curves.keys(): curve = self.curves.get(str(curveName)) if show: curve.showMinPeak(True) curve.attachMinMarker(self) else: curve.showMinPeak(False) curve.detachMinMarker() finally: self.curves_lock.release() self.replot()
[docs] def showCurve(self, curve, on): '''switch visibility of a curve (as well as any markers associated to it) on/off **Important**: This is a non-thread safe method. Do not manipulate curve objects without protecting the access with Taurusplot.curves_lock :param curve: (TaurusCurve) the curve object :param on: (bool) if True, the curve will be shown otherwise it will be hidden ''' curve.setVisible(on) legend = self.legend() if legend: widget = legend.find(curve) if isinstance(curve, TaurusCurve): title = curve.title() # if hidding the curve, hide peaks also # change legend color too if on is False: title.setColor(Qt.Qt.darkGray) curve.showMaxPeak(False) curve.showMinPeak(False) curve.detachMarkers() # if the curve is shown, show the markers as well (if required) # and change the color in the legend too! else: if curve.yAxis() == Qwt5.QwtPlot.yLeft: title.setColor( else: title.setColor(Qt.Qt.darkBlue) if self._showMaxPeaks: curve.showMaxPeak(True) curve.attachMarkers(self) if self._showMinPeaks: curve.showMinPeak(True) curve.attachMarkers(self) curve.setTitle(title) widget.setText(title) self.replot()
[docs] def toggleCurveState(self, curve): '''cycles through 3 possible states for a curve: - invisible - attached to Y1 - attached to Y2 :param curve: (TaurusCurve) the curve object ''' self.curves_lock.acquire() try: # get the key in the self.curves directory curveName = None for curveName, c in self.curves.items(): if c is curve: break axis = curve.yAxis() # Toggle state if not curve.isVisible(): self.setCurvesYAxis([curveName], Qwt5.QwtPlot.yLeft) elif axis == Qwt5.QwtPlot.yLeft: self.setCurvesYAxis([curveName], Qwt5.QwtPlot.yRight) elif axis == Qwt5.QwtPlot.yRight: self.showCurve(curve, False) self.autoShowYAxes() finally: self.curves_lock.release() self.replot()
@Qt.pyqtSlot("QString", name="dataChanged")
[docs] def curveDataChanged(self, name): '''slot that is called whenever a curve emits a dataChanged signal :emits: "dataChanged(const QString &)" :param name: (str) curve name ''' self.curves_lock.acquire() try: curve = self.curves.get(str(name)) curve.safeSetData() # self._zoomer.setZoomBase() if self.getXDynScale(): # keep the scale width constant, but translate it to get the last value max = curve._xValues[-1] min = max - self.getXAxisRange() self.setAxisScale(Qwt5.QwtPlot.xBottom, min, max) finally: self.curves_lock.release() self.dataChanged.emit(str(name)) self.replot()
[docs] def attachRawData(self, rawdata, properties=None, id=None): """attaches a curve to the plot formed from raw data that comes in a dict :param rawdata: (dict) A dictionary defining a rawdata curve. It has the following structure (all keys are optional, but either "y" or "f(x)" must be present. Also, the value of x, y and f(x) can be None): {"title":<str>, "x":list<float>, "y":list<float>, "f(x)": <str (an expression to evaluate on the x values)>} :param properties: (CurveAppearanceProperties) appearance properties for the curve :param id: (str) This will be the internal name identifier used for the curve. If not given, it defaults to the title or to "rawdata" if no title is given. :return: (QwtPlotCurve) the attached curve *Note*: every member of the rawdata dictionary is optional except for the y values (or, alternatively, f(x) AND x) *Note*: using "name" in the rawdata dictionary is a still-supported-but-deprecated synonim of "title". """ if properties is None: properties = CurveAppearanceProperties(, lWidth=2) # Deprecation Warning: if "pen" in rawdata or "style" in rawdata: raise DeprecationWarning( "'pen' or 'style' are no longer supported. Use the properties parameter instead") if "name" in rawdata: if "title" in rawdata: self.error( 'Inconsistence: both "name" and "title" passed for rawdata. Use "title" only') else: self.warning( 'The use of "name" (=%s) for attaching rawdata is deprecated. Use "title" instead' % rawdata["name"]) rawdata["title"] = rawdata["name"] y = rawdata.get("y", None) fx = rawdata.get("f(x)", None) x = rawdata.get("x", None) if fx is None: if y is None: raise ValueError('Either "f(x)" or "y" keys must be present') title = str(rawdata.get("title", "rawdata")) if x is None: # if no x is given, the indices will be used x = numpy.arange(len(y)) else: x = numpy.array(x) else: if y is not None: raise ValueError( 'only one of "f(x)" or "y" keys can be present') if x is None: # we need x values in which to evaluate raise ValueError('Missing "x" values') title = str(rawdata.get("title", fx)) x = numpy.array(x) sev = SafeEvaluator({'x': x}) try: y = sev.eval(fx) except: # TODO: deal with this exception properly. self.warning( "the function '%s' could not be evaluated (skipping)" % title) return #@todo: support error bars # ex=rawdata.get("ex",numpy.zeros(len(y))) # ey=rawdata.get("ey",numpy.zeros(len(y))) # at this point, both x and y must be valid y = numpy.array(y) if id is None: name = title else: name = id self.curves_lock.acquire() try: if name in self.curves: curve = self.curves.get(name) if curve.isRawData: self.detachRawData(name) else: curve.unregisterDataChanged(self, self.curveDataChanged) curve.detach() self.curves.pop(name) self.debug('overwriting curve %s with raw data' % name) self.debug('attaching raw data with name %s' % name) curve = TaurusCurve(name, rawData=rawdata, optimized=self.isOptimizationEnabled()) # curve.fireEvent = lambda arg:None #!!! reimplementing FireEvent # on the fly! (ugly-lazy hack) curve.attach(self) if self._showMaxPeaks: curve.attachMaxMarker(self) if self._showMinPeaks: curve.attachMinMarker(self) curve._xValues, curve._yValues = x, y curve.setData(x, y) # note that the title and the name may differ curve.setTitle(title) curve.setAppearanceProperties(properties) self.curves[name] = curve self.showCurve(curve, True) self.showLegend(len(self.curves) > 1, forever=False) self._zoomer1.setZoomBase() self._zoomer2.setZoomBase() self.replot() finally: self.curves_lock.release() return curve
[docs] def detachRawData(self, name): '''dettaches a raw data curve :param name: (str) name (identifier) of the curve to dettach ''' name = str(name) self.debug("detaching raw data with name %s" % name) self.curves_lock.acquire() try: curve = self.curves.get(name) if curve is None or not curve.isRawData: self.error( "detachRawData failed: '%s' is not a rawData curve" % name) return curve.detach() self.curves.pop(name) finally: self.curves_lock.release() self.replot()
[docs] def clearAllRawData(self): """ removes all rawdata curves from the plot. :return: (list<str>) the list of removed curve names """ self.curves_lock.acquire() try: names = [name for name in self.curves if self.curves[ name].isRawData] finally: self.curves_lock.release() for name in names: self.detachRawData(name) return names
[docs] def getCurveData(self, curvename, numpy=False): """returns the data in the curve as two lists (x,y) of values :param curvename: (str) the curve name :param numpy: (bool) if True, the result is returned as numpy arrays instead of lists :return: (tuple<list,list>) tuple of two lists (x,y) containing the curve data """ self.curves_lock.acquire() try: if curvename in self.curves: data = self.curves[curvename].data() x = [data.x(i) for i in range(data.size())] y = [data.y(i) for i in range(data.size())] else: self.error("Curve '%s' not found" % curvename) raise KeyError() finally: self.curves_lock.release() if numpy: x, y = numpy.array(x), numpy.array(y) return x, y
[docs] def updateCurves(self, names): ''' Updates the TaurusCurves being plotted. It adds a new curve for each new curve model passed and removes curves if they are not in the names. :param names: (sequence<str>) a sequence of curve models. One curve will be created for each element of names. Each curve model can consist of a single attribute name (which will be used for the Y values) or by two attribute names separated by a '|' (in which case, the left-hand attribute is used for the X values and the right hand value for the Y values) ''' self.curves_lock.acquire() try: xnames, ynames = [], [] for name in names: n = name.split("|") yname = n[-1] xname = None if len(n) > 1: xname = n[0] xnames.append(xname) ynames.append(yname) del_curves = [name for name in self.curves if name not in ynames] # if all curves were removed, reset the color palette if len(del_curves) == len(self.curves): self._curvePens.setCurrentIndex(0) for i, name in enumerate(ynames): xname = xnames[i] name = str(name) self.debug('updating curve %s' % name) if name not in self.curves: curve = TaurusCurve(name, xname, self, optimized=self.isOptimizationEnabled()) curve.attach(self) self.curves[name] = curve self.showCurve(curve, True) if self._showMaxPeaks: curve.attachMaxMarker(self) if self._showMinPeaks: curve.attachMinMarker(self) curve.setPen(next(self._curvePens)) curve.setUseParentModel(self.getUseParentModel()) curve.setTitleText(self.getDefaultCurvesTitle()) curve.registerDataChanged(self, self.curveDataChanged) self.curveDataChanged(name) # curves to be removed for name in del_curves: name = str(name) #curve = self.curves.pop(name) curve = self.curves.get(name) if not curve.isRawData: # The rawdata curves should not be dettached by updateCurves. Call detachRawdata insted curve.unregisterDataChanged(self, self.curveDataChanged) curve.detach() self.curves.pop(name) if del_curves: self.autoShowYAxes() # legend self.showLegend(len(self.curves) > 1, forever=False) self.replot() finally: self.curves_lock.release()
[docs] def getLegend(self): '''Returns the legend object of this plot :return: (QwtLegend) ''' return self._legend
[docs] def updateLegend(self, force=False): '''Updates the legend object of the plot (if it does not exist, it may create a fresh one) :param force: (bool) if True, the legend will be updated even if it is not being shown. (default=False) ''' if self._showLegend: if force or not self.legend(): self._legend = Qwt5.QwtLegend() self._legend.setItemMode(Qwt5.QwtLegend.ClickableItem) self._legend.setDisplayPolicy(Qwt5.QwtLegend.FixedIdentifier, Qwt5.QwtLegendItem.ShowLine | Qwt5.QwtLegendItem.ShowSymbol | Qwt5.QwtLegendItem.ShowText) self.insertLegend(self._legend, self._legendPos) self._legend.setToolTip( "Clicking on a legend item changes\n the associated Y axis for the curve.") else: self._legend = None self.insertLegend(None)
[docs] def showLegend(self, show, forever=True): '''whether to show or not the legend. :param show: (bool) if True, the legend will be shown :param forever: (bool) if True, the setting will be permant (e.g., the legend won't be hidden even if only one curve is plotted) (default=True) ''' if forever: self._legendDecissionIsForever = forever self._showLegend = show else: if not self._legendDecissionIsForever: self._showLegend = show self.updateLegend() self._showLegendAction.setChecked(self._showLegend)
[docs] def getModelObj(self, idx): '''See :meth:`TaurusBaseComponent.getModelObj`''' return self.getCurve(self._modelNames[idx]).getModelObj()
# def defineStyle(self): # pass
[docs] def minimumSizeHint(self): '''See :meth:`QWidget.minimumSizeHint`''' return Qt.QSize(48, 26)
[docs] def sizeHint(self): '''See :meth:`QWidget.sizeHint`''' return Qt.QSize(300, 200)
@Qt.pyqtSlot('QString', name='modelChanged')
[docs] def parentModelChanged(self, parentmodel_name): '''See :meth:`TaurusBaseComponent.parentModelChanged`''' self.curves_lock.acquire() try: for curve in self.curves.values(): curve.setModelCheck(curve.getModel(), False) finally: self.curves_lock.release()
[docs] def getParentTaurusComponent(self): '''See :meth:`TaurusBaseComponent.getParentTaurusComponent`''' p = self.parentWidget() while p and not isinstance(p, TaurusBaseComponent): if isinstance(p, Qt.QDialog) or isinstance(p, Qt.QMainWindow): p = None break p = p.parentWidget() return p
# def keyPressEvent(self,keyEvent): # """This function will capture any key press and react on ESC key pressed to set autoscale on all axis""" # #Leave this commented unless you want to debug # #elif (keyEvent.key() == Qt.Qt.Key_D): # # self.__debug() # # keyEvent.accept() # # # #else: # # keyEvent.ignore() # return
[docs] def closeEvent(self, event): '''See :meth:`Qwidget.closeEvent`''' # make sure no dialogs are left open if self.DataImportDlg is not None: self.DataImportDlg.close()
[docs] def contextMenuEvent(self, event): """ This function is called when there is context menu event. See :meth:`Qwidget.closeEvent` A pop up menu will be shown with the available options. Different parts of the plot (canvas, axes,...) behave differently""" # print "!!!!!!!!", self.canvas().underMouse(), # self.axisWidget(self.yLeft).underMouse(), # self.axisWidget(self.yRight).underMouse(), # self.axisWidget(self.xBottom).underMouse() if self.canvas().underMouse(): self._canvasContextMenu().exec_(event.globalPos()) elif self.axisWidget(self.yLeft).underMouse(): self._axisContextMenu(self.yLeft).exec_(event.globalPos()) elif self.axisWidget(self.yRight).underMouse(): self._axisContextMenu(self.yRight).exec_(event.globalPos()) elif self.axisWidget(self.xBottom).underMouse(): self._axisContextMenu(self.xBottom).exec_(event.globalPos()) else: # default catch-all #@TODO FOR SOME REASON, the underMouse() method # used above fails sometimes !!!??? self._canvasContextMenu().exec_(event.globalPos()) event.accept()
def _canvasContextMenu(self): """Returns a contextMenu for the canvas :return: (Qt.QMenu) the context menu for the canvas """ menu = Qt.QMenu(self) menu.addAction(self._configDialogAction) menu.addAction(self._inputDataAction) menu.addAction(self._saveConfigAction) menu.addAction(self._loadConfigAction) menu.addAction(self._setCurvesTitleAction) menu.addSeparator() scalesSubMenu = menu.addMenu("&Scales") scalesSubMenu.addAction(self._autoscaleAllAxisAction) scalesSubMenu.addSeparator() for axis in (Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.yLeft, Qwt5.QwtPlot.yRight): if self.axisEnabled(axis): scalesSubMenu.addMenu(self._axisContextMenu(axis=axis)) menu.addAction(self._showMaxAction) menu.addAction(self._showMinAction) menu.addAction(self._showLegendAction) menu.addAction(self._dataInspectorAction) menu.addAction(self._setFormatterAction) menu.addSeparator() exportSubMenu = menu.addMenu("&Export && Print") menu.addAction(self._curveStatsAction) exportSubMenu.addAction(self._printAction) exportSubMenu.addAction(self._exportPdfAction) exportSubMenu.addAction(self._exportAsciiAction) menu.addSeparator() menu.addAction(self._pauseAction) if self.isWindow(): menu.addAction(self._closeWindowAction) return menu def _axisContextMenu(self, axis=None): '''Returns a context menu for the given axis :param axis: (Qwt5.QwtPlot.Axis) the axis :return: (Qt.QMenu) the context menu for the given axis ''' menu = Qt.QMenu(self) axisname = self.getAxisName(axis) menu.setTitle("Options for axis %s" % axisname) autoScaleThisAxis = lambda: self.setAxisAutoScale(axis=axis) autoscaleAction = menu.addAction("AutoScale %s" % axisname) autoscaleAction.triggered.connect(autoScaleThisAxis) if not self.getXIsTime(): switchThisAxis = lambda: self.setAxisScaleType( axis=axis, scale=None) switchThisAxisAction = menu.addAction( "Toggle linear/log for %s" % axisname) switchThisAxisAction.triggered.connect(switchThisAxis) if axis in (Qwt5.QwtPlot.yLeft, Qwt5.QwtPlot.yRight): zoomOnThisAxis = lambda: self.toggleZoomer(axis=axis) zoomOnThisAxisAction = menu.addAction( "Zoom-to-region acts on %s" % axisname) zoomOnThisAxisAction.triggered.connect(zoomOnThisAxis) elif axis in (Qwt5.QwtPlot.xBottom, Qwt5.QwtPlot.xTop): if self.isXDynScaleSupported(): xDynAction = menu.addAction("&Auto-scroll %s" % axisname) xDynAction.setToolTip( 'If enabled, the scale of %s will be autoadjusted to provide a fixed window moving to show always the last value') xDynAction.setCheckable(True) xDynAction.setChecked(self.getXDynScale()) xDynAction.toggled.connect(self.setXDynScale) return menu
[docs] def showConfigDialog(self): """Slot for the showConfigMenuAction. Launches the plot configuration dialog. """ self._configDialog = TaurusPlotConfigDialog(self) self._configDialog.exec_() # destroy the dialog (it may probably not be used anymore) del self._configDialog
[docs] def getCurveAppearancePropertiesDict(self): '''Returns the appearance properties of all curves in the plot. :return: (dict<str,CurveAppearanceProperties>) a dictionary whose keys are the curve names and whose values are the corresponding CurveAppearanceProperties object .. seealso:: :meth:`setCurveAppearanceProperties` ''' self.curves_lock.acquire() try: propdict = {} for name, curve in self.curves.items(): propdict[name] = copy.deepcopy(curve.getAppearanceProperties()) finally: self.curves_lock.release() return propdict
[docs] def setCurveAppearanceProperties(self, propDict): """It gets a dictionary of namecurvenames,properties and applies the properties to the corresponding curves. :param propDict: (dict<str,CurveAppearanceProperties>) a dictionary whose keys are the curve names and whose values are the corresponding CurveAppearanceProperties object .. seealso:: :meth:`getCurveAppearancePropertiesDict` """ self.curves_lock.acquire() try: for name, prop in propDict.items(): c = self.curves[name] c.setAppearanceProperties(copy.deepcopy(prop)) visible = getattr(prop, 'visible', True) if visible is not None: self.showCurve(c, visible) finally: self.curves_lock.release() self.autoShowYAxes() self.replot()
[docs] def onCurveAppearanceChanged(self, prop, names): """Applies the properties given in prop to all the curves named in names. This functions is called from the config dialog when changes are applied. :param prop: (CurveAppearanceProperties) the properties object :param names: (sequence<str>) a sequence of names of curves to which the properties should be applied """ propDict = {} for name in names: propDict[name] = prop self.setCurveAppearanceProperties(propDict)
def _createAxesDict(self): '''returns a dictionary containing relevant information about the current axes :return: (dict) Configurations that can be loaded with applyAxesConfig ''' xMin, xMax = self.getAxisScale(Qwt5.QwtPlot.xBottom) y1Min, y1Max = self.getAxisScale(Qwt5.QwtPlot.yLeft) y2Min, y2Max = self.getAxisScale(Qwt5.QwtPlot.yRight) axesdict = {'xMin': xMin, 'xMax': xMax, 'y1Min': y1Min, 'y1Max': y1Max, 'y2Min': y2Min, 'y2Max': y2Max, 'xMode': int(self.getAxisTransformationType(Qwt5.QwtPlot.xBottom)), 'y1Mode': int(self.getAxisTransformationType(Qwt5.QwtPlot.yLeft)), 'y2Mode': int(self.getAxisTransformationType(Qwt5.QwtPlot.yRight)), 'xDyn': self.getXDynScale(), 'xIsTime': self.getXIsTime() } return axesdict def _createMiscDict(self): '''returns a dictionary containing misc information about the plot :return: (dict) configurations that can be loaded with applyMiscConfig ''' miscdict = {'defaultCurvesTitle': self.getDefaultCurvesTitle(), 'canvasBackground': self.canvasBackground(), 'orderedCurveNames': self.getCurveNamesSorted(), 'plotTitle': str(self.title().text()), 'formatter': self.getFormat()} if self.isWindow(): miscdict["Geometry"] = self.saveGeometry() return miscdict
[docs] def checkConfigVersion(self, configdict, showDialog=False, supportedVersions=None): ''' Check if the version of configdict is supported. :param configdict: (dict) configuration dictionary to check :param showDialog: (bool) whether to show a QtWarning dialog if check failed (false by default) :param supportedVersions: (sequence<str>, or None) supported version numbers, if None given, the versions supported by this widget will be used (i.e., those defined in self._supportedConfigVersions) :return: (bool) returns True if the configdict is of the right version .. seealso:: :meth:`TaurusBaseComponent.checkConfigVersion` ''' if supportedVersions is None: supportedVersions = self._supportedConfigVersions version = configdict.get("ConfigVersion", "__UNVERSIONED__") if version not in supportedVersions: msg = 'Unsupported Config Version %s. (Supported: %s)' % ( version, repr(supportedVersions)) self.warning(msg) if showDialog: Qt.QMessageBox.warning( self, "Wrong Configuration Version", msg, Qt.QMessageBox.Ok) return False return True
[docs] def createConfigDict(self, allowUnpickable=False, curvenames=None): "Deprecation warning: createConfigDict is deprecated. Please use createConfig instead") return self.createConfig(allowUnpickable=False, curvenames=None)
[docs] def createConfig(self, allowUnpickable=False, curvenames=None, **kwargs): '''Returns a pickable dictionary containing all relevant information about the current plot. Implemented as in :meth:`TaurusBaseComponent.createConfig` For Tango attributes it stores the attribute name and the curve properties For raw data curves, it stores the data as well. Hint: The following code allows you to serialize the configuration dictionary as a string (which you can store as a QSetting, or as a Tango Attribute):: import pickle c = pickle.dumps(taurusplot.createConfig()) #c is a string that can be stored :param curvenames: (sequence<str>) a sequence of curve names for which the configuration will be stored (all by default). :return: (dict) configurations (which can be loaded with applyConfig) .. seealso:: :meth:`createConfig`, :meth:`TaurusBaseComponent.createConfig` ''' axesdict = self._createAxesDict() rawdatadict = CaselessDict() tangodict = CaselessDict() propdict = {} miscdict = self._createMiscDict() self.curves_lock.acquire() try: if curvenames is None: curvenames = list(self.curves) curvenames = self._lowerIfInsensitive(curvenames) for name in curvenames: curve = self.curves.get(name) propdict[name] = copy.deepcopy(curve.getAppearanceProperties()) if curve.isRawData: rawdatadict[name] = curve.getRawData() else: tangodict[name] = curve.getModel() except Exception as e: self.error( 'Exception while gathering curves configuration info' + str(e)) finally: self.curves_lock.release() curvenames = CaselessList(curvenames) model = CaselessList([m for m in self.getModel() if m in curvenames]) configdict = {"Axes": axesdict, "Misc": miscdict, "RawData": rawdatadict, "TangoCurves": tangodict, "CurveProp": propdict, "ConfigVersion": self._supportedConfigVersions[-1], "model": model} return configdict
[docs] def applyConfig(self, configdict, **kwargs): """implemented as in :meth:`TaurusBaseComponent.applyConfig` :param configdict: (dict<str,object>) .. seealso:: :meth:`createConfig`, :meth:`TaurusBaseComponent.applyConfig` """ if not self.checkConfigVersion(configdict): return # attach the curves for rd in configdict["RawData"].values(): self.attachRawData(rd) # for backwards compatibility, if the ordered list of models is not # stored, it uses the unsorted dict values models = configdict.get("model", list(configdict["TangoCurves"].values())) self.addModels(models) # set curve properties self.setCurveAppearanceProperties(configdict["CurveProp"]) self.updateLegend(force=True) # set the axes self.applyAxesConfig(configdict["Axes"]) # set other misc configurations self.applyMiscConfig(configdict["Misc"])
[docs] def applyMiscConfig(self, miscdict): '''sets the configurations according to settings stored in the misc dict, which can be generated with _createMiscDict() :param miscdict: (dict) Dictionary of properties ''' self.setDefaultCurvesTitle(miscdict["defaultCurvesTitle"]) self.setCanvasBackground(miscdict["canvasBackground"]) self.sortCurves(ordered=miscdict.get("orderedCurveNames", None)) if "plotTitle" in miscdict: self.setTitle(miscdict['plotTitle']) # set geometry (if this is a top level window) if self.isWindow() and 'Geometry' in miscdict: self.restoreGeometry(miscdict['Geometry']) if "formatter" in miscdict: self.setFormat(miscdict['formatter'])
[docs] def applyAxesConfig(self, axes): '''sets the axes according to settings stored in the axes dict, which can be generated with _createAxesDict() :param axes: (dict) contains axes properties ''' self.setXIsTime(axes["xIsTime"]) self.setXDynScale(axes["xDyn"]) self.setAxisScale(Qwt5.QwtPlot.xBottom, axes["xMin"], axes["xMax"]) self.setAxisScale(Qwt5.QwtPlot.yLeft, axes["y1Min"], axes["y1Max"]) self.setAxisScale(Qwt5.QwtPlot.yRight, axes["y2Min"], axes["y2Max"]) if not self.getXIsTime(): self.setAxisScaleType(Qwt5.QwtPlot.xBottom, axes["xMode"]) self.setAxisScaleType(Qwt5.QwtPlot.yLeft, axes["y1Mode"]) self.setAxisScaleType(Qwt5.QwtPlot.yRight, axes["y2Mode"])
[docs] def saveConfig(self, ofile=None, curvenames=None): """Stores the current curves and their display properties in a file for later retrieval. :param ofile: (file or string) file or filename to store the configuration. If None passed, :param curvenames: (list<str>) a list of curve names for which the configuration will be stored (all by default). :return: (str) file name used """ import pickle if ofile is None: ofile, _ = compat.getSaveFileName( self, 'Save Taurusplot Configuration', 'TaurusplotConfig.pck', 'TaurusPlot Curve Properties File (*.pck)' ) if not ofile: return if not isinstance(ofile, file): ofile = open(ofile, 'w') configdict = self.createConfig(curvenames=curvenames)"Saving current settings in '%s'" % pickle.dump(configdict, ofile) return
[docs] def loadConfig(self, ifile=None): """Reads a file stored by saveConfig() and applies the settings :param ifile: (file or string) file or filename from where to read the configuration :return: (str) file name used """ import pickle if ifile is None: ifile, _ = compat.getOpenFileName( self, 'Load Taurusplot Configuration', '', 'TaurusPlot Curve Properties File (*.pck)') if not ifile: return if not isinstance(ifile, file): ifile = open(ifile, 'r') configdict = pickle.load(ifile) self.applyConfig(configdict) return
[docs] def setEventFilters(self, filters=None, curvenames=None, preqt=False): '''propagates a list of taurus filters to the curves given by curvenames. See :meth:`TaurusBaseComponent.setEventFilters` ''' if curvenames is None: curvenames = self.curves.keys() self.curves_lock.acquire() try: for name in curvenames: self.curves[name].setEventFilters(filters, preqt=preqt) finally: self.curves_lock.release()
[docs] def setAxisAutoScale(self, axis): """Sets the axis to autoscale and resets the zoomer for that axis if needed :param axis: (Qwt5.QwtPlot.Axis) the axis .. seealso:: :meth:`autoScaleAllAxes` """ Qwt5.QwtPlot.setAxisAutoScale(self, axis) for z in self.getZoomers(axis): z.setZoomBase() self.replot()
[docs] def autoScaleAllAxes(self): '''Optimized autoscale of whole plot''' minX = float('inf') maxX = float('-inf') if self.getXDynScale(): originalXRange = self.getXAxisRange() self.curves_lock.acquire() try: for c in self.curves.values(): if c.minXValue() < minX: minX = c.minXValue() if c.maxXValue() > maxX: maxX = c.maxXValue() if minX != maxX: break finally: self.curves_lock.release() for axis in range(Qwt5.QwtPlot.axisCnt): if axis == Qwt5.QwtPlot.xBottom and minX == maxX: Qwt5.QwtPlot.setAxisScale( self, axis, minX - 0.5 * originalXRange, minX + 0.5 * originalXRange) else: Qwt5.QwtPlot.setAxisAutoScale(self, axis) self.replot() # Update the zoom stacks self._zoomer1.setZoomBase() self._zoomer2.setZoomBase()
[docs] def setAxisScale(self, axis, min, max): """Rescales the given axis to the range defined by min and max. If min and max are None, autoscales. It also takes care of resetting the affected zoomer(s) :param axis: (Qwt5.QwtPlot.Axis) the axis :param min: (float or None) minimum value for the axis :param max: (float or None) maximum value for the axis **Example**:: tt=TaurusTrend() tt.setAxisScale(tt.yLeft, 0, 10) #this will set the Y1 axis range from 0 to 10 tt.setAxisScale(tt.xBottom, None, None) #This will autoscale the X axis """ if min is None and max is None: self.setAxisAutoScale(axis) else: Qwt5.QwtPlot.setAxisScale(self, axis, min, max) self.replot() for z in self.getZoomers(axis): z.setZoomBase()
[docs] def getAxisScale(self, axis): """returns the lower and higher bounds for the given axis, or None,None if the axis is in autoscale mode :param axis: (Qwt5.QwtPlot.Axis) the axis :return: (float,float) atuple of floats (or None,None) """ if self.axisAutoScale(axis): return None, None return self.axisScaleDiv(axis).lowerBound(), self.axisScaleDiv(axis).upperBound()
[docs] def getXAxisRange(self, axis=Qwt5.QwtPlot.xBottom): '''same as self.axisScaleDiv(axis).range() :param axis: (Qwt5.QwtPlot.Axis) the (X) axis. (default=Qwt5.QwtPlot.xBottom) :return: (float) the absolute difference between the higher and lower limits of the axis scale ''' return self.axisScaleDiv(axis).range()
[docs] def setAxisScaleType(self, axis, scale=None): '''sets the type of scale, (log or linear) for a given axis, If scale is None, the scale type will be toggled :param axis: (Qwt5.QwtPlot.Axis) the axis :param scale: (Qwt5.QwtScaleTransformation.Type) the scale transformation. For convenience, the strings "Linear" and "Logarithmic" can be used as well''' if self.getXIsTime() and isinstance(self.axisScaleEngine(axis), DateTimeScaleEngine): raise ValueError( 'TaurusPlot.setAxisScaleType cannot be called with time scales') if not Qwt5.QwtPlot.axisValid(axis): self.error("TaurusPlot.setScale() invalid axis: " + axis) if scale is None: currentType = self.getAxisTransformationType(axis) if currentType == Qwt5.QwtScaleTransformation.Linear: scale = Qwt5.QwtScaleTransformation.Log10 elif currentType == Qwt5.QwtScaleTransformation.Log10: scale = Qwt5.QwtScaleTransformation.Linear if scale in ("Linear", Qwt5.QwtScaleTransformation.Linear, int(Qwt5.QwtScaleTransformation.Linear)): newScale = Qwt5.QwtLinearScaleEngine() elif scale in ("Logarithmic", Qwt5.QwtScaleTransformation.Log10, int(Qwt5.QwtScaleTransformation.Log10)): newScale = Qwt5.QwtLog10ScaleEngine() else: self.error( "TaurusPlot.setAxisScaleType() invalid scale: %s" % str(scale)) return self.setAxisScaleEngine(axis, newScale) # update the data in the curves (because of the filtering done for # possitive values in log mode) self.__updateCurvesData() return
[docs] def axisScaleDiv(self, axis): """ Return the scale division of a specified axis. :param axis: (Qwt5.QwtPlot.Axis) the axis :return: (Qwt5.QwtScaleDiv) scale division """ div = Qwt5.QwtPlot.axisScaleDiv(self, axis) # fix compatibility issue with Qwt < 5.2 (contributed by A. Persson) if Qwt5.QWT_VERSION < 0x050200: div.lowerBound = div.lBound div.upperBound = div.hBound return div
def __updateCurvesData(self): '''call safeSetData again on all curves to force a refiltering in case the scale changed its type''' self.curves_lock.acquire() try: for c in self.curves.values(): c.safeSetData() finally: self.curves_lock.release() self.replot()
[docs] def exportPdf(self, fileName=None): """Export the plot to a PDF. slot for the _exportPdfAction. :param fileName: (str) The name of the file to which the plot will be exported. If None given, the user will be prompted for a file name. """ if fileName is None: fileName, _ = compat.getSaveFileName( self, 'Export File Name', 'plot.pdf', 'PDF Documents (*.pdf)') fileName = str(fileName) if fileName: try: # check if the file is actually writable f = open(fileName, 'w') f.close() except: self.error("Can't write to '%s'" % fileName) Qt.QMessageBox.warning(self, "File Error", "Can't write to\n'%s'" % fileName, Qt.QMessageBox.Ok) return printer = Qt.QPrinter() printer.setOutputFormat(Qt.QPrinter.PdfFormat) printer.setOrientation(Qt.QPrinter.Landscape) printer.setOutputFileName(fileName) printer.setCreator('TaurusPlot') self.print_(printer)
[docs] def exportPrint(self): '''Launches a QPrintDialog for printing the plot''' printer = Qt.QPrinter(Qt.QPrinter.HighResolution) printer.setOutputFileName('') printer.setCreator('TaurusPlot') printer.setOrientation(Qt.QPrinter.Landscape) printer.setColorMode(Qt.QPrinter.Color) docName = str(self.title().text()) if docName: docName.replace('\n', ' -- ') printer.setDocName(docName) dialog = Qt.QPrintDialog(printer) if dialog.exec_(): filter = Qwt5.QwtPlotPrintFilter() if (Qt.QPrinter.GrayScale == printer.colorMode()): filter.setOptions(Qwt5.QwtPlotPrintFilter.PrintAll & ~Qwt5.QwtPlotPrintFilter.PrintBackground | Qwt5.QwtPlotPrintFilter.PrintFrameWithScales) self.print_(printer, filter)
[docs] def exportAscii(self, curves=None): '''Opens a dialog for exporting curves to ASCII files. :param curves: (sequence<str>) the curves curves that will be exportable. if None given, all curves are offered for export. ''' self.curves_lock.acquire() try: if curves is None: curves = self.getCurveNamesSorted() frozendata = {} for k in curves: frozendata[k] = self.getCurveData(k) finally: self.curves_lock.release() klass = getattr(self, 'exportDlgClass', None) if klass is None: from taurus.qt.qtgui.panel import QDataExportDialog klass = QDataExportDialog dialog = klass(parent=self, datadict=frozendata, sortedNames=curves) dialog.setXIsTime(self.getXIsTime()) return dialog.exec_()
[docs] def importAscii(self, filenames=None, xcol=None, **kwargs): '''imports curves from ASCII files. It uses :meth:numpy.loadtxt The data in the file(s) must be formatted in columns, with possibly a header and/or commented lines. Each column in a file will be imported as an independent RawData curve (except for the column whose index is passed in xcol) :param filenames: (sequence<str> or None) the names of the files to be read. If None passed, the user will be allowed to select them from a dialog. (default=None) :param xcol: (int or None) index of the column (starting at 0) containing the abscissas data. If None passed, the abcissa is generated as indexes starting from 0. :param `**kwargs`: Other keyword arguments can be passed to this method, which will be passed to :meth:`numpy.loadtxt` when reading each file. Accepted keywords are: - dtype=<type 'float'> - comments='#' - delimiter=None - converters=None - skiprows=0 - usecols=None - unpack=False .. seealso:: :meth:`numpy.loadtxt` ''' if filenames is None: filenames, _ = compat.getOpenFileNames( self, 'Choose input files', '', 'Ascii file (*)') if not filenames: return False rawdata = {} for fname in filenames: fname = str(fname) if self.xIsTime and xcol is not None: converters = kwargs.get('converters', {}) converters[xcol] = isodatestr2float kwargs['converters'] = converters M = numpy.loadtxt(fname, **kwargs) if len(M.shape) == 1: # make sure we are dealing with a 2D matrix even if it is just # a colum M = M.reshape(M.size, 1) if xcol is None: rawdata["x"] = None else: rawdata["x"] = M[:, xcol] for col in range(M.shape[1]): if col == xcol: continue # ignore the xcol (it has already been set) rawdata["y"] = M[:, col] rawdata["title"] = "%s[%i]" % (os.path.basename(fname), col) self.attachRawData(copy.deepcopy(rawdata))
[docs] def showDataImportDlg(self): '''Launches the data import dialog. This dialog lets the user manage which attributes are attached to the plot (using :class:`TaurusModelChooser`) and also to generate raw data or import it from files ''' if self.DataImportDlg is None: from taurus.qt.qtgui.panel import TaurusModelChooser self.DataImportDlg = Qt.QDialog(self) self.DataImportDlg.setWindowTitle( "%s - Import Data" % (str(self.windowTitle()))) self.DataImportDlg.modelChooser = TaurusModelChooser( selectables=[taurus.core.taurusbasetypes.TaurusElementType.Attribute]) from taurus.qt.qtgui.panel import QRawDataWidget self.DataImportDlg.rawDataChooser = QRawDataWidget() tabs = Qt.QTabWidget() tabs.addTab(self.DataImportDlg.modelChooser, "&Attributes") tabs.addTab(self.DataImportDlg.rawDataChooser, "&Raw Data") mainlayout = Qt.QVBoxLayout(self.DataImportDlg) mainlayout.addWidget(tabs) self.DataImportDlg.modelChooser.updateModels.connect(self.setModel) self.DataImportDlg.rawDataChooser.ReadFromFiles.connect(self.readFromFiles) self.DataImportDlg.rawDataChooser.AddCurve.connect(self.attachRawData) models_and_display = [(m, self.getCurveTitle( m.split('|')[-1])) for m in self._modelNames] self.DataImportDlg.modelChooser.setListedModels(models_and_display)
[docs] def readFromFiles(self, xcol, skiprows): '''helper slot. Calls self.importAscii(xcol=xcol, skiprows=skiprows ) See meth:`importAscii`''' self.importAscii(xcol=xcol, skiprows=skiprows)
[docs] def isXDynScaleSupported(self): '''Whether this widget offers xDynScale-related options. Useful for showing-hiding them in menus and dialogs :return: (bool) .. seealso:: :meth:`setXDynScaleSupported`, :meth:`getXDynScale` ''' return self._xDynScaleSupported
[docs] def setXDynScaleSupported(self, supported): '''Whether this widget should offer xDynScale-related options in menus and dialogs. :param supported: (bool) if True, the options related to xDynScale will be shown .. seealso:: :meth:`isXDynScaleSupported`, :meth:`getXDynScale`''' self._xDynScaleSupported = False
[docs] def setXDynScale(self, enabled=True): '''it enables/disables the Dynamic scaling feature (also known as Fixed-range X scale, or "auto-scroll mode"). The Dynamic scaling consists in ensuring that: - the range (=max-min) is always constant - the latest point plotted is always within range. :param enabled: (bool) if True, the Dynamic scaling is enabled for the X axis. Otherwise it is disabled. (Default=True) .. seealso:: :meth:`getXDynScale`, :meth:`setXDynScaleSupported`''' self._xDynScale = enabled
[docs] def getXDynScale(self): '''Whether the current X scale is in Dynamic scaling mode :return: (bool) .. seealso:: :meth:`setXDynScale`, meth:`isXDynScaleSupported` ''' return self._xDynScale
[docs] def setCurvesYAxis(self, curvesNamesList, axis): """Change the Y axis of the given curves to the given axis. :param curvesNamesList: (list<str>) the names of the curves whose Y axis is to be changed :param axis: (Qwt5.QwtPlot.Axis) the axis """ if not Qwt5.QwtPlot.axisValid(axis): raise ValueError("TaurusPlot::setCurvesYAxis. Invalid axis ID: " + \ repr(axis)) self.curves_lock.acquire() try: for curveName in curvesNamesList: curve = self.curves.get(curveName) curve.setYAxis(axis) curve._maxPeakMarker.setYAxis(axis) curve._minPeakMarker.setYAxis(axis) self.showCurve(curve, True) finally: self.curves_lock.release() # disable the other axis if no curves are assigned to it self.autoShowYAxes() # change the axis of the picked marker if needed if self._pickedCurveName in curvesNamesList: self._pickedMarker.setYAxis(axis) self.CurvesYAxisChanged.emit(curvesNamesList, axis) self.replot()
[docs] def autoShowYAxes(self): """shows/hides Y1 and Y2 depending of whether there are curves associated to them. Also takes care of changing the zoomer if needed""" self.curves_lock.acquire() try: # get a list of *unique* axes with visible curves attached axes = list( set([curve.yAxis() for curve in self.curves.values() if curve.isVisible()])) n = len(axes) if n == 0: self.enableAxis(Qwt5.QwtPlot.yLeft, False) self.enableAxis(Qwt5.QwtPlot.yRight, False) elif n == 1: for axis in [Qwt5.QwtPlot.yLeft, Qwt5.QwtPlot.yRight]: self.enableAxis(axis, (axis == axes[0])) # enable the zoom of the axis that has contents self.toggleZoomer(axes[0]) else: self.enableAxis(Qwt5.QwtPlot.yLeft, True) self.enableAxis(Qwt5.QwtPlot.yRight, True) finally: self.curves_lock.release()
[docs] def pickDataPoint(self, pos, scope=20, showMarker=True, targetCurveNames=None): '''Finds the pyxel-wise closest data point to the given position. The valid search space is constrained by the scope and targetCurveNames parameters. :param pos: (Qt.QPoint or Qt.QPolygon) the position around which to look for a data point. The position should be passed as a Qt.QPoint (if a Qt.QPolygon is given, the first point of the polygon is used). The position is expected in pixel units, with (0,0) being the top-left corner of the plot canvas. :param scope: (int) defines the area around the given position to be considered when searching for data points. A data point is considered within scope if its manhattan distance to position (in pixels) is less than the value of the scope parameter. (default=20) :param showMarker: (bool) If True, a marker will be put on the picked data point. (default=True) :param targetCurveNames: (sequence<str>) the names of the curves to be searched. If None passed, all curves will be searched :return: (tuple<Qt.QPointF,str,int> or tuple<None,None,None>) if a point was picked within the scope, it returns a tuple containing the picked point (as a Qt.QPointF), the curve name and the index of the picked point in the curve data. If no point was found within the scope, it returns None,None,None ''' if isinstance(pos, Qt.QPolygon): pos = pos.first() scopeRect = Qt.QRect(0, 0, scope, scope) scopeRect.moveCenter(pos) mindist = scope picked = None pickedCurveName = None pickedIndex = None self.curves_lock.acquire() try: if targetCurveNames is None: targetCurveNames = self.curves.keys() for name in targetCurveNames: curve = self.curves.get(name, None) if curve is None: self.error("Curve '%s' not found" % name) if not curve.isVisible(): continue data = for i in range(data.size()): point = Qt.QPoint(self.transform(curve.xAxis(), data.x( i)), self.transform(curve.yAxis(), data.y(i))) if scopeRect.contains(point): dist = (pos - point).manhattanLength() if dist < mindist: mindist = dist picked = Qt.QPointF(data.x(i), data.y(i)) pickedCurveName = name pickedIndex = i pickedAxes = curve.xAxis(), curve.yAxis() _displayValue = getattr(curve, 'owner', curve ).displayValue finally: self.curves_lock.release() if showMarker and picked is not None: self._pickedMarker.detach() self._pickedMarker.setValue(picked) self._pickedMarker.setAxis(*pickedAxes) self._pickedMarker.attach(self) self._pickedCurveName = pickedCurveName self._pickedMarker.pickedIndex = pickedIndex pickedCurveTitle = self.getCurveTitle(pickedCurveName) self.replot() label = self._pickedMarker.label() display_y = _displayValue(picked.y()) if self.getXIsTime(): infotxt = "'%s'[%i]:\n\t (t=%s, y=%s)" % ( pickedCurveTitle, pickedIndex, datetime.fromtimestamp(picked.x()).ctime(), display_y ) else: infotxt = "'%s'[%i]:\n\t (x=%.5g, y=%s)" % ( pickedCurveTitle, pickedIndex, picked.x(), display_y ) label.setText(infotxt) fits = label.textSize().width() < self.size().width() if fits: self._pickedMarker.setLabel(Qwt5.QwtText(label)) self._pickedMarker.alignLabel() self.replot() else: popup = Qt.QWidget(self, Qt.Qt.Popup) popup.setLayout(Qt.QVBoxLayout()) # @todo: make the widget background semitransparent green! popup.layout().addWidget(Qt.QLabel(infotxt)) popup.setWindowOpacity(self._pickedMarker.labelOpacity) popup.move(self.pos().x() - popup.size().width(), self.pos().y()) popup.move(self.pos()) Qt.QTimer.singleShot(5000, popup.hide) return picked, pickedCurveName, pickedIndex
[docs] def toggleDataInspectorMode(self, enable=None): ''' Enables/Disables the Inspector Mode. When "Inspector Mode" is enabled, the zoomer is disabled and clicking on the canvas triggers a search of a nearby data point using pickDataPoint (the cursor changes to indicate the mode). :param enable: (bool or None) If True, it enables the Inspector Mode. If False, it disables it. If None passed, it toggles the mode. :return: (bool) whether the inspector mode has been enabled (True) or disabled (False) ''' if enable is None: enable = not(self._inspectorMode) self._pointPicker.setEnabled(enable) # enables/disables the picker self._zoomer.setEnabled(self._allowZoomers and not( enable)) # disables/enables the zoomers if enable: cursor = Qt.Qt.WhatsThisCursor else: cursor = Qt.Qt.CrossCursor self._pickedMarker.detach() # clears previous marker self.replot() self.canvas().setCursor(cursor) self._inspectorMode = enable return self._inspectorMode
[docs] def onCurveStatsAction(self): ''' slot for the curveStatsAction. Allows the user to select a range and then shows curve statistics on that range. ''' if getattr(self, '_curveStatsDialog', None) is None: from taurus.qt.qtgui.qwt5 import CurveStatsDialog self._curveStatsDialog = CurveStatsDialog(self) self._curveStatsDialog.closed.connect(self._onCurveStatsDialogClosed) self._curveStatsDialog.finished.connect(self._onCurveStatsDialogClosed) elif not self._curveStatsDialog.isVisible(): self._curveStatsDialog.refreshCurves() # it will be reenabed by _onCurveStatsDialogClosed self._curveStatsAction.setEnabled(False)
def _onCurveStatsDialogClosed(self, *args): '''slot called when the Curve Stats dialog is closed''' self._curveStatsAction.setEnabled(True)
[docs] def selectXRegion(self, axis=Qwt5.QwtPlot.xBottom, callback=None): '''Changes the input mode to allow the user to select a region of the X axis :param axis: (Qwt5.QwtPlot.xBottom or Qwt5.QwtPlot.xTop) on which the region will be defined (Default=Qwt5.QwtPlot.xBottom) :param callback: (method) a function that will be called when the user finishes selecting the region. If None passed (default) nothing is done ''' self.__xRegionCallback = callback self._xRegionPicker.setAxis(axis, Qwt5.QwtPlot.yLeft) self._beginXRegionMode()
def _onXRegionEvent(self, pos): '''slot called when the _xRegionPicker picks a point''' if self.__xRegionEnd is not None: self.debug('Ignoring xRegionEvent. Reason: not-reset region') return x = pos.x() if self.__xRegionStart is None: self.__xRegionStart = x self.__xRegionStartMarker.setXValue(x) self.__xRegionStartMarker.attach(self) self.replot() else: self.__xRegionEnd = x self.__xRegionEndMarker.setXValue(x) self.__xRegionEndMarker.attach(self) xmin, xmax = self.__xRegionStart, self.__xRegionEnd if xmin > xmax: xmin, xmax = xmax, xmin self.__xRegionCallback((xmin, xmax)) self.replot() self._endXRegionMode() def _beginXRegionMode(self): '''pre-region selection tasks''' self.__xRegionStart = None self.__xRegionEnd = None self.__xRegionStartMarker = Qwt5.QwtPlotMarker() self.__xRegionStartMarker.setLineStyle(Qwt5.QwtPlotMarker.VLine) self.__xRegionStartMarker.setLinePen(Qt.QPen(, 2)) self.__xRegionEndMarker = Qwt5.QwtPlotMarker() self.__xRegionEndMarker.setLineStyle(Qwt5.QwtPlotMarker.VLine) self.__xRegionEndMarker.setLinePen(Qt.QPen(, 2)) self._zoomer.setEnabled(False) # disables the zoomers self.canvas().setCursor(Qt.Qt.SplitHCursor) self._xRegionPicker.setEnabled(True) def _endXRegionMode(self): '''post-region selection tasks''' self._xRegionPicker.setEnabled(False) self.canvas().setCursor(Qt.Qt.CrossCursor) self._zoomer.setEnabled(self._allowZoomers) self.__xRegionStartMarker.detach() self.__xRegionEndMarker.detach() self.replot()
[docs] def getCurveStats(self, limits=None, curveNames=None): '''Shows a dialog containing descriptive statistics on curves :param limits: (None or tuple<float,float>) tuple containing (min,max) limits. Points of the curve whose abscisa value is outside of these limits are ignored. If None is passed, the limit is not enforced :param curveNames: (seq<str>) sequence of curve names for which statistics are requested. If None passed (default), all curves are considered :return: (dict) Returns a dictionary whose keys are the curve names and whose values are the dictionaries returned by :meth:`TaurusCurve.getStats` ''' if limits is not None and limits[0] is None and limits[1] is None: limits = None if curveNames is None: curveNames = self.getCurveNamesSorted() stats = {} self.curves_lock.acquire() try: for name in curveNames: curve = self.curves.get(name, None) stats[name] = curve.getStats(limits=limits) stats[name]['title'] = str(curve.title().text()) finally: self.curves_lock.release() return stats
#-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT property definition #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- #-~-~-~-~-~-~-~-~-~-~-~-~ # grid properties #-~-~-~-~-~-~-~-~-~-~-~-~ @Qt.pyqtSlot('QColor')
[docs] def setGridColor(self, color): '''Changes the color of the plot grid and refreshes the plot :param color:(Qt.QColor) the new color for the grid ''' self._gridPen.setColor(color) self._grid.setPen(self._gridPen) self.replot()
[docs] def getGridColor(self): '''Returns the color of the plot grid :return: (Qt.QColor) ''' return self._gridPen.color()
[docs] def resetGridColor(self): '''equivalent to self.setGridColor(Qt.Qt.gray)''' self.setGridColor(Qt.Qt.gray)
[docs] def setGridWidth(self, width): '''Changes the width of the plot grid lines and refreshes the plot :param width: (int) with in pixels for the grid lines ''' self._gridPen.setWidth(width) self._grid.setPen(self._gridPen) self.replot()
[docs] def getGridWidth(self): '''Returns the width of the grid lines :return: (int) with of the gridlines (in pixels) ''' return self._gridPen.width()
[docs] def resetGridWidth(self): '''equivalent to self.setGridWidth(1)''' self.setGridWidth(1)
#-~-~-~-~-~-~-~-~-~-~-~-~ # model properties #-~-~-~-~-~-~-~-~-~-~-~-~ def _splitModel(self, modelNames): '''convert str to list if needed (commas and whitespace are considered as separators)''' if isinstance(modelNames, string_types): modelNames = str(modelNames).replace(',', ' ') modelNames = modelNames.split() return modelNames def _lowerIfInsensitive(self, modelNames): """filter a model name list converting to lowercase the names belonging to case-insensitive schemes""" models = [] for m in modelNames: scheme = getSchemeFromName(m) # scan is not a scheme, but a "legacy" way in which Sardana plots # the scan data comming from the door; as Tango scheme it is # case insensitive if scheme == "scan" or not taurus.Factory(scheme).caseSensitive: models.append(str(m).lower()) else: models.append(str(m)) return models @Qt.pyqtSlot('QStringList')
[docs] def setModel(self, modelNames): '''sets the models of the Tango attributes that should be displayed in this TaurusPlot. :param modelNames: (sequence<str> or str) the names of the models to be plotted. For convenience, strings are also accepted (instead of a sequence of strings), in which case the string will be internally converted to a sequence by splitting it on whitespace. .. seealso:: :meth:`addModels`, :meth:`removeModels` ''' if modelNames is None: modelNames = [] modelNames = self._splitModel(modelNames) self._modelNames = self._lowerIfInsensitive(modelNames) self.updateCurves(self._modelNames) self.modelChanged.emit() # update the modelchooser list if self.DataImportDlg is not None: self.DataImportDlg.modelChooser.setListedModels(self._modelNames)
[docs] def getModel(self): '''returns the list of model names. :return: (CaselessList<str>) .. seealso:: :meth:`setModel` ''' return self._modelNames
[docs] def resetModel(self): '''equivalent to setModel([])''' self.setModel([])
[docs] def addModels(self, modelNames): '''Adds models to the existing ones: :param modelNames: (sequence<str>) the names of the models to be added to the plot. .. seealso:: :meth:`setModels`, :meth:`removeModels` ''' modelNames = self._lowerIfInsensitive(self._splitModel(modelNames)) modelNames = [str(m) for m in modelNames if m not in self._modelNames] self.setModel(self._modelNames + modelNames)
[docs] def removeModels(self, modelNames): '''Removes models from those already in the plot. :param modelNames: (sequence<str>) the names of the models to be added to the plot. .. seealso:: :meth:`setModels`, :meth:`addModels` ''' modelNames = self._lowerIfInsensitive(self._splitModel(modelNames)) for name in modelNames: try: self._modelNames.remove(name) except: self.warning("'%s' not in model list" % name) self.setModel(self._modelNames)
[docs] def setUseParentModel(self, yesno): '''Sets whether the TaurusCurves of this plot should use the plot's parent model :param yesno: (bool) if True, the curves in the plot will use the Plot's parent model .. seealso:: :meth:`TaurusBaseComponent.setParentModel` ''' if yesno: self.deprecated(dep='setUseParentModel(True)', rel="4.3.2", alt='explicit models including the parent model') if yesno and self._designMode: Qt.QMessageBox.information(self, "UseParentModel usage note", "Using the UseParentModel feature may require you to call " + "recheckTaurusParent() manually for this widget after calling " + "setupUi in your code." + "See the documentation of TaurusBaseWidget.recheckTaurusParent()") self._useParentModel = yesno parent_widget = self.getParentTaurusComponent() if parent_widget: if yesno: parent_widget.modelChanged.connect(self.parentModelChanged) else: parent_widget.modelChanged.disconnect(self.parentModelChanged) self.curves_lock.acquire() try: for curve in self.curves.values(): curve.setUseParentModel(yesno) finally: self.curves_lock.release()
[docs] def getUseParentModel(self): '''See: :meth:`TaurusBaseComponent.getParentModel`''' return self._useParentModel
[docs] def resetUseParentModel(self): '''equivalent to setUseParentModel(False)''' self.setUseParentModel(False)
#-~-~-~-~-~-~-~-~-~-~-~-~ # legend properties #-~-~-~-~-~-~-~-~-~-~-~-~ @Qt.pyqtSlot(int)
[docs] def setLegendPosition(self, pos): '''Specify the position of the legend relative to the plot :param pos: (Qwt5.QwtPlot.LegendPosition) .. seealso:: :meth:`Qwt5.QwtPlot.LegendPosition` ''' if pos == 0: self._legendPos = Qwt5.QwtPlot.LeftLegend elif pos == 1: self._legendPos = Qwt5.QwtPlot.RightLegend elif pos == 2: self._legendPos = Qwt5.QwtPlot.BottomLegend elif pos == 3: self._legendPos = Qwt5.QwtPlot.TopLegend elif pos == 4: self._legendPos = Qwt5.QwtPlot.ExternalLegend l = self.legend() if l: self.insertLegend(l, self._legendPos) self.replot()
[docs] def getLegendPosition(self): '''returns the current legend position :return: (Qwt5.QwtPlot.LegendPosition) ''' if self._legendPos == Qwt5.QwtPlot.LeftLegend: return 0 elif self._legendPos == Qwt5.QwtPlot.RightLegend: return 1 elif self._legendPos == Qwt5.QwtPlot.BottomLegend: return 2 elif self._legendPos == Qwt5.QwtPlot.TopLegend: return 3 elif self._legendPos == Qwt5.QwtPlot.ExternalLegend: return 4
[docs] def resetLegendPosition(self): '''equivalent to setLegendPosition(Qwt5.QwtPlot.RightLegend)''' self.setLegendPosition(Qwt5.QwtPlot.RightLegend)
[docs] def setDefaultCurvesTitle(self, titletext): '''sets the default title to be used for curves attached to this plot (the title is used, for example in the legend). Note that this does not affect to already existing curves. If you want that, see setCurvesTitle. :param titletext: (str) the default text to be used for the titles of curves. It may contain any of the placeholders described in TaurusCurve.setTitleText .. seealso:: :meth:`setCurvesTitle`, :meth:`TaurusCurve.setTitleText` ''' self._defaultCurvesTitle = titletext
[docs] def getDefaultCurvesTitle(self): '''See setDefaultCurvesTitle''' return self._defaultCurvesTitle
[docs] def resetDefaultCurvesTitle(self): '''resets the defaultCurvesTitle property to '<label>' .. seealso:: :meth:`setDefaultCurvesTitle`''' self.setDefaultCurvesTitle('<label>')
[docs] def setCurvesTitle(self, titletext, curveNamesList=None): '''Changes the titles of current curves. :param titletext: (str) string to use as title for the curves. It may include placeholders as those defined in TaurusCurve.compileTitleText() :param curveNamesList: (sequence<str> or iterator<str>) names of the curves to which the title will be changed (if None given , it will apply to all the curves except the raw data ones) :return: (caselessDict<str,str>) dictionary with key=curvename and value=newtitle .. seealso:: :meth:`changeCurvesTitlesDialog`, :meth:`setDefaultCurvesTitle`, :meth:`TaurusCurve.setTitleText` ''' self.curves_lock.acquire() try: if curveNamesList is None: curveNamesList = [ n for n, c in self.curves.items() if not c.isRawData] newTitlesDict = CaselessDict() for curveName in curveNamesList: curve = self.curves.get(curveName) curve.setTitleText(titletext) newTitlesDict[curveName] = str(curve.title().text()) self.updateLegend(self.legend()) return newTitlesDict finally: self.curves_lock.release()
[docs] def changeCurvesTitlesDialog(self, curveNamesList=None): '''Shows a dialog to set the curves titles (it will change the current curves titles and the default curves titles) :param curveNamesList: (sequence<str> or iterator<str>) names of the curves to which the title will be changed (if None given , it will apply to all the curves except the raw data ones and it will also be used as default for newly created ones) :return: (caselessDict<str,str>) dictionary with key=curvename and value=newtitle .. seealso:: :meth:`setCurvesTitle`, :meth:`setDefaultCurvesTitle` ''' newTitlesDict = None placeholders = ['<label>', '<model>', '<attr_name>', '<attr_fullname>', '<dev_alias>', '<dev_name>', '<dev_fullname>', '<current_title>'] try: current = placeholders.index(self._defaultCurvesTitle) items = placeholders except: current = len(placeholders) items = placeholders + [self._defaultCurvesTitle] msg = 'New text to be used for the curves.\nYou can use any of the following placeholders:\n%s' % ", ".join( placeholders) titletext, ok = Qt.QInputDialog.getItem( self, 'New Title for Curves', msg, items, current, True) if ok: titletext = str(titletext) if curveNamesList is None: self.setDefaultCurvesTitle(titletext) newTitlesDict = self.setCurvesTitle( titletext, curveNamesList=curveNamesList) return newTitlesDict
#-~-~-~-~-~-~-~-~-~-~-~-~ # Axes properties #-~-~-~-~-~-~-~-~-~-~-~-~
[docs] def setXIsTime(self, enable, axis=Qwt5.QwtPlot.xBottom): '''Specifies whether we the plot is in Time or in normal mode (i.e, whether the abscissas should be interpreted as unix epoch values or not) :param enable: (bool) if True, the plot will be in Time Mode :param axis: (Qwt5.QwtPlot.xBottom or Qwt5.QwtPlot.xTop) the X axis to which this setting applies. (Default=Qwt5.QwtPlot.xBottom) .. seealso:: :ref:`TaurusPlot user manual <taurusplottime>` ''' if enable: DateTimeScaleEngine.enableInAxis(self, axis) self._axesnames[Qwt5.QwtPlot.xBottom] = "T" else: DateTimeScaleEngine.disableInAxis(self, axis) self._axesnames[Qwt5.QwtPlot.xBottom] = "X" self._zoomer1.setXIsTime(enable) self._zoomer2.setXIsTime(enable) self._xIsTime = enable self.replot()
[docs] def getXIsTime(self): '''Returns whether the X axis is in "Time mode" :return: (bool) True means the X axis is in Time mode, False .. seealso:: :meth:`setXIsTime` ''' return self._xIsTime
[docs] def resetXIsTime(self): '''equivalent to setXIsTime(False)''' self.setXIsTime(False)
[docs] def setAllowZoomers(self, allow): '''enable/disable the zoomers for the plot. (The zoomers provide zooming by selecting a region of the plot) :param allow: (bool) If True, zoomers are enabled, otherwise, they are disabled ''' self._allowZoomers = allow self._zoomer.setEnabled(allow)
[docs] def getAllowZoomers(self): '''Whether the Zoomers are enabled for this plot :return: (bool) .. seealso:: :meth:`setAllowZoomers` ''' return self._allowZoomers
[docs] def resetAllowZoomers(self): '''same as setAllowZoomers(True)''' self.setAllowZoomers(True)
[docs] def setPannerEnabled(self, enable): '''Specify whether the plot can be panned (i.e., dragged around to navigate it) :param enable: (bool) If True, panning is enabled, otherwise, it is disabled ''' self._panner.setEnabled(enable)
[docs] def isPannerEnabled(self): '''Whether the Panner is enabled for this plot :return: (bool) .. seealso:: :meth:`setPannerEnabled` ''' return self._panner.isEnabled()
[docs] def resetPannerEnabled(self): '''same as setPannerEnabled(True)''' self.setPannerEnabled(True)
[docs] def setMagnifierEnabled(self, enable): '''Specify whether the plot can be magnified (i.e. zoomed in and out with the mousewheel) :param enable: (bool) If True, magnifying is enabled, otherwise, it is disabled ''' self._magnifier.setEnabled(enable)
[docs] def isMagnifierEnabled(self): '''Whether the magnifier is enabled for this plot :return: (bool) .. seealso:: :meth:`setMagnifierEnabled` ''' return self._magnifier.isEnabled()
[docs] def resetMagnifierEnabled(self): '''same as `setMagnifierEnabled(True)`''' self.setMagnifierEnabled(True)
[docs] def setOptimizationEnabled(self, enable): '''Specify whether the plot should use paint optimizations :param enable: (bool) If True, optimization is enabled, otherwise, it is disabled ''' # set the optimized flag for use with new curves self._optimizationEnabled = enable # make sure that already-created curves are also optimized try: for curveName in self.curves: curve = self.curves.get(str(curveName)) curve.setPaintAttribute(curve.PaintFiltered, enable) curve.setPaintAttribute(curve.ClipPolygons, enable) finally: self.curves_lock.release()
[docs] def isOptimizationEnabled(self): '''Whether painting optimization is enabled for this plot :return: (bool) ''' return self._optimizationEnabled
[docs] def resetOptimizationEnabled(self): '''Equivalent to `setOptimizationEnabled(True)` ''' self.setOptimizationEnabled(True)
[docs] def getQtDesignerPluginInfo(cls): """Returns pertinent information in order to be able to build a valid QtDesigner widget plugin :return: (dict) a map with pertinent designer information""" ret = TaurusBaseWidget.getQtDesignerPluginInfo() ret['module'] = 'taurus.qt.qtgui.qwt5' ret['group'] = 'Taurus Display' ret['icon'] = 'designer:qwtplot.png' return ret
##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# # Qt Properties # ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# gridColor = Qt.pyqtProperty( "QColor", getGridColor, setGridColor, resetGridColor) gridWidth = Qt.pyqtProperty( "int", getGridWidth, setGridWidth, resetGridWidth) legendPosition = Qt.pyqtProperty( "int", getLegendPosition, setLegendPosition, resetLegendPosition) model = Qt.pyqtProperty("QStringList", getModel, setModel, resetModel) useParentModel = Qt.pyqtProperty( "bool", getUseParentModel, setUseParentModel, resetUseParentModel) xIsTime = Qt.pyqtProperty("bool", getXIsTime, setXIsTime, resetXIsTime) allowZoomers = Qt.pyqtProperty( "bool", getAllowZoomers, setAllowZoomers, resetAllowZoomers) enablePanner = Qt.pyqtProperty( "bool", isPannerEnabled, setPannerEnabled, resetPannerEnabled) enableMagnifier = Qt.pyqtProperty( "bool", isMagnifierEnabled, setMagnifierEnabled, resetMagnifierEnabled) defaultCurvesTitle = Qt.pyqtProperty( "QString", getDefaultCurvesTitle, setDefaultCurvesTitle, resetDefaultCurvesTitle) enableOptimization = Qt.pyqtProperty( "bool", isOptimizationEnabled, setOptimizationEnabled, resetOptimizationEnabled)
def main(): import sys import taurus.qt.qtgui.application import taurus.core.util.argparse parser = taurus.core.util.argparse.get_taurus_parser() parser.set_usage("%prog [options] [<model1> [<model2>] ...]") parser.set_description("a taurus application for plotting 1D data sets") parser.add_option("-x", "--x-axis-mode", dest="x_axis_mode", default='e', metavar="t|n", help="interprete X values as either timestamps (t) or numbers (n). Accepted values: t|n (e is also accepted as a synonim of n)") parser.add_option("--config", "--config-file", dest="config_file", default=None, help="use the given config file for initialization") parser.add_option("--import-ascii", dest="import_ascii", default=None, help="import the given ascii file into the plot") parser.add_option("--export", "--export-file", dest="export_file", default=None, help="use the given file to as output instead of showing the plot") parser.add_option("--window-name", dest="window_name", default="TaurusPlot", help="Name of the window") app = taurus.qt.qtgui.application.TaurusApplication(cmd_line_parser=parser, app_name="taurusplot", app_version=taurus.Release.version) args = app.get_command_line_args() options = app.get_command_line_options() if options.x_axis_mode.lower() not in ['t', 'e', 'n']: parser.print_help(sys.stderr) sys.exit(1) models = args w = TaurusPlot() w.setWindowTitle(options.window_name) w.setXIsTime(options.x_axis_mode.lower() == 't') if options.config_file is not None: w.loadConfig(options.config_file) if options.import_ascii is not None: w.importAscii([options.import_ascii], xcol=0) if models: w.setModel(models) if options.export_file is not None: curves = dict.fromkeys(w.trendSets, 0) def exportIfAllCurves(curve, trend=w, counters=curves): curve = str(curve) print('*' * 10 + ' %s: Event received for %s ' % (, curve) + '*' * 10) if curve in counters: counters[curve] += 1 if all(counters.values()): trend.exportPdf(options.export_file) print('*' * 10 + ' %s: Exported to : %s ' % (, options.export_file) + '*' * 10) trend.close() return if not curves: w.close() else: for ts in w.trendSets.values(): ts.dataChanged.connect(exportIfAllCurves) sys.exit(app.exec_()) # exit without showing the widget # show the widget # if no models are passed, show the data import dialog if (len(models) == 0 and options.config_file is None and options.import_ascii is None ): w.showDataImportDlg() sys.exit(app.exec_()) if __name__ == "__main__": main()