예제 #1
0
 def prepare(self, plots, nb_points=None):
     self.win.clear()
     plot_widgets = {}
     nb_points = 2**16 if nb_points is None else nb_points
     plots_per_row = int(len(plots)**0.5)
     for idx, plot in enumerate(plots):
         plot_curves = {}
         x_axis, curves = plot['x_axis'], plot['curves']
         if idx % plots_per_row == 0:
             self.win.nextRow()
         nb_curves = len(curves)
         labels = dict(bottom=x_axis['label'])
         if nb_curves == 1:
             labels['left'] = curves[0]['label']
         plot_widget = self.win.addPlot(labels=labels)
         plot_widget.showGrid(x=True, y=True)
         if nb_curves > 1:
             plot_widget.addLegend()
         plot_widget.x_axis = dict(x_axis, data=empty_data(nb_points))
         styles = LoopList(CURVE_STYLES)
         for curve in curves:
             pen, symbol = styles.current()
             styles.next()
             style = dict(pen=pen,
                          symbol=symbol,
                          symbolSize=5,
                          symbolPen=pen,
                          symbolBrush=pen)
             curve_item = plot_widget.plot(name=curve['label'], **style)
             curve_item.curve_data = empty_data(nb_points)
             plot_curves[curve['name']] = curve_item
         plot_widgets[plot_widget] = plot_curves
     self._plots = plot_widgets
     self._event_nb = 0
     self._last_event_nb = 0
     self._start_update()
예제 #2
0
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
예제 #3
0
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 used 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):
        PlotDataItem.__init__(self, *args, **kwargs)
        TaurusBaseComponent.__init__(self, 'TaurusBaseComponent')
        self._UImodifiable = False
        self._maxBufferSize = 65536  # (=2**16, i.e., 64K events))
        self._xBuffer = None
        self._yBuffer = None
        self._curveColors = LoopList(CURVE_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

    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)
        self._curves = []
        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 xrange(ntrends):
            subname = "%s[%i]" % (base_name, i)
            kw['name'] = subname
            curve = PlotDataItem(*a, **kw)
            if 'pen' not in kw:
                curve.setPen(self._curveColors.next().color())
            self._curves.append(curve)
        self._updateViewBox()

    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):
        """Add/remove the "extra" curves from the viewbox if needed"""
        if self._curves:
            viewBox = self.getViewBox()
            self.forgetViewBox()
        for curve in self._curves:
            curve.forgetViewBox()
            curve_viewBox = curve.getViewBox()

            if curve_viewBox is not None:
                curve_viewBox.removeItem(curve)
            if viewBox is not None:
                viewBox.addItem(curve)

    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 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 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()

        # 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=True):
        """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:
            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'