class TaurusTrendSet(PlotDataItem, TaurusBaseComponent): """ A PlotDataItem for displaying trend curve(s) associated to a TaurusAttribute. The TaurusTrendSet itself does not contain any data, but acts as a manager that dynamically adds/removes curve(s) (other PlotDataItems) to its associated plot. If the attribute is a scalar, the Trend Set generates only one curve representing the evolution of the value of the attribute. If the attribute is an array, as many curves as the attribute size are created, each representing the evolution of the value of a component of the array. When an event is received, all curves belonging to a TaurusTrendSet are updated. TaurusTrendSet can be considered as a container of (sorted) curves. As such, the curves contained by it can be accessed by index:: ts = TaurusTrendSet('eval:rand(3)') # (...) wait for a Taurus Event arriving so that the curves are created ncurves = len(ts) # ncurves will be 3 (assuming the event arrived) curve0 = ts[0] # you can access the curve by index Note that internally each curve is a :class:`pyqtgraph.PlotDataItem` (i.e., it is not aware of events by itself, but it relies on the TaurusTrendSet object to update its values) """ def __init__(self, *args, **kwargs): _ = kwargs.pop("xModel", None) yModel = kwargs.pop("yModel", None) colors = kwargs.pop("colors", None) if colors is None: colors = LoopList(CURVE_COLORS) name = kwargs.pop("name", None) PlotDataItem.__init__(self, x=[], y=[], name=name) TaurusBaseComponent.__init__(self, "TaurusBaseComponent") self._UImodifiable = False self._maxBufferSize = 65536 # (=2**16, i.e., 64K events)) self._xBuffer = None self._yBuffer = None self._curveColors = colors self._args = args self._kwargs = kwargs self._curves = [] self._timer = Qt.QTimer() self._timer.timeout.connect(self._forceRead) self._legend = None # register config properties self.setModelInConfig(True) self.registerConfigProperty( self._getCurvesOpts, self._setCurvesOpts, "opts" ) # TODO: store forceReadPeriod config # TODO: store _maxBufferSize config if yModel is not None: self.setModel(yModel) def __repr__(self): return "<TrendSet {} ({} items)>".format(self.base_name(), len(self)) def name(self): """Reimplemented from PlotDataItem to avoid having the ts itself added to legends. .. seealso:: :meth:`basename` """ return None def base_name(self): """Returns the name of the trendset, which is used as a prefix for constructing the associated curves names .. seealso:: :meth:`name` """ return PlotDataItem.name(self) def __getitem__(self, k): return self._curves[k] def __len__(self): return len(self._curves) def __contains__(self, k): return k in self._curves def setModel(self, name): """Reimplemented from :meth:`TaurusBaseComponent.setModel`""" TaurusBaseComponent.setModel(self, name) # force a read to ensure that the curves are created self._forceRead() def _initBuffers(self, ntrends): """initializes new x and y buffers""" self._yBuffer = ArrayBuffer( numpy.zeros((min(128, self._maxBufferSize), ntrends), dtype="d"), maxSize=self._maxBufferSize, ) self._xBuffer = ArrayBuffer( (numpy.zeros(min(128, self._maxBufferSize), dtype="d")), maxSize=self._maxBufferSize, ) def _initCurves(self, ntrends): """ Initializes new curves """ # self._removeFromLegend(self._legend) # remove previously existing curves from views self._updateViewBox(None) self._curves = [] if self._curveColors is None: self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) a = self._args kw = self._kwargs.copy() base_name = ( self.base_name() or taurus.Attribute(self.getModel()).getSimpleName() ) for i in range(ntrends): subname = "%s[%i]" % (base_name, i) kw["name"] = subname curve = TrendCurve(*a, **kw) if "pen" not in kw: curve.setPen(next(self._curveColors)) self._curves.append(curve) self._updateViewBox(self.getViewBox()) def _addToLegend(self, legend): # ------------------------------------------------------------------ # In theory, TaurusTrendSet only uses viewBox.addItem to add its # sub-curves to the plot. In theory this should not add the curves # to the legend, and therefore we should do it here. # But somewhere the curves are already being added to the legend, and # if we re-add them here we get duplicated legend entries # TODO: Find where are the curves being added to the legend pass # if legend is None: # return # for c in self._curves: # legend.addItem(c, c.name()) # ------------------------------------------------------------------- def _removeFromLegend(self, legend): if legend is None: return for c in self._curves: legend.removeItem(c.name()) def _updateViewBox(self, viewBox): """Add/remove the "extra" curves from the viewbox if needed""" for curve in self._curves: curve_viewBox = curve.getViewBox() if curve_viewBox is not None: plotItem = None viewWidget = curve_viewBox.getViewWidget() if viewWidget is not None: plotItem = viewWidget.getPlotItem() curve_viewBox.removeItem(curve) if plotItem is not None: plotItem.removeItem(curve) if viewBox is not None: plotItem = None viewWidget = viewBox.getViewWidget() if viewWidget is not None: plotItem = viewWidget.getPlotItem() if plotItem is not None: curve = ensure_unique_curve_name(curve, plotItem) _cname = curve.name() params = {"all trends": _cname} plotItem.addItem(curve, params=params) def _updateBuffers(self, evt_value): """Update the x and y buffers with the new data. If the new data is not compatible with the existing buffers, the buffers are reset """ # TODO: we use .magnitude below to avoid issue #509 in pint # https://github.com/hgrecco/pint/issues/509 ntrends = numpy.size(evt_value.rvalue.magnitude) if not self._isDataCompatible(evt_value, ntrends): self._initBuffers(ntrends) self._yUnits = evt_value.rvalue.units self._initCurves(ntrends) try: self._yBuffer.append(evt_value.rvalue.to(self._yUnits).magnitude) except Exception as e: self.warning( "Problem updating buffer Y (%s):%s", evt_value.rvalue, e ) evt_value = None try: self._xBuffer.append(evt_value.time.totime()) except Exception as e: self.warning("Problem updating buffer X (%s):%s", evt_value, e) return self._xBuffer.contents(), self._yBuffer.contents() def _isDataCompatible(self, evt_value, ntrends): """ Check that the new evt_value is compatible with the current data in the buffers. Check shape and unit compatibility. """ if self._xBuffer is None or self._yBuffer is None: return False rvalue = evt_value.rvalue if rvalue.dimensionality != self._yUnits.dimensionality: return False current_trends = numpy.prod(self._yBuffer.contents().shape[1:]) if ntrends != current_trends: return False return True def _addData(self, x, y): for i, curve in enumerate(self._curves): curve.setData(x=x, y=y[:, i]) def clearBuffer(self): """Reset the buffered data""" self._initBuffers(len(self._curves)) def handleEvent(self, evt_src, evt_type, evt_value): """Reimplementation of :meth:`TaurusBaseComponent.handleEvent`""" # model = evt_src if evt_src is not None else self.getModelObj() # TODO: support boolean values from evt_value.rvalue if ( evt_value is None or not hasattr(evt_value, "rvalue") or evt_value.rvalue is None ): self.info("Invalid value. Ignoring.") return else: try: xValues, yValues = self._updateBuffers(evt_value) except Exception: # TODO: handle dropped events see: TaurusTrend._onDroppedEvent raise self._addData(xValues, yValues) def parentChanged(self): """Reimplementation of :meth:`PlotDataItem.parentChanged` to handle the change of the containing viewbox """ PlotDataItem.parentChanged(self) self._updateViewBox(self.getViewBox()) # update legend if needed try: legend = self.getViewWidget().getPlotItem().legend except Exception: legend = None if legend is not self._legend: self._removeFromLegend(self._legend) self._addToLegend(legend) self._legend = legend # Set period from ForcedReadTool (if found) try: for a in self.getViewBox().menu.actions(): if isinstance(a, ForcedReadTool) and a.autoconnect(): self.setForcedReadPeriod(a.period()) break except Exception as e: self.debug("cannot set period from ForcedReadTool: %r", e) @property def forcedReadPeriod(self): """Returns the forced reading period (in ms). A value <= 0 indicates that the forced reading is disabled """ return self._timer.interval() def setForcedReadPeriod(self, period): """ Forces periodic reading of the subscribed attribute in order to show new points even if no events are received. It will create fake events as needed with the read value. It will also block the plotting of regular events when period > 0. :param period: (int) period in milliseconds. Use period<=0 to stop the forced periodic reading """ # stop the timer and remove the __ONLY_OWN_EVENTS filter self._timer.stop() filters = self.getEventFilters() if self.__ONLY_OWN_EVENTS in filters: filters.remove(self.__ONLY_OWN_EVENTS) self.setEventFilters(filters) # if period is positive, set the filter and start if period > 0: self.insertEventFilter(self.__ONLY_OWN_EVENTS) self._timer.start(period) def _forceRead(self, cache=False): """Forces a read of the associated attribute. :param cache: (bool) If True, the reading will be done with cache=True but the timestamp of the resulting event will be replaced by the current time. If False, no cache will be used at all. """ value = self.getModelValueObj(cache=cache) if cache and value is not None: value = copy.copy(value) value.time = TaurusTimeVal.now() self.fireEvent(self, TaurusEventType.Periodic, value) def __ONLY_OWN_EVENTS(self, s, t, v): """An event filter that rejects all events except those that originate from this object """ if s is self: return s, t, v else: return None def _getCurvesOpts(self): """returns a list of serialized opts (one for each curve)""" from taurus.qt.qtgui.tpg import serialize_opts return [serialize_opts(copy.copy(c.opts)) for c in self._curves] def _setCurvesOpts(self, all_opts): """restore options to curves""" # If no curves are yet created, force a read to create them if not self._curves: self._forceRead(cache=True) # Check consistency in the number of curves if len(self._curves) != len(all_opts): self.warning( "Cannot apply curve options (mismatch in curves number)" ) return from taurus.qt.qtgui.tpg import deserialize_opts for c, opts in zip(self._curves, all_opts): c.opts = deserialize_opts(opts) # This is a workaround for the following pyqtgraph's bug: # https://github.com/pyqtgraph/pyqtgraph/issues/531 if opts["connect"] == "all": c.opts["connect"] = "all" elif opts["connect"] == "pairs": c.opts["connect"] = "pairs" elif opts["connect"] == "finite": c.opts["connect"] = "finite" def getFullModelNames(self): """ Return a tuple of (None, fullmodelname) for API compatibility with :class:`TaurusPlotDataItem`. """ return (None, self.getFullModelName()) def setBufferSize(self, buffer_size): """sets the maximum number of points to store per trend curve :param buffer_size: (int) max number of points to store per trend curve """ self._maxBufferSize = buffer_size try: if self._xBuffer is not None: # discard oldest data if needed for downsizing excess = len(self._xBuffer) - buffer_size if excess > 0: self._xBuffer.moveLeft(excess) self._xBuffer.resizeBuffer(buffer_size) # resize self._xBuffer.setMaxSize(buffer_size) if self._yBuffer is not None: # discard oldest data if needed for downsizing excess = len(self._yBuffer) - buffer_size if excess > 0: self._yBuffer.moveLeft(excess) self._yBuffer.resizeBuffer(buffer_size) # resize self._yBuffer.setMaxSize(buffer_size) except ValueError: self.info( "buffer downsizing requested." + "Current contents will be discarded" ) self.clearBuffer() def bufferSize(self): """returns the maximum number of points to be stored by the trends""" return self._maxBufferSize
class TaurusTrend(PlotWidget, TaurusBaseComponent): """ TaurusTrend is a general widget for plotting the evolution of a value over time. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusTrend incorporates the following tools/features: - Secondary Y axis (right axis) - Time X axis - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing taurus models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): TaurusBaseComponent.__init__(self, 'TaurusTrend') PlotWidget.__init__(self, parent=parent, **kwargs) # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) plot_item = self.getPlotItem() menu = plot_item.getViewBox().menu # add save & retrieve configuration actions saveConfigAction = QtGui.QAction('Save configuration', menu) saveConfigAction.triggered.connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction('Retrieve saved configuration', menu) loadConfigAction.triggered.connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, 'state') # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(plot_item) # add model chooser self._model_chooser_tool = TaurusModelChooserTool( self, itemClass=TaurusTrendSet) self._model_chooser_tool.attachToPlotItem(plot_item) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(plot_item) # Add time X axis axis = DateAxisItem(orientation='bottom') axis.attachToPlotItem(plot_item) # add plot configuration dialog cprop_tool = CurvesPropertiesTool(self) cprop_tool.attachToPlotItem(plot_item, y2=self._y2) # add force read tool fr_tool = ForcedReadTool(self) fr_tool.attachToPlotItem(self.getPlotItem()) # Register config properties self.registerConfigDelegate(self._y2, 'Y2Axis') self.registerConfigDelegate(legend_tool, 'legend') self.registerConfigDelegate(fr_tool, 'forceread') def setModel(self, names): """Set a list of models""" self._model_chooser_tool.updateModels(names or []) def createConfig(self, allowUnpickable=False): """ Reimplemented from BaseConfigurableClass to manage the config properties of the trendsets attached to this plot """ try: # Temporarily register trendsets as delegates tmpreg = [] data_items = self.getPlotItem().listDataItems() for idx, item in enumerate(data_items): if isinstance(item, TaurusTrendSet): name = '__TaurusTrendSet_%d__' % idx tmpreg.append(name) self.registerConfigDelegate(item, name) configdict = copy.deepcopy(TaurusBaseComponent.createConfig( self, allowUnpickable=allowUnpickable)) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) return configdict def applyConfig(self, configdict, depth=None): """ Reimplemented from BaseConfigurableClass to manage the config properties of the trendsets attached to this plot """ try: # Temporarily register trendsets as delegates tmpreg = [] tsets = [] for name in configdict['__orderedConfigNames__']: if name.startswith('__TaurusTrendSet_'): # Instantiate empty TaurusTrendSet tset = TaurusTrendSet() tsets.append(tset) self.registerConfigDelegate(tset, name) tmpreg.append(name) # remove the trendsets from the second axis (Y2) to avoid dups self._y2.clearItems() TaurusBaseComponent.applyConfig( self, configdict=configdict, depth=depth) plot_item = self.getPlotItem() legend = plot_item.legend # keep a dict of existing trendsets (to use it for avoiding dups) currentTrendSets = dict() curveNames = [] for tset in plot_item.listDataItems(): if isinstance(tset, TaurusTrendSet): currentTrendSets[tset.getFullModelName()] = tset curveNames.extend([c.name for c in tset]) # remove trendsets that exists in currentTrendSets from plot # (to avoid duplicates). Also remove curves from the legend for tset in tsets: ts = currentTrendSets.get(tset.getFullModelName(), None) if ts is not None: plot_item.removeItem(ts) # Add to plot **after** their configuration has been applied for tset in tsets: # First we add all the trendsets to self. This way the plotItem # can keep a list of dataItems (PlotItem.listDataItems()) self.addItem(tset) # Add trendsets to Y2 axis, when the trendset configurations # have been applied. # Ideally, the Y2ViewBox class must handle the action of adding # trendsets to itself, but we want add the trendsets when they # are restored with all their properties. if tset.getFullModelName() in self._y2.getCurves(): plot_item.getViewBox().removeItem(tset) self._y2.addItem(tset) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state['view']['viewRange'] return state
class TaurusPlot(PlotWidget, TaurusBaseComponent): """ TaurusPlot is a general widget for plotting 1D data sets. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusPGPlot incorporates the following tools/features: - Secondary Y axis (right axis) - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): TaurusBaseComponent.__init__(self, 'TaurusPlot') PlotWidget.__init__(self, parent=parent, **kwargs) # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) # add save & retrieve configuration actions menu = self.getPlotItem().getViewBox().menu saveConfigAction = QtGui.QAction('Save configuration', menu) saveConfigAction.triggered[()].connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction('Retrieve saved configuration', menu) loadConfigAction.triggered[()].connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, 'state') # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(self.getPlotItem()) # add model chooser model_chooser_tool = TaurusXYModelChooserTool(self) model_chooser_tool.attachToPlotItem(self.getPlotItem(), self, self._curveColors) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(self.getPlotItem()) # add plot configuration dialog cprop_tool = CurvesPropertiesTool(self) cprop_tool.attachToPlotItem(self.getPlotItem(), y2=self._y2) # Register config properties self.registerConfigDelegate(self._y2, 'Y2Axis') self.registerConfigDelegate(legend_tool, 'legend') def setModel(self, models): """Set a list of models""" # TODO: remove previous models! # TODO: support setting xmodels as well # TODO: Consider supporting a space-separated string as a model for model in models: curve = TaurusPlotDataItem(name=model) curve.setModel(model) curve.setPen(self._curveColors.next().color()) self.addItem(curve) def createConfig(self, allowUnpickable=False): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curve_list = self.getPlotItem().listDataItems() for idx, curve in enumerate(curve_list): if isinstance(curve, TaurusPlotDataItem): name = '__TaurusPlotDataItem_%d__' % idx tmpreg.append(name) self.registerConfigDelegate(curve, name) configdict = copy.deepcopy( TaurusBaseComponent.createConfig( self, allowUnpickable=allowUnpickable)) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) return configdict def applyConfig(self, configdict, depth=None): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curves = [] for name in configdict['__orderedConfigNames__']: if name.startswith('__TaurusPlotDataItem_'): # Instantiate empty TaurusPlotDataItem curve = TaurusPlotDataItem() curves.append(curve) self.registerConfigDelegate(curve, name) tmpreg.append(name) # remove the curves from the second axis (Y2) for avoid dups self._y2.clearItems() TaurusBaseComponent.applyConfig(self, configdict=configdict, depth=depth) # keep a dict of existing curves (to use it for avoiding dups) currentCurves = dict() for curve in self.getPlotItem().listDataItems(): if isinstance(curve, TaurusPlotDataItem): currentCurves[curve.getFullModelNames()] = curve # remove curves that exists in currentCurves, also remove from # the legend (avoid duplicates) for curve in curves: c = currentCurves.get(curve.getFullModelNames(), None) if c is not None: self.getPlotItem().legend.removeItem(c.name()) self.getPlotItem().removeItem(c) # Add to plot **after** their configuration has been applied for curve in curves: # First we add all the curves in self. This way the plotItem # can keeps a list of dataItems (plotItem.listDataItems()) self.addItem(curve) # Add curves to Y2 axis, when the curve configurations # have been applied. # Ideally, the Y2ViewBox class must handle the action of adding # curves to itself, but we want add the curves when they are # restored with all their properties. if curve.getFullModelNames() in self._y2.getCurves(): self.getPlotItem().getViewBox().removeItem(curve) self._y2.addItem(curve) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state['view']['viewRange'] return state
class TaurusTrend(PlotWidget, BaseConfigurableClass): """ TaurusTrend is a general widget for plotting the evolution of a value over time. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusTrend incorporates the following tools/features: - Secondary Y axis (right axis) - Time X axis - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing taurus models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): if Qt.QT_VERSION < 0x050000: # Workaround for issue when using super with pyqt<5 BaseConfigurableClass.__init__(self) PlotWidget.__init__(self, parent=parent, **kwargs) else: super(TaurusTrend, self).__init__(parent=parent, **kwargs) # Compose with a Logger self._logger = Logger(name=self.__class__.__name__) self.debug = self._logger.debug self.info = self._logger.info self.warning = self._logger.warning self.error = self._logger.error # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) plot_item = self.getPlotItem() menu = plot_item.getViewBox().menu # add save & retrieve configuration actions saveConfigAction = QtGui.QAction("Save configuration", menu) saveConfigAction.triggered.connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction("Retrieve saved configuration", menu) loadConfigAction.triggered.connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, "state") # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(plot_item) # add model chooser self._model_chooser_tool = TaurusXYModelChooserTool( self, itemClass=TaurusTrendSet, showX=False) self._model_chooser_tool.attachToPlotItem(self.getPlotItem(), self, self._curveColors) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(plot_item) # Add time X axis axis = DateAxisItem(orientation="bottom") axis.attachToPlotItem(plot_item) # add plot configuration dialog self._cprop_tool = CurvesPropertiesTool(self) self._cprop_tool.attachToPlotItem(plot_item, y2=self._y2) # add data inspector widget inspector_tool = DataInspectorTool(self) inspector_tool.attachToPlotItem(self.getPlotItem()) # add force read tool self._fr_tool = ForcedReadTool(self) self._fr_tool.attachToPlotItem(self.getPlotItem()) # Add the auto-pan ("oscilloscope mode") tool self._autopan = XAutoPanTool() self._autopan.attachToPlotItem(self.getPlotItem()) # Register config properties self.registerConfigDelegate(self._model_chooser_tool, "XYmodelchooser") # self.registerConfigDelegate(self._y2, "Y2Axis") self.registerConfigDelegate(self._cprop_tool, "CurvePropertiesTool") self.registerConfigDelegate(legend_tool, "legend") self.registerConfigDelegate(self._fr_tool, "forceread") self.registerConfigDelegate(inspector_tool, "inspector") # -------------------------------------------------------------------- # workaround for bug in pyqtgraph v<=0.10.0, already fixed in # https://github.com/pyqtgraph/pyqtgraph/commit/52754d4859 # TODO: remove this once pyqtgraph v>0.10 is released def __getattr__(self, item): try: return PlotWidget.__getattr__(self, item) except NameError: raise AttributeError("{} has no attribute {}".format( self.__class__.__name__, item)) # -------------------------------------------------------------------- def __getitem__(self, idx): """ Provides a list-like interface: items can be accessed using slice notation """ return self._getCurves()[idx] def __len__(self): return len(self._getCurves()) def _getCurves(self): """returns a flat list with all items from all trend sets""" ret = [] for ts in self.getTrendSets(): ret += ts[:] return ret def getTrendSets(self): return [ e for e in self.getPlotItem().listDataItems() if isinstance(e, TaurusTrendSet) ] def setModel(self, names): """Set a list of models""" # support passing a string in names instead of a sequence if isinstance(names, string_types): names = [names] self._model_chooser_tool.updateModels(names or []) def addModels(self, names): """Reimplemented to delegate to the model chooser""" # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.addModels(names) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state["view"]["viewRange"] return state def setXAxisMode(self, x_axis_mode): """Required generic TaurusTrend API """ if x_axis_mode != "t": raise NotImplementedError( # TODO 'X mode "{}" not yet supported'.format(x_axis_mode)) def setForcedReadingPeriod(self, period): """Required generic TaurusTrend API """ self._fr_tool.setPeriod(period) def setMaxDataBufferSize(self, buffer_size): """Required generic TaurusTrend API """ raise NotImplementedError( # TODO "Setting the max buffer size is not yet supported by tpg trend")
class TaurusPlot(PlotWidget, BaseConfigurableClass): """ TaurusPlot is a general widget for plotting 1D data sets. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusPlot incorporates the following tools/features: - Secondary Y axis (right axis) - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): if Qt.QT_VERSION < 0x050000: # Workaround for issue when using super with pyqt<5 BaseConfigurableClass.__init__(self) PlotWidget.__init__(self, parent=parent, **kwargs) else: super(TaurusPlot, self).__init__(parent=None, **kwargs) # Compose with a Logger self._logger = Logger(name=self.__class__.__name__) self.debug = self._logger.debug self.info = self._logger.info self.warning = self._logger.warning self.error = self._logger.error # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) # add save & retrieve configuration actions menu = self.getPlotItem().getViewBox().menu saveConfigAction = QtGui.QAction("Save configuration", menu) saveConfigAction.triggered.connect(self._onSaveConfigAction) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction("Retrieve saved configuration", menu) loadConfigAction.triggered.connect(self._onRetrieveConfigAction) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, "state") # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(self.getPlotItem()) # add model chooser self._model_chooser_tool = TaurusXYModelChooserTool(self) self._model_chooser_tool.attachToPlotItem(self.getPlotItem(), self, self._curveColors) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(self.getPlotItem()) # add plot configuration dialog self._cprop_tool = CurvesPropertiesTool(self) self._cprop_tool.attachToPlotItem(self.getPlotItem(), y2=self._y2) # add a data inspector inspector_tool = DataInspectorTool(self) inspector_tool.attachToPlotItem(self.getPlotItem()) # enable Autorange self.getPlotItem().getViewBox().enableAutoRange(True) self._y2.enableAutoRange(True) # Register config properties self.registerConfigDelegate(self._model_chooser_tool, "XYmodelchooser") self.registerConfigDelegate(self._y2, "Y2Axis") self.registerConfigDelegate(self._cprop_tool, "CurvePropertiesTool") self.registerConfigDelegate(legend_tool, "legend") self.registerConfigDelegate(inspector_tool, "inspector") # -------------------------------------------------------------------- # workaround for bug in pyqtgraph v<=0.10.0, already fixed in # https://github.com/pyqtgraph/pyqtgraph/commit/52754d4859 # TODO: remove this once pyqtgraph v>0.10 is released def __getattr__(self, item): try: return PlotWidget.__getattr__(self, item) except NameError: raise AttributeError("{} has no attribute {}".format( self.__class__.__name__, item)) # -------------------------------------------------------------------- def __getitem__(self, idx): """ Provides a list-like interface: items can be accessed using slice notation """ return self.getPlotItem().listDataItems()[idx] def __len__(self): return len(self.getPlotItem().listDataItems()) def setModel(self, names): """Reimplemented to delegate to the model chooser""" # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.updateModels(names) def addModels(self, names): """Reimplemented to delegate to the model chooser""" # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.addModels(names) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state["view"]["viewRange"] return state def setXAxisMode(self, x_axis_mode): """Required generic TaurusPlot API """ from taurus_pyqtgraph import DateAxisItem if x_axis_mode == "t": axis = DateAxisItem(orientation="bottom") axis.attachToPlotItem(self.getPlotItem()) elif x_axis_mode == "n": axis = self.getPlotItem().axes["bottom"]["item"] if isinstance(axis, DateAxisItem): axis.detachFromPlotItem() else: raise ValueError("Unsupported x axis mode {}".format(x_axis_mode)) def _onSaveConfigAction(self): """wrapper to avoid issues with overloaded signals""" return self.saveConfigFile() def _onRetrieveConfigAction(self): """wrapper to avoid issues with overloaded signals""" return self.loadConfigFile()
class TaurusPlot(PlotWidget, BaseConfigurableClass): """ TaurusPlot is a general widget for plotting 1D data sets. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusPlot incorporates the following tools/features: - Secondary Y axis (right axis) - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): if Qt.QT_VERSION < 0x050000: # Workaround for issue when using super with pyqt<5 BaseConfigurableClass.__init__(self) PlotWidget.__init__(self, parent=parent, **kwargs) else: super(TaurusPlot, self).__init__(parent=None, **kwargs) # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) # add save & retrieve configuration actions menu = self.getPlotItem().getViewBox().menu saveConfigAction = QtGui.QAction("Save configuration", menu) saveConfigAction.triggered.connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction("Retrieve saved configuration", menu) loadConfigAction.triggered.connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, "state") # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(self.getPlotItem()) # add model chooser self._model_chooser_tool = TaurusXYModelChooserTool(self) self._model_chooser_tool.attachToPlotItem(self.getPlotItem(), self, self._curveColors) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(self.getPlotItem()) # add plot configuration dialog cprop_tool = CurvesPropertiesTool(self) cprop_tool.attachToPlotItem(self.getPlotItem(), y2=self._y2) # add a data inspector inspector_tool = DataInspectorTool(self) inspector_tool.attachToPlotItem(self.getPlotItem()) # Register config properties self.registerConfigDelegate(self._y2, "Y2Axis") self.registerConfigDelegate(legend_tool, "legend") self.registerConfigDelegate(inspector_tool, "inspector") # -------------------------------------------------------------------- # workaround for bug in pyqtgraph v<=0.10.0, already fixed in # https://github.com/pyqtgraph/pyqtgraph/commit/52754d4859 # TODO: remove this once pyqtgraph v>0.10 is released def __getattr__(self, item): try: return PlotWidget.__getattr__(self, item) except NameError: raise AttributeError("{} has no attribute {}".format( self.__class__.__name__, item)) # -------------------------------------------------------------------- def setModel(self, names): """Reimplemented to delegate to the """ # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.updateModels(names) def addModels(self, names): """Reimplemented to delegate to the """ # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.addModels(names) def createConfig(self, allowUnpickable=False): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curve_list = self.getPlotItem().listDataItems() for idx, curve in enumerate(curve_list): if isinstance(curve, TaurusPlotDataItem): name = "__TaurusPlotDataItem_%d__" % idx tmpreg.append(name) self.registerConfigDelegate(curve, name) configdict = copy.deepcopy( BaseConfigurableClass.createConfig( self, allowUnpickable=allowUnpickable)) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) return configdict def applyConfig(self, configdict, depth=None): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curves = [] for name in configdict["__orderedConfigNames__"]: if name.startswith("__TaurusPlotDataItem_"): # Instantiate empty TaurusPlotDataItem curve = TaurusPlotDataItem() curves.append(curve) self.registerConfigDelegate(curve, name) tmpreg.append(name) # remove the curves from the second axis (Y2) for avoid dups self._y2.clearItems() BaseConfigurableClass.applyConfig(self, configdict=configdict, depth=depth) # keep a dict of existing curves (to use it for avoiding dups) currentCurves = dict() for curve in self.getPlotItem().listDataItems(): if isinstance(curve, TaurusPlotDataItem): currentCurves[curve.getFullModelNames()] = curve # remove curves that exists in currentCurves, also remove from # the legend (avoid duplicates) for curve in curves: c = currentCurves.get(curve.getFullModelNames(), None) if c is not None: self.getPlotItem().legend.removeItem(c.name()) self.getPlotItem().removeItem(c) # Add to plot **after** their configuration has been applied for curve in curves: # First we add all the curves in self. This way the plotItem # can keeps a list of dataItems (plotItem.listDataItems()) self.addItem(curve) # Add curves to Y2 axis, when the curve configurations # have been applied. # Ideally, the Y2ViewBox class must handle the action of adding # curves to itself, but we want add the curves when they are # restored with all their properties. if curve.getFullModelNames() in self._y2.getCurves(): self.getPlotItem().getViewBox().removeItem(curve) self._y2.addItem(curve) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state["view"]["viewRange"] return state