示例#1
0
class SpectraPlotWidget(PlotWidget):
    sigEnergyChanged = Signal(object)

    def __init__(self, *args, **kwargs):
        super(SpectraPlotWidget, self).__init__(*args, **kwargs)
        self._data = None
        self.positionmap = dict()
        self.wavenumbers = None
        self._meanSpec = True # whether current spectrum is a mean spectrum
        self.line = InfiniteLine(movable=True)
        self.line.setPen((255, 255, 0, 200))
        self.line.setZValue(100)
        self.line.sigPositionChanged.connect(self.sigEnergyChanged)
        self.line.sigPositionChanged.connect(self.getEnergy)
        self.addItem(self.line)
        self.getViewBox().invertX(True)
        self.selectedPixels = None
        self._y = None

    def getEnergy(self, lineobject):
        if self._y is not None:
            x_val = lineobject.value()
            idx = val2ind(x_val, self.wavenumbers)
            x_val = self.wavenumbers[idx]
            y_val = self._y[idx]
            if not self._meanSpec:
                txt_html = f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                                Spectrum #{self.spectrumInd}</div>'
            else:
                txt_html = f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                                 {self._mean_title}</div>'

            txt_html += f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                                 X = {x_val: .2f}, Y = {y_val: .4f}</div>'
            self.txt.setHtml(txt_html)

    def setHeader(self, header: NonDBHeader, field: str, *args, **kwargs):
        self.header = header
        self.field = field
        # get wavenumbers
        spectraEvent = next(header.events(fields=['spectra']))
        self.wavenumbers = spectraEvent['wavenumbers']
        self.N_w = len(self.wavenumbers)
        self.rc2ind = spectraEvent['rc_index']
        # make lazy array from document
        data = None
        try:
            data = header.meta_array(field)
        except IndexError:
            msg.logMessage('Header object contained no frames with field ''{field}''.', msg.ERROR)

        if data is not None:
            # kwargs['transform'] = QTransform(1, 0, 0, -1, 0, data.shape[-2])
            self._data = data

    def showSpectra(self, i=0):
        if self._data is not None:
            self.clear()
            self._meanSpec = False
            self.spectrumInd = i
            self.plot(self.wavenumbers, self._data[i])

    def getSelectedPixels(self, selectedPixels):
        self.selectedPixels = selectedPixels
        # print(selectedPixels)

    def showMeanSpectra(self):
        self._meanSpec = True
        self.clear()
        if self.selectedPixels is not None:
            n_spectra = len(self.selectedPixels)
            tmp = np.zeros((n_spectra, self.N_w))
            for j in range(n_spectra):  # j: jth selected pixel
                row_col = tuple(self.selectedPixels[j])
                tmp[j, :] = self._data[self.rc2ind[row_col]]
            self._mean_title = f'ROI mean of {n_spectra} spectra'
        else:
            n_spectra = len(self._data)
            tmp = np.zeros((n_spectra, self.N_w))
            for j in range(n_spectra):
                tmp[j, :] = self._data[j]
            self._mean_title = f'Total mean of {n_spectra} spectra'
        if n_spectra > 0:
            meanSpec = np.mean(tmp, axis=0)
        else:
            meanSpec = np.zeros_like(self.wavenumbers) + 1e-3
        self.plot(self.wavenumbers, meanSpec)

    def plot(self, x, y, *args, **kwargs):
        # set up infinity line and get its position
        self.plotItem.plot(x, y, *args, **kwargs)
        self.addItem(self.line)
        x_val = self.line.value()
        if x_val == 0:
            y_val = 0
        else:
            idx = val2ind(x_val, self.wavenumbers)
            x_val = self.wavenumbers[idx]
            y_val = y[idx]

        if not self._meanSpec:
            txt_html = f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                            Spectrum #{self.spectrumInd}</div>'
        else:
            txt_html = f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                             {self._mean_title}</div>'

        txt_html += f'<div style="text-align: center"><span style="color: #FFF; font-size: 12pt">\
                             X = {x_val: .2f}, Y = {y_val: .4f}</div>'
        self.txt = TextItem(html=txt_html, anchor=(0, 0))
        ymax = max(y)
        self._y = y
        self.txt.setPos(1500, 0.95 * ymax)
        self.addItem(self.txt)
示例#2
0
class ScanPlotWidget(PlotWidget):
    """
    Extend the PlotWidget Class with more functionality used for qudi scan images.
    Supported features:
     - draggable/static crosshair with optional range and size constraints.
     - zoom feature by rubberband selection
     - rubberband area selection

    This class depends on the ScanViewBox class defined further below.
    This class can be promoted in the Qt designer.
    """
    sigMouseAreaSelected = QtCore.Signal(
        QtCore.QRectF)  # mapped rectangle mouse cursor selection
    sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF)
    sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF)

    def __init__(self, *args, **kwargs):
        kwargs['viewBox'] = ScanViewBox()  # Use custom pg.ViewBox subclass
        super().__init__(*args, **kwargs)
        self.getViewBox().sigMouseAreaSelected.connect(
            self.sigMouseAreaSelected)

        self._min_crosshair_factor = 0.02
        self._crosshair_size = (0, 0)
        self._crosshair_range = None
        self.getViewBox().sigRangeChanged.connect(
            self._constraint_crosshair_size)

        self.crosshair = ROI((0, 0), (0, 0),
                             pen={
                                 'color': '#00ff00',
                                 'width': 1
                             })
        self.hline = InfiniteLine(pos=0,
                                  angle=0,
                                  movable=True,
                                  pen={
                                      'color': '#00ff00',
                                      'width': 1
                                  },
                                  hoverPen={
                                      'color': '#ffff00',
                                      'width': 1
                                  })
        self.vline = InfiniteLine(pos=0,
                                  angle=90,
                                  movable=True,
                                  pen={
                                      'color': '#00ff00',
                                      'width': 1
                                  },
                                  hoverPen={
                                      'color': '#ffff00',
                                      'width': 1
                                  })
        self.vline.sigDragged.connect(self._update_pos_from_line)
        self.hline.sigDragged.connect(self._update_pos_from_line)
        self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi)
        self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged)

    @property
    def crosshair_enabled(self):
        items = self.items()
        return (self.vline in items) and (self.hline
                                          in items) and (self.crosshair
                                                         in items)

    @property
    def crosshair_movable(self):
        return bool(self.crosshair.translatable)

    @property
    def crosshair_position(self):
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        return tuple(pos)

    @property
    def crosshair_size(self):
        return tuple(self._crosshair_size)

    @property
    def crosshair_min_size_factor(self):
        return float(self._min_crosshair_factor)

    @property
    def crosshair_range(self):
        if self._crosshair_range is None:
            return None
        return tuple(self._crosshair_range)

    @property
    def selection_enabled(self):
        return bool(self.getViewBox().rectangle_selection)

    @property
    def zoom_by_selection_enabled(self):
        return bool(self.getViewBox().zoom_by_selection)

    def toggle_selection(self, enable):
        """
        De-/Activate the rectangular rubber band selection tool.
        If active you can select a rectangular region within the ViewBox by dragging the mouse
        with the left button. Each selection rectangle in real-world data coordinates will be
        emitted by sigMouseAreaSelected.
        By using activate_zoom_by_selection you can optionally de-/activate zooming in on the
        selection.

        @param bool enable: Toggle selection on (True) or off (False)
        """
        return self.getViewBox().toggle_selection(enable)

    def toggle_zoom_by_selection(self, enable):
        """
        De-/Activate automatic zooming into a selection.
        See also: toggle_selection

        @param bool enable: Toggle zoom upon selection on (True) or off (False)
        """
        return self.getViewBox().toggle_zoom_by_selection(enable)

    def _update_pos_from_line(self, obj):
        """
        Called each time the position of the InfiniteLines has been changed by a user drag.
        Causes the crosshair rectangle to follow the lines.
        """
        if obj not in (self.hline, self.vline):
            return
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        size = self.crosshair.size()
        self.crosshair.blockSignals(True)
        self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2))
        self.crosshair.blockSignals(False)
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def _update_pos_from_roi(self, obj):
        """
        Called each time the position of the rectangular ROI has been changed by a user drag.
        Causes the InfiniteLines to follow the ROI.
        """
        if obj is not self.crosshair:
            return
        pos = self.crosshair.pos()
        size = self.crosshair.size()
        pos[0] += size[0] / 2
        pos[1] += size[1] / 2
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def toggle_crosshair(self, enable, movable=True):
        """
        Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be
        dragged by the user.

        @param bool enable: enable crosshair (True), disable crosshair (False)
        @param bool movable: enable user drag (True), disable user drag (False)
        """
        if not isinstance(enable, bool):
            raise TypeError('Positional argument "enable" must be bool type.')
        if not isinstance(movable, bool):
            raise TypeError('Optional argument "movable" must be bool type.')

        self.toggle_crosshair_movable(movable)

        is_enabled = self.crosshair_enabled
        if enable and not is_enabled:
            self.addItem(self.vline)
            self.addItem(self.hline)
            self.addItem(self.crosshair)
        elif not enable and is_enabled:
            self.removeItem(self.vline)
            self.removeItem(self.hline)
            self.removeItem(self.crosshair)
        return

    def toggle_crosshair_movable(self, enable):
        """
        Toggle if the crosshair can be dragged by the user.

        @param bool enable: enable (True), disable (False)
        """
        self.crosshair.translatable = bool(enable)
        self.vline.setMovable(enable)
        self.hline.setMovable(enable)
        return

    def set_crosshair_pos(self, pos):
        """
        Set the crosshair center to the given coordinates.

        @param QPointF|float[2] pos: (x,y) position of the crosshair
        """
        try:
            pos = tuple(pos)
        except TypeError:
            pos = (pos.x(), pos.y())
        size = self.crosshair.size()

        self.crosshair.blockSignals(True)
        self.vline.blockSignals(True)
        self.hline.blockSignals(True)
        self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.crosshair.blockSignals(False)
        self.vline.blockSignals(False)
        self.hline.blockSignals(False)
        self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos))
        return

    def set_crosshair_size(self, size, force_default=True):
        """
        Set the default size of the crosshair rectangle (x, y) and update the display.

        @param QSize|float[2] size: the (x,y) size of the crosshair rectangle
        @param bool force_default: Set default crosshair size and enforce minimal size (True).
                                   Enforce displayed crosshair size while keeping default size
                                   untouched (False).
        """
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        if force_default:
            if size[0] <= 0 and size[1] <= 0:
                self._crosshair_size = (0, 0)
            else:
                self._crosshair_size = size
                # Check if actually displayed size needs to be adjusted due to minimal size
                size = self._get_corrected_crosshair_size(size)

        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1] - size[1] / 2
        pos[0] -= size[0] / 2

        if self._crosshair_range:
            crange = self._crosshair_range
            self.crosshair.maxBounds = QtCore.QRectF(
                crange[0][0] - size[0] / 2, crange[1][0] - size[1] / 2,
                crange[0][1] - crange[0][0] + size[0],
                crange[1][1] - crange[1][0] + size[1])
        self.crosshair.blockSignals(True)
        self.crosshair.setSize(size)
        self.crosshair.setPos(pos)
        self.crosshair.blockSignals(False)
        return

    def set_crosshair_min_size_factor(self, factor):
        """
        Sets the minimum crosshair size factor. This will determine the minimum size of the
        smallest edge of the crosshair center rectangle.
        This minimum size is calculated by taking the smallest visible axis of the ViewBox and
        multiplying it with the scale factor set by this method.
        The crosshair rectangle will be then scaled accordingly if the set crosshair size is
        smaller than this minimal size.

        @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced.
        """
        if factor <= 0:
            self._min_crosshair_factor = 0
        elif factor <= 1:
            self._min_crosshair_factor = float(factor)
        else:
            raise ValueError('Crosshair min size factor must be a value <= 1.')
        return

    def set_crosshair_range(self, new_range):
        """
        Sets a range boundary for the crosshair position.

        @param float[2][2] new_range: two min-max range value tuples (for x and y axis).
                                      If None set unlimited ranges.
        """
        if new_range is None:
            self.vline.setBounds([None, None])
            self.hline.setBounds([None, None])
            self.crosshair.maxBounds = None
        else:
            self.vline.setBounds(new_range[0])
            self.hline.setBounds(new_range[1])
            size = self.crosshair.size()
            pos = self.crosshair_position
            self.crosshair.maxBounds = QtCore.QRectF(
                new_range[0][0] - size[0] / 2, new_range[1][0] - size[1] / 2,
                new_range[0][1] - new_range[0][0] + size[0],
                new_range[1][1] - new_range[1][0] + size[1])
            self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self._crosshair_range = new_range
        return

    def set_crosshair_pen(self, pen):
        """
        Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines.

        @param pen: pyqtgraph compatible pen to use
        """
        self.crosshair.setPen(pen)
        self.vline.setPen(pen)
        self.hline.setPen(pen)
        return

    def _constraint_crosshair_size(self):
        if self._min_crosshair_factor == 0:
            return
        if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0:
            return
        corr_size = self._get_corrected_crosshair_size(self._crosshair_size)
        if corr_size != tuple(self.crosshair.size()):
            self.set_crosshair_size(corr_size, force_default=False)
        return

    def _get_corrected_crosshair_size(self, size):
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        min_size = min(size)
        if min_size == 0:
            return size
        vb_size = self.getViewBox().viewRect().size()
        short_index = int(vb_size.width() > vb_size.height())
        min_vb_size = vb_size.width() if short_index == 0 else vb_size.height()
        min_vb_size *= self._min_crosshair_factor

        if min_size < min_vb_size:
            scale_factor = min_vb_size / min_size
            size = (size[0] * scale_factor, size[1] * scale_factor)
        return size
示例#3
0
class SpectraPlotWidget(PlotWidget):
    sigEnergyChanged = Signal(object)

    def __init__(self, linePos=650, txtPosRatio=0.35, invertX=True, *args, **kwargs):
        """
        A widget to display a 1D spectrum
        :param linePos: the initial position of the InfiniteLine
        :param txtPosRatio: a coefficient that determines the relative position of the textItem
        :param invertX: whether to invert X-axis
        """
        super(SpectraPlotWidget, self).__init__(*args, **kwargs)
        self._data = None
        assert (txtPosRatio >= 0) and (txtPosRatio <= 1), 'Please set txtPosRatio value between 0 and 1.'
        self.txtPosRatio = txtPosRatio
        self.positionmap = dict()
        self.wavenumbers = None
        self._meanSpec = True  # whether current spectrum is a mean spectrum
        self.line = InfiniteLine(movable=True)
        self.line.setPen((255, 255, 0, 200))
        self.line.setValue(linePos)
        self.line.sigPositionChanged.connect(self.sigEnergyChanged)
        self.line.sigPositionChanged.connect(self.getEnergy)
        self.addItem(self.line)
        self.cross = PlotDataItem([linePos], [0], symbolBrush=(255, 0, 0), symbolPen=(255, 0, 0), symbol='+',
                                  symbolSize=20)
        self.cross.setZValue(100)
        self.addItem(self.cross)
        self.txt = TextItem()
        self.getViewBox().invertX(invertX)
        self.spectrumInd = 0
        self.selectedPixels = None
        self._y = None

    def getEnergy(self, lineobject):
        if self._y is not None:
            x_val = lineobject.value()
            idx = val2ind(x_val, self.wavenumbers)
            x_val = self.wavenumbers[idx]
            y_val = self._y[idx]
            if not self._meanSpec:
                txt_html = toHtml(f'Spectrum #{self.spectrumInd}')
            else:
                txt_html = toHtml(f'{self._mean_title}')

            txt_html += toHtml(f'X = {x_val: .2f}, Y = {y_val: .4f}')
            self.txt.setHtml(txt_html)
            self.cross.setData([x_val], [y_val])

    def setHeader(self, header: NonDBHeader, field: str, *args, **kwargs):
        self.header = header
        self.field = field
        # get wavenumbers
        spectraEvent = next(header.events(fields=['spectra']))
        self.wavenumbers = spectraEvent['wavenumbers']
        self.N_w = len(self.wavenumbers)
        self.rc2ind = spectraEvent['rc_index']
        # make lazy array from document
        data = None
        try:
            data = header.meta_array(field)
        except IndexError:
            msg.logMessage(f'Header object contained no frames with field {field}.', msg.ERROR)

        if data is not None:
            # kwargs['transform'] = QTransform(1, 0, 0, -1, 0, data.shape[-2])
            self._data = data

    def showSpectra(self, i=0):
        if (self._data is not None) and (i < len(self._data)):
            self.getViewBox().clear()
            self._meanSpec = False
            self.spectrumInd = i
            self.plot(self.wavenumbers, self._data[i])

    def getSelectedPixels(self, selectedPixels):
        self.selectedPixels = selectedPixels
        # print(selectedPixels)

    def clearAll(self):
        # remove legend
        _legend = self.plotItem.legend
        if (_legend is not None) and (_legend.scene() is not None):
            _legend.scene().removeItem(_legend)
        self.getViewBox().clear()

    def showMeanSpectra(self):
        self._meanSpec = True
        self.getViewBox().clear()
        if self.selectedPixels is not None:
            n_spectra = len(self.selectedPixels)
            tmp = np.zeros((n_spectra, self.N_w))
            for j in range(n_spectra):  # j: jth selected pixel
                row_col = tuple(self.selectedPixels[j])
                tmp[j, :] = self._data[self.rc2ind[row_col]]
            self._mean_title = f'ROI mean of {n_spectra} spectra'
        else:
            n_spectra = len(self._data)
            tmp = np.zeros((n_spectra, self.N_w))
            for j in range(n_spectra):
                tmp[j, :] = self._data[j]
            self._mean_title = f'Total mean of {n_spectra} spectra'
        if n_spectra > 0:
            meanSpec = np.mean(tmp, axis=0)
        else:
            meanSpec = np.zeros_like(self.wavenumbers) + 1e-3
        self.plot(self.wavenumbers, meanSpec)

    def plot(self, x, y, *args, **kwargs):
        # set up infinity line and get its position
        self.plotItem.plot(x, y, *args, **kwargs)
        self.addItem(self.line)
        self.addItem(self.cross)
        x_val = self.line.value()
        if x_val == 0:
            y_val = 0
        else:
            idx = val2ind(x_val, self.wavenumbers)
            x_val = self.wavenumbers[idx]
            y_val = y[idx]

        if not self._meanSpec:
            txt_html = toHtml(f'Spectrum #{self.spectrumInd}')
        else:
            txt_html = toHtml(f'{self._mean_title}')

        txt_html += toHtml(f'X = {x_val: .2f}, Y = {y_val: .4f}')
        self.txt.setHtml(txt_html)
        ymax = np.max(y)
        self._y = y
        r = self.txtPosRatio
        self.txt.setPos(r * x[-1] + (1 - r) * x[0], ymax)
        self.cross.setData([x_val], [y_val])
        self.addItem(self.txt)
示例#4
0
class SliderGraph(PlotWidget):
    """
    An widget graph element that shows a line plot with more sequences. It
    also plot a vertical line that can be moved left and right by a user. When
    the line is moved a callback function is called with selected value (on
    x axis).

    Attributes
    ----------
    x_axis_label : str
        A text label for x axis
    y_axis_label : str
        A text label for y axis
    callback : callable
        A function which is called when selection is changed.
    background : str, optional (default: "w")
        Plot background color
    """
    def __init__(self, x_axis_label, y_axis_label, callback):
        super().__init__(background="w")

        axis = self.getAxis("bottom")
        axis.setLabel(x_axis_label)
        axis = self.getAxis("left")
        axis.setLabel(y_axis_label)

        self.getViewBox().setMenuEnabled(False)
        self.getViewBox().setMouseEnabled(False, False)
        self.showGrid(True, True, alpha=0.5)
        self.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0))
        self.hideButtons()

        # tuples to store horisontal lines and labels
        self.plot_horlabel = []
        self.plot_horline = []
        self._line = None
        self.callback = callback

        # variables to store sequences
        self.sequences = None
        self.x = None
        self.selection_limit = None
        self.data_increasing = None  # true if data mainly increasing

    def update(self,
               x,
               y,
               colors,
               cutpoint_x=None,
               selection_limit=None,
               names=None):
        """
        Function replots a graph.

        Parameters
        ----------
        x : np.ndarray
            One-dimensional array with X coordinates of the points
        y : array-like
            List of np.ndarrays that contains an array of Y values for each
            sequence.
        colors : array-like
            List of Qt colors (eg. Qt.red) for each sequence.
        cutpoint_x : int, optional
            A starting cutpoint - the location of the vertical line.
        selection_limit : tuple
            The tuple of two values that limit the range for selection.
        names : array-like
            The name of each sequence that shows in the legend, if None
            legend is not shown.
        legend_anchor : array-like
            The anchor of the legend in the graph
        """
        self.clear_plot()
        if names is None:
            names = [None] * len(y)

        self.sequences = y
        self.x = x
        self.selection_limit = selection_limit

        self.data_increasing = [np.sum(d[1:] - d[:-1]) > 0 for d in y]

        # plot sequence
        for s, c, n, inc in zip(y, colors, names, self.data_increasing):
            c = QColor(c)
            self.plot(x, s, pen=mkPen(c, width=2), antialias=True)

            if n is not None:
                label = TextItem(text=n,
                                 anchor=(0, 1),
                                 color=QColor(0, 0, 0, 128))
                label.setPos(x[-1], s[-1])
                self._set_anchor(label, len(x) - 1, inc)
                self.addItem(label)

        self._plot_cutpoint(cutpoint_x)
        self.autoRange()

    def clear_plot(self):
        """
        This function clears the plot and removes data.
        """
        self.clear()
        self.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0))
        self.plot_horlabel = []
        self.plot_horline = []
        self._line = None
        self.sequences = None

    def set_cut_point(self, x):
        """
        This function sets the cutpoint (selection line) at the specific
        location.

        Parameters
        ----------
        x : int
            Cutpoint location at the x axis.
        """
        self._plot_cutpoint(x)

    def _plot_cutpoint(self, x):
        """
        Function plots the cutpoint.

        Parameters
        ----------
        x : int
            Cutpoint location.
        """
        if x is None:
            self._line = None
            return
        if self._line is None:
            # plot interactive vertical line
            self._line = InfiniteLine(angle=90,
                                      pos=x,
                                      movable=True,
                                      bounds=self.selection_limit
                                      if self.selection_limit is not None else
                                      (self.x.min(), self.x.max()))
            self._line.setCursor(Qt.SizeHorCursor)
            self._line.setPen(mkPen(QColor(Qt.black), width=2))
            self._line.sigPositionChanged.connect(self._on_cut_changed)
            self.addItem(self._line)
        else:
            self._line.setValue(x)

        self._update_horizontal_lines()

    def _plot_horizontal_lines(self):
        """
        Function plots the vertical dashed lines that points to the selected
        sequence values at the y axis.
        """
        for _ in range(len(self.sequences)):
            self.plot_horline.append(
                PlotCurveItem(pen=mkPen(QColor(Qt.blue), style=Qt.DashLine)))
            self.plot_horlabel.append(
                TextItem(color=QColor(Qt.black), anchor=(0, 1)))
        for item in self.plot_horlabel + self.plot_horline:
            self.addItem(item)

    def _set_anchor(self, label, cutidx, inc):
        """
        This function set the location of the text label around the selected
        point at the curve. It place the text such that it is not plotted
        at the line.

        Parameters
        ----------
        label : TextItem
            Text item that needs to have location set.
        cutidx : int
            The index of the selected element in the list. If index in first
            part of the list we put label on the right side else on the left,
            such that it does not disappear at the graph edge.
        inc : bool
            This parameter tels whether the curve value is increasing or
            decreasing.
        """
        if inc:
            label.anchor = Point(0, 0) if cutidx < len(self.x) / 2 \
                else Point(1, 1)
        else:
            label.anchor = Point(0, 1) if cutidx < len(self.x) / 2 \
                else Point(1, 0)

    def _update_horizontal_lines(self):
        """
        This function update the horisontal lines when selection changes.
        If lines are present jet it calls the function to init them.
        """
        if not self.plot_horline:  # init horizontal lines
            self._plot_horizontal_lines()

        # in every case set their position
        location = int(round(self._line.value()))
        cutidx = np.searchsorted(self.x, location)
        minx = np.min(self.x)
        for s, curve, label, inc in zip(self.sequences, self.plot_horline,
                                        self.plot_horlabel,
                                        self.data_increasing):
            y = s[cutidx]
            curve.setData([minx, location], [y, y])
            self._set_anchor(label, cutidx, inc)
            label.setPos(location, y)
            label.setPlainText("{:.3f}".format(y))

    def _on_cut_changed(self, line):
        """
        This function is called when selection changes. It extract the selected
        value and calls the callback function.

        Parameters
        ----------
        line : InfiniteLine
            The cutpoint - selection line.
        """
        # cut changed by means of a cut line over the scree plot.
        value = int(round(line.value()))

        # vertical line can take only int positions
        self._line.setValue(value)

        self._update_horizontal_lines()
        self.callback(value)
示例#5
0
class ScanPlotWidget(PlotWidget):
    """
    Extend the PlotWidget Class with more functionality used for qudi scan images.
    Supported features:
     - draggable/static crosshair with optional range and size constraints.
     - zoom feature by rubberband selection
     - rubberband area selection

    This class depends on the ScanViewBox class defined further below.
    This class can be promoted in the Qt designer.
    """
    sigMouseAreaSelected = QtCore.Signal(QtCore.QRectF)  # mapped rectangle mouse cursor selection
    sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF)
    sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF)

    def __init__(self, *args, **kwargs):
        kwargs['viewBox'] = ScanViewBox()  # Use custom pg.ViewBox subclass
        super().__init__(*args, **kwargs)
        self.getViewBox().sigMouseAreaSelected.connect(self.sigMouseAreaSelected)

        self._min_crosshair_factor = 0.02
        self._crosshair_size = (0, 0)
        self._crosshair_range = None
        self.getViewBox().sigRangeChanged.connect(self._constraint_crosshair_size)

        self.crosshair = ROI((0, 0), (0, 0), pen={'color': '#00ff00', 'width': 1})
        self.hline = InfiniteLine(pos=0,
                                  angle=0,
                                  movable=True,
                                  pen={'color': '#00ff00', 'width': 1},
                                  hoverPen={'color': '#ffff00', 'width': 1})
        self.vline = InfiniteLine(pos=0,
                                  angle=90,
                                  movable=True,
                                  pen={'color': '#00ff00', 'width': 1},
                                  hoverPen={'color': '#ffff00', 'width': 1})
        self.vline.sigDragged.connect(self._update_pos_from_line)
        self.hline.sigDragged.connect(self._update_pos_from_line)
        self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi)
        self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged)

    @property
    def crosshair_enabled(self):
        items = self.items()
        return (self.vline in items) and (self.hline in items) and (self.crosshair in items)

    @property
    def crosshair_movable(self):
        return bool(self.crosshair.translatable)

    @property
    def crosshair_position(self):
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        return tuple(pos)

    @property
    def crosshair_size(self):
        return tuple(self._crosshair_size)

    @property
    def crosshair_min_size_factor(self):
        return float(self._min_crosshair_factor)

    @property
    def crosshair_range(self):
        if self._crosshair_range is None:
            return None
        return tuple(self._crosshair_range)

    @property
    def selection_enabled(self):
        return bool(self.getViewBox().rectangle_selection)

    @property
    def zoom_by_selection_enabled(self):
        return bool(self.getViewBox().zoom_by_selection)

    def toggle_selection(self, enable):
        """
        De-/Activate the rectangular rubber band selection tool.
        If active you can select a rectangular region within the ViewBox by dragging the mouse
        with the left button. Each selection rectangle in real-world data coordinates will be
        emitted by sigMouseAreaSelected.
        By using activate_zoom_by_selection you can optionally de-/activate zooming in on the
        selection.

        @param bool enable: Toggle selection on (True) or off (False)
        """
        return self.getViewBox().toggle_selection(enable)

    def toggle_zoom_by_selection(self, enable):
        """
        De-/Activate automatic zooming into a selection.
        See also: toggle_selection

        @param bool enable: Toggle zoom upon selection on (True) or off (False)
        """
        return self.getViewBox().toggle_zoom_by_selection(enable)

    def _update_pos_from_line(self, obj):
        """
        Called each time the position of the InfiniteLines has been changed by a user drag.
        Causes the crosshair rectangle to follow the lines.
        """
        if obj not in (self.hline, self.vline):
            return
        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1]
        size = self.crosshair.size()
        self.crosshair.blockSignals(True)
        self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2))
        self.crosshair.blockSignals(False)
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def _update_pos_from_roi(self, obj):
        """
        Called each time the position of the rectangular ROI has been changed by a user drag.
        Causes the InfiniteLines to follow the ROI.
        """
        if obj is not self.crosshair:
            return
        pos = self.crosshair.pos()
        size = self.crosshair.size()
        pos[0] += size[0] / 2
        pos[1] += size[1] / 2
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1]))
        return

    def toggle_crosshair(self, enable, movable=True):
        """
        Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be
        dragged by the user.

        @param bool enable: enable crosshair (True), disable crosshair (False)
        @param bool movable: enable user drag (True), disable user drag (False)
        """
        if not isinstance(enable, bool):
            raise TypeError('Positional argument "enable" must be bool type.')
        if not isinstance(movable, bool):
            raise TypeError('Optional argument "movable" must be bool type.')

        self.toggle_crosshair_movable(movable)

        is_enabled = self.crosshair_enabled
        if enable and not is_enabled:
            self.addItem(self.vline)
            self.addItem(self.hline)
            self.addItem(self.crosshair)
        elif not enable and is_enabled:
            self.removeItem(self.vline)
            self.removeItem(self.hline)
            self.removeItem(self.crosshair)
        return

    def toggle_crosshair_movable(self, enable):
        """
        Toggle if the crosshair can be dragged by the user.

        @param bool enable: enable (True), disable (False)
        """
        self.crosshair.translatable = bool(enable)
        self.vline.setMovable(enable)
        self.hline.setMovable(enable)
        return

    def set_crosshair_pos(self, pos):
        """
        Set the crosshair center to the given coordinates.

        @param QPointF|float[2] pos: (x,y) position of the crosshair
        """
        try:
            pos = tuple(pos)
        except TypeError:
            pos = (pos.x(), pos.y())
        size = self.crosshair.size()

        self.crosshair.blockSignals(True)
        self.vline.blockSignals(True)
        self.hline.blockSignals(True)
        self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self.vline.setPos(pos[0])
        self.hline.setPos(pos[1])
        self.crosshair.blockSignals(False)
        self.vline.blockSignals(False)
        self.hline.blockSignals(False)
        self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos))
        return

    def set_crosshair_size(self, size, force_default=True):
        """
        Set the default size of the crosshair rectangle (x, y) and update the display.

        @param QSize|float[2] size: the (x,y) size of the crosshair rectangle
        @param bool force_default: Set default crosshair size and enforce minimal size (True).
                                   Enforce displayed crosshair size while keeping default size
                                   untouched (False).
        """
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        if force_default:
            if size[0] <= 0 and size[1] <= 0:
                self._crosshair_size = (0, 0)
            else:
                self._crosshair_size = size
                # Check if actually displayed size needs to be adjusted due to minimal size
                size = self._get_corrected_crosshair_size(size)

        pos = self.vline.pos()
        pos[1] = self.hline.pos()[1] - size[1] / 2
        pos[0] -= size[0] / 2

        if self._crosshair_range:
            crange = self._crosshair_range
            self.crosshair.maxBounds = QtCore.QRectF(crange[0][0] - size[0] / 2,
                                                     crange[1][0] - size[1] / 2,
                                                     crange[0][1] - crange[0][0] + size[0],
                                                     crange[1][1] - crange[1][0] + size[1])
        self.crosshair.blockSignals(True)
        self.crosshair.setSize(size)
        self.crosshair.setPos(pos)
        self.crosshair.blockSignals(False)
        return

    def set_crosshair_min_size_factor(self, factor):
        """
        Sets the minimum crosshair size factor. This will determine the minimum size of the
        smallest edge of the crosshair center rectangle.
        This minimum size is calculated by taking the smallest visible axis of the ViewBox and
        multiplying it with the scale factor set by this method.
        The crosshair rectangle will be then scaled accordingly if the set crosshair size is
        smaller than this minimal size.

        @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced.
        """
        if factor <= 0:
            self._min_crosshair_factor = 0
        elif factor <= 1:
            self._min_crosshair_factor = float(factor)
        else:
            raise ValueError('Crosshair min size factor must be a value <= 1.')
        return

    def set_crosshair_range(self, new_range):
        """
        Sets a range boundary for the crosshair position.

        @param float[2][2] new_range: two min-max range value tuples (for x and y axis).
                                      If None set unlimited ranges.
        """
        if new_range is None:
            self.vline.setBounds([None, None])
            self.hline.setBounds([None, None])
            self.crosshair.maxBounds = None
        else:
            self.vline.setBounds(new_range[0])
            self.hline.setBounds(new_range[1])
            size = self.crosshair.size()
            pos = self.crosshair_position
            self.crosshair.maxBounds = QtCore.QRectF(new_range[0][0] - size[0] / 2,
                                                     new_range[1][0] - size[1] / 2,
                                                     new_range[0][1] - new_range[0][0] + size[0],
                                                     new_range[1][1] - new_range[1][0] + size[1])
            self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2)
        self._crosshair_range = new_range
        return

    def set_crosshair_pen(self, pen):
        """
        Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines.

        @param pen: pyqtgraph compatible pen to use
        """
        self.crosshair.setPen(pen)
        self.vline.setPen(pen)
        self.hline.setPen(pen)
        return

    def _constraint_crosshair_size(self):
        if self._min_crosshair_factor == 0:
            return
        if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0:
            return
        corr_size = self._get_corrected_crosshair_size(self._crosshair_size)
        if corr_size != tuple(self.crosshair.size()):
            self.set_crosshair_size(corr_size, force_default=False)
        return

    def _get_corrected_crosshair_size(self, size):
        try:
            size = tuple(size)
        except TypeError:
            size = (size.width(), size.height())

        min_size = min(size)
        if min_size == 0:
            return size
        vb_size = self.getViewBox().viewRect().size()
        short_index = int(vb_size.width() > vb_size.height())
        min_vb_size = vb_size.width() if short_index == 0 else vb_size.height()
        min_vb_size *= self._min_crosshair_factor

        if min_size < min_vb_size:
            scale_factor = min_vb_size / min_size
            size = (size[0] * scale_factor, size[1] * scale_factor)
        return size