Пример #1
0
class DataFrameEditor(DockMixin, QWidget):
    """An editor for data frames"""

    dock_cls = DataFrameDock

    #: A signal that is emitted, if the table is cleared
    cleared = QtCore.pyqtSignal()

    #: A signal that is emitted when a cell has been changed. The argument
    #: is a tuple of two integers and one float:
    #: the row index, the column index and the new value
    cell_edited = QtCore.pyqtSignal(int, int, object, object)

    #: A signal that is emitted, if rows have been inserted into the dataframe.
    #: The first value is the integer of the (original) position of the row,
    #: the second one is the number of rows
    rows_inserted = QtCore.pyqtSignal(int, int)

    @property
    def hidden(self):
        return not self.table.filled

    def __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(
            self.table.setSortingEnabled)

    def update_index_editable(self):
        model = self.table.model()
        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        self.cb_index_editable.setChecked(model.index_editable)

    def set_lbl_size_text(self, nrows=None, ncols=None):
        """Set the text of the :attr:`lbl_size` label to display the size"""
        model = self.table.model()
        nrows = nrows if nrows is not None else model.rowCount()
        ncols = ncols if ncols is not None else model.columnCount()
        if not nrows and not ncols:
            self.lbl_size.setText('')
        else:
            self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols))

    def clear_table(self):
        """Clear the table and emit the :attr:`cleared` signal"""
        df = pd.DataFrame()
        self.set_df(df, show=False)

    def _open_ds_from_console(self, oname, df):
        self.set_df(df)

    @docstrings.dedent
    def set_df(self, df, *args, **kwargs):
        """
        Fill the table from a :class:`~pandas.DataFrame`

        Parameters
        ----------
        %(DataFrameModel.parameters.no_parent)s
        show: bool
            If True (default), show and raise_ the editor
        """
        show = kwargs.pop('show', True)
        self.table.set_df(df, *args, **kwargs)
        self.set_lbl_size_text(*df.shape)
        model = self.table.model()
        self.cb_dtypes_changeable.setChecked(model.dtypes_changeable)

        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        else:
            self.cb_index_editable.setEnabled(True)
        self.cb_index_editable.setChecked(model.index_editable)
        self.cleared.emit()
        if show:
            self.show_plugin()
            self.dock.raise_()

    def set_index_editable(self, state):
        """Set the :attr:`DataFrameModel.index_editable` attribute"""
        self.table.model().index_editable = state == Qt.Checked

    def set_dtypes_changeable(self, state):
        """Set the :attr:`DataFrameModel.dtypes_changeable` attribute"""
        self.table.model().dtypes_changeable = state == Qt.Checked

    def toggle_fmt_button(self, text):
        try:
            text % 1.1
        except (TypeError, ValueError):
            self.btn_change_format.setEnabled(False)
        else:
            self.btn_change_format.setEnabled(
                text.strip() != self.table.model()._format)

    def update_format(self):
        """Update the format of the table"""
        self.table.model().set_format(self.format_editor.text().strip())

    def to_dock(self, main, *args, **kwargs):
        connect = self.dock is None
        super(DataFrameEditor, self).to_dock(main, *args, **kwargs)
        if connect:
            self.dock.toggleViewAction().triggered.connect(self.maybe_tabify)

    def maybe_tabify(self):
        main = self.dock.parent()
        if self.is_shown and main.dockWidgetArea(
                main.help_explorer.dock) == main.dockWidgetArea(self.dock):
            main.tabifyDockWidget(main.help_explorer.dock, self.dock)

    def _open_dataframe(self):
        self.open_dataframe()

    def open_dataframe(self, fname=None, *args, **kwargs):
        """Opens a file dialog and the dataset that has been inserted"""
        if fname is None:
            fname = QFileDialog.getOpenFileName(
                self, 'Open dataset', os.getcwd(),
                'Comma separated files (*.csv);;'
                'Excel files (*.xls *.xlsx);;'
                'JSON files (*.json);;'
                'All files (*)'
                )
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if isinstance(fname, pd.DataFrame):
            self.set_df(fname)
        elif not fname:
            return
        else:
            ext = osp.splitext(fname)[1]
            open_funcs = {
                '.xls': pd.read_excel, '.xlsx': pd.read_excel,
                '.json': pd.read_json,
                '.tab': partial(pd.read_csv, delimiter='\t'),
                '.dat': partial(pd.read_csv, delim_whitespace=True),
                }
            open_func = open_funcs.get(ext, pd.read_csv)
            try:
                df = open_func(fname)
            except Exception:
                self.error_msg.showTraceback(
                    '<b>Could not open DataFrame %s with %s</b>' % (
                        fname, open_func))
                return
            self.set_df(df)

    def close(self, *args, **kwargs):
        if self.dock is not None:
            self.dock.close(*args, **kwargs)  # removes the dock window
            del self.dock
        return super(DataFrameEditor, self).close(*args, **kwargs)
Пример #2
0
class StackedReader(DataReader, StraditizerControlBase):
    """A DataReader for stacked area plots

    This reader only works within the straditizer GUI because the digitization
    (see :meth:`digitize`) is interactive. The user has to manually distinguish
    the stacked variables."""

    #: The QTreeWidgetItem that holds the digitization widgets
    digitize_child = None

    #: A QPushButton to select the previous variable during the digitization
    #: (see :meth:`decrease_current_col`)
    btn_prev = None

    #: A QPushButton to select the next variable during the digitization
    #: (see :meth:`increase_current_col`)
    btn_next = None

    #: A QPushButton to select the features in the image for the current
    #: variable (see :meth:`select_current_column`)
    btn_edit = None

    #: A QPushButton to add a new variable to the current ones
    #: (see :meth:`select_and_add_current_column`)
    btn_add = None

    #: A QLabel to display the current column
    lbl_col = None

    strat_plot_identifier = 'stacked'

    _current_col = 0

    def digitize(self):
        """Digitize the data interactively

        This method creates a new child item for the digitize button in the
        straditizer control to manually distinguish the variables in the
        stacked diagram."""
        if getattr(self, 'straditizer_widgets', None) is None:
            self.init_straditizercontrol(get_straditizer_widgets())
        digitizer = self.straditizer_widgets.digitizer
        digitizing = digitizer.btn_digitize.isChecked()
        if digitizing and self.digitize_child is None:
            raise ValueError("Apparently another digitization is in progress!")
        elif not digitizing and self.digitize_child is None:
            if len(self.columns) == 1 or self._current_col not in self.columns:
                self._current_col = self.columns[0]
            if len(self.columns) == 1:
                super(StackedReader, self).digitize()
            # start digitization
            digitizer.btn_digitize.setCheckable(True)
            digitizer.btn_digitize.setChecked(True)
            self._init_digitize_child()
            # Disable the changing of readers
            digitizer.cb_readers.setEnabled(False)
            digitizer.tree.expandItem(digitizer.digitize_item)
            self.enable_or_disable_navigation_buttons()
            self.reset_lbl_col()
        elif not digitizing:
            # stop digitization
            digitizer.btn_digitize.setChecked(False)
            digitizer.btn_digitize.setCheckable(False)
            self._remove_digitze_child(digitizer)
            digitizer.cb_readers.setEnabled(
                digitizer.should_be_enabled(digitizer.cb_readers))
            del self.straditizer_widgets

    def _init_digitize_child(self):
        self.lbl_col = QLabel('')
        self.btn_prev = QPushButton('<')
        self.btn_next = QPushButton('>')
        self.btn_edit = QPushButton('Edit')
        self.btn_add = QPushButton('+')
        self.reset_lbl_col()
        self.btn_box = w = QWidget()
        vbox = QVBoxLayout()
        vbox.addWidget(self.lbl_col)
        hbox = QHBoxLayout()
        hbox.addWidget(self.btn_prev)
        hbox.addWidget(self.btn_next)
        hbox.addWidget(self.btn_edit)
        hbox.addWidget(self.btn_add)
        vbox.addLayout(hbox)
        w.setLayout(vbox)

        self.digitize_child = QTreeWidgetItem(0)
        self.straditizer_widgets.digitizer.digitize_item.addChild(
            self.digitize_child)
        self.straditizer_widgets.digitizer.tree.setItemWidget(
            self.digitize_child, 0, w)
        self.widgets2disable = [self.btn_prev, self.btn_next,
                                self.btn_edit, self.btn_add]

        self.btn_next.clicked.connect(self.increase_current_col)
        self.btn_prev.clicked.connect(self.decrease_current_col)
        self.btn_edit.clicked.connect(self.select_current_column)
        self.btn_add.clicked.connect(self.select_and_add_current_column)

    def reset_lbl_col(self):
        """Reset the :attr:`lbl_col` to display the current column"""
        self.lbl_col.setText('Part %i of %i' % (
            self.columns.index(self._current_col) + 1, len(self.columns)))

    def increase_current_col(self):
        """Take the next column as the current column"""
        self._current_col = min(self.columns[-1], self._current_col + 1)
        self.reset_lbl_col()
        self.enable_or_disable_navigation_buttons()

    def decrease_current_col(self):
        """Take the previous column as the current column"""
        self._current_col = max(self.columns[0], self._current_col - 1)
        self.reset_lbl_col()
        self.enable_or_disable_navigation_buttons()

    def _remove_digitze_child(self, digitizer):
        digitizer.digitize_item.takeChild(
            digitizer.digitize_item.indexOfChild(
                self.digitize_child))
        digitizer.btn_digitize.setChecked(False)
        digitizer.btn_digitize.setCheckable(False)
        for btn in self.widgets2disable:
            btn.clicked.disconnect()
        del (self.digitize_child, self.btn_prev, self.btn_next, self.btn_add,
             self.btn_edit, self.lbl_col, self.btn_box)
        self.widgets2disable.clear()

    def enable_or_disable_navigation_buttons(self):
        """Enable or disable :attr:`btn_prev` and :attr:`btn_next`

        Depending on the current column, we disable the navigation buttons
        :attr:`btn_prev` and :attr:`btn_next`"""
        disable_all = self.columns is None or len(self.columns) == 1
        self.btn_prev.setEnabled(not disable_all and
                                 self._current_col != self.columns[0])
        self.btn_next.setEnabled(not disable_all and
                                 self._current_col != self.columns[-1])

    def select_and_add_current_column(self):
        """Select the features for a column and create it as a new one"""
        return self._select_current_column(True)

    def select_current_column(self):
        """Select the features of the current column"""
        return self._select_current_column()

    def _select_current_column(self, add_on_apply=False):
        image = self.to_grey_pil(self.image).astype(int) + 1
        start = self.start_of_current_col
        end = start + self.full_df[self._current_col].values
        all_end = start + self.full_df.loc[:, self._current_col:].values.sum(
            axis=1)
        x = np.meshgrid(*map(np.arange, image.shape[::-1]))[0]
        image[(x < start[:, np.newaxis]) | (x > all_end[:, np.newaxis])] = 0
        labels = skim.label(image, 8)
        self.straditizer_widgets.selection_toolbar.data_obj = self
        self.apply_button.clicked.connect(
            self.add_col if add_on_apply else self.update_col)
        self.apply_button.clicked.connect(self.update_plotted_full_df)
        self.straditizer_widgets.selection_toolbar.start_selection(
            labels, rgba=self.image_array(), remove_on_apply=False)
        self.select_all_labels()
        # set values outside the current column to 0
        self._selection_arr[(x < start[:, np.newaxis]) |
                            (x >= end[:, np.newaxis])] = -1
        self._select_img.set_array(self._selection_arr)
        self.draw_figure()

    @property
    def start_of_current_col(self):
        """The first x-pixel of the current column"""
        if self._current_col == self.columns[0]:
            start = np.zeros(self.binary.shape[:1])
        else:
            idx = self.columns.index(self._current_col)
            start = self.full_df.iloc[:, :idx].values.sum(axis=1)
        start += self.column_starts[0]
        return start

    def update_plotted_full_df(self):
        """Update the plotted full_df if it is shown

        See Also
        --------
        plot_full_df"""
        pc = self.straditizer_widgets.plot_control.table
        if pc.can_plot_full_df() and pc.get_full_df_lines():
            pc.remove_full_df_plot()
            pc.plot_full_df()

    def update_col(self):
        """Update the current column based on the selection.

        This method updates the end of the current column and adds or removes
        the changes from the columns to the right."""
        current = self._current_col
        start = self.start_of_current_col
        selected = self.selected_part
        end = (self.binary.shape[1] - selected[:, ::-1].argmax(axis=1) -
               start)
        not_selected = ~selected.any()
        end[not_selected] = 0

        diff_end = self.parent._full_df.loc[:, current] - end
        self.parent._full_df.loc[:, current] = end
        if current != self.columns[-1]:
            self.parent._full_df.loc[:, current + 1] += diff_end

    def get_binary_for_col(self, col):
        s, e = self.column_bounds[self.columns.index(col)]
        if self.parent._full_df is None:
            return self.binary[:, s:e]
        else:
            vals = self.full_df.loc[:, col].values
            ret = np.zeros((self.binary.shape[0], int(vals.max())))
            dist = np.tile(np.arange(ret.shape[1])[np.newaxis], (len(ret), 1))
            ret[dist <= vals[:, np.newaxis]] = 1
            return ret

    def add_col(self):
        """Create a column out of the current selection"""
        def increase_col_nums(df):
            df_cols = df.columns.values
            df_cols[df_cols >= current] += 1
            df.columns = df_cols
        current = self._current_col
        start = self.start_of_current_col
        selected = self.selected_part
        end = (self.binary.shape[1] - selected[:, ::-1].argmax(axis=1) -
               start)
        not_selected = ~selected.any()
        end[not_selected] = 0

        # ----- Update of reader column numbers -----
        for reader in self.iter_all_readers:
            for i, col in enumerate(reader.columns):
                if col >= current:
                    reader.columns[i] += 1
        self.columns.insert(self.columns.index(current + 1), current)
        self.parent._column_starts = np.insert(
            self.parent._column_starts, current, self._column_starts[current])
        if self.parent._column_ends is not None:
            self.parent._column_ends = np.insert(
                self.parent._column_ends, current,
                self.parent._column_ends[current])

        # ----- Update of column numbers in dataframes -----
        # increase column numbers in full_df
        full_df = self.parent._full_df
        increase_col_nums(full_df)
        # increase column numbers in samples
        samples = self.parent._sample_locs
        if samples is not None:
            increase_col_nums(samples)

        # ----- Update of DataFrames -----
        # update the current column in full_df and add the new one
        full_df.loc[:, current + 1] -= end
        full_df[current] = end
        full_df.sort_index(axis=1, inplace=True)
        # update the current column in samples and add the new one
        if samples is not None:
            new_samples = full_df.loc[samples.index, current]
            samples.loc[:, current + 1] -= new_samples
            samples[current] = new_samples
            samples.sort_index(axis=1, inplace=True)
        rough_locs = self.parent.rough_locs
        if rough_locs is not None:
            rough_locs[(current + 1, 'vmin')] = rough_locs[(current, 'vmin')]
            rough_locs[(current + 1, 'vmax')] = rough_locs[(current, 'vmax')]
            rough_locs.loc[:, current] = -1
            rough_locs.sort_index(inplace=True, level=0)
        self.reset_lbl_col()
        self.enable_or_disable_navigation_buttons()

    def plot_full_df(self, ax=None):
        """Plot the lines for the digitized diagram"""
        vals = self.full_df.values
        starts = self.column_starts
        self.lines = lines = []
        y = np.arange(np.shape(self.image)[0])
        ax = ax or self.ax
        if self.extent is not None:
            y += self.extent[-1]
            starts += self.extent[0]
        x = np.zeros_like(vals[:, 0]) + starts[0]
        for i in range(vals.shape[1]):
            x += vals[:, i]
            lines.extend(ax.plot(x.copy(), y, lw=2.0))

    def plot_potential_samples(self, excluded=False, ax=None, plot_kws={},
                               *args, **kwargs):
        """Plot the ranges for potential samples"""
        vals = self.full_df.values.copy()
        starts = self.column_starts.copy()
        self.sample_ranges = lines = []
        y = np.arange(np.shape(self.image)[0])
        ax = ax or self.ax
        plot_kws = dict(plot_kws)
        plot_kws.setdefault('marker', '+')
        if self.extent is not None:
            y += self.extent[-1]
            starts = starts + self.extent[0]
        x = np.zeros(vals.shape[0]) + starts[0]
        for i, (col, arr) in enumerate(zip(self.columns, vals.T)):
            all_indices, excluded_indices = self.find_potential_samples(
                i, *args, **kwargs)
            if excluded:
                all_indices = excluded_indices
            if not all_indices:
                x += arr
                continue
            mask = np.ones(arr.size, dtype=bool)
            for imin, imax in all_indices:
                mask[imin:imax] = False
            for imin, imax in all_indices:
                lines.extend(ax.plot(
                    np.where(mask, np.nan, arr)[imin:imax] + x[imin:imax],
                    y[imin:imax], **plot_kws))
            x += arr

    def resize_axes(self, grouper, bounds):
        """Reimplemented to do nothing"""
        xmin = bounds.min()
        xmax = bounds.max()
        grouper.plotters[0].update(xlim=(xmin, xmax))
        return
Пример #3
0
class DataFrameEditor(DockMixin, QWidget):
    """An editor for data frames"""

    dock_cls = DataFrameDock

    #: A signal that is emitted, if the table is cleared
    cleared = QtCore.pyqtSignal()

    #: A signal that is emitted when a cell has been changed. The argument
    #: is a tuple of two integers and one float:
    #: the row index, the column index and the new value
    cell_edited = QtCore.pyqtSignal(int, int, object, object)

    #: A signal that is emitted, if rows have been inserted into the dataframe.
    #: The first value is the integer of the (original) position of the row,
    #: the second one is the number of rows
    rows_inserted = QtCore.pyqtSignal(int, int)

    @property
    def hidden(self):
        return not self.table.filled

    def __init__(self, *args, **kwargs):
        super(DataFrameEditor, self).__init__(*args, **kwargs)
        self.error_msg = PyErrorMessage(self)

        # Label for displaying the DataFrame size
        self.lbl_size = QLabel()

        # A Checkbox for enabling and disabling the editability of the index
        self.cb_index_editable = QCheckBox('Index editable')

        # A checkbox for enabling and disabling the change of data types
        self.cb_dtypes_changeable = QCheckBox('Datatypes changeable')

        # A checkbox for enabling and disabling sorting
        self.cb_enable_sort = QCheckBox('Enable sorting')

        # A button to open a dataframe from the file
        self.btn_open_df = QToolButton(parent=self)
        self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png')))
        self.btn_open_df.setToolTip('Open a DataFrame from your disk')

        self.btn_from_console = LoadFromConsoleButton(pd.DataFrame)
        self.btn_from_console.setToolTip('Show a DataFrame from the console')

        # The table to display the DataFrame
        self.table = DataFrameView(pd.DataFrame(), self)

        # format line edit
        self.format_editor = QLineEdit()
        self.format_editor.setText(self.table.model()._format)

        # format update button
        self.btn_change_format = QPushButton('Update')
        self.btn_change_format.setEnabled(False)

        # table clearing button
        self.btn_clear = QPushButton('Clear')
        self.btn_clear.setToolTip(
            'Clear the table and disconnect from the DataFrame')

        # refresh button
        self.btn_refresh = QToolButton()
        self.btn_refresh.setIcon(QIcon(get_icon('refresh.png')))
        self.btn_refresh.setToolTip('Refresh the table')

        # close button
        self.btn_close = QPushButton('Close')
        self.btn_close.setToolTip('Close this widget permanentely')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_index_editable)
        hbox.addWidget(self.cb_dtypes_changeable)
        hbox.addWidget(self.cb_enable_sort)
        hbox.addWidget(self.lbl_size)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_open_df)
        hbox.addWidget(self.btn_from_console)
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)
        self.bottom_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.format_editor)
        hbox.addWidget(self.btn_change_format)
        hbox.addStretch(0)
        hbox.addWidget(self.btn_clear)
        hbox.addWidget(self.btn_close)
        hbox.addWidget(self.btn_refresh)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.cb_dtypes_changeable.stateChanged.connect(
            self.set_dtypes_changeable)
        self.cb_index_editable.stateChanged.connect(self.set_index_editable)
        self.btn_from_console.object_loaded.connect(self._open_ds_from_console)
        self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text())
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_clear.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(self.clear_table)
        self.btn_close.clicked.connect(lambda: self.close())
        self.btn_refresh.clicked.connect(self.table.reset_model)
        self.btn_open_df.clicked.connect(self._open_dataframe)
        self.table.set_index_action.triggered.connect(
            self.update_index_editable)
        self.table.append_index_action.triggered.connect(
            self.update_index_editable)
        self.cb_enable_sort.stateChanged.connect(
            self.table.setSortingEnabled)

    def update_index_editable(self):
        model = self.table.model()
        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        self.cb_index_editable.setChecked(model.index_editable)

    def set_lbl_size_text(self, nrows=None, ncols=None):
        """Set the text of the :attr:`lbl_size` label to display the size"""
        model = self.table.model()
        nrows = nrows if nrows is not None else model.rowCount()
        ncols = ncols if ncols is not None else model.columnCount()
        if not nrows and not ncols:
            self.lbl_size.setText('')
        else:
            self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols))

    def clear_table(self):
        """Clear the table and emit the :attr:`cleared` signal"""
        df = pd.DataFrame()
        self.set_df(df, show=False)

    def _open_ds_from_console(self, oname, df):
        self.set_df(df)

    @docstrings.dedent
    def set_df(self, df, *args, **kwargs):
        """
        Fill the table from a :class:`~pandas.DataFrame`

        Parameters
        ----------
        %(DataFrameModel.parameters.no_parent)s
        show: bool
            If True (default), show and raise_ the editor
        """
        show = kwargs.pop('show', True)
        self.table.set_df(df, *args, **kwargs)
        self.set_lbl_size_text(*df.shape)
        model = self.table.model()
        self.cb_dtypes_changeable.setChecked(model.dtypes_changeable)

        if len(model.df.index.names) > 1:
            model.index_editable = False
            self.cb_index_editable.setEnabled(False)
        else:
            self.cb_index_editable.setEnabled(True)
        self.cb_index_editable.setChecked(model.index_editable)
        self.cleared.emit()
        if show:
            self.show_plugin()
            self.dock.raise_()

    def set_index_editable(self, state):
        """Set the :attr:`DataFrameModel.index_editable` attribute"""
        self.table.model().index_editable = state == Qt.Checked

    def set_dtypes_changeable(self, state):
        """Set the :attr:`DataFrameModel.dtypes_changeable` attribute"""
        self.table.model().dtypes_changeable = state == Qt.Checked

    def toggle_fmt_button(self, text):
        try:
            text % 1.1
        except (TypeError, ValueError):
            self.btn_change_format.setEnabled(False)
        else:
            self.btn_change_format.setEnabled(
                text.strip() != self.table.model()._format)

    def update_format(self):
        """Update the format of the table"""
        self.table.model().set_format(self.format_editor.text().strip())

    def to_dock(self, main, *args, **kwargs):
        connect = self.dock is None
        super(DataFrameEditor, self).to_dock(main, *args, **kwargs)
        if connect:
            self.dock.toggleViewAction().triggered.connect(self.maybe_tabify)

    def maybe_tabify(self):
        main = self.dock.parent()
        if self.is_shown and main.dockWidgetArea(
                main.help_explorer.dock) == main.dockWidgetArea(self.dock):
            main.tabifyDockWidget(main.help_explorer.dock, self.dock)

    def _open_dataframe(self):
        self.open_dataframe()

    def open_dataframe(self, fname=None, *args, **kwargs):
        """Opens a file dialog and the dataset that has been inserted"""
        if fname is None:
            fname = QFileDialog.getOpenFileName(
                self, 'Open dataset', os.getcwd(),
                'Comma separated files (*.csv);;'
                'Excel files (*.xls *.xlsx);;'
                'JSON files (*.json);;'
                'All files (*)'
                )
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if isinstance(fname, pd.DataFrame):
            self.set_df(fname)
        elif not fname:
            return
        else:
            ext = osp.splitext(fname)[1]
            open_funcs = {
                '.xls': pd.read_excel, '.xlsx': pd.read_excel,
                '.json': pd.read_json,
                '.tab': partial(pd.read_csv, delimiter='\t'),
                '.dat': partial(pd.read_csv, delim_whitespace=True),
                }
            open_func = open_funcs.get(ext, pd.read_csv)
            try:
                df = open_func(fname)
            except Exception:
                self.error_msg.showTraceback(
                    '<b>Could not open DataFrame %s with %s</b>' % (
                        fname, open_func))
                return
            self.set_df(df)

    def close(self, *args, **kwargs):
        if self.dock is not None:
            self.dock.close(*args, **kwargs)  # removes the dock window
            del self.dock
        return super(DataFrameEditor, self).close(*args, **kwargs)
Пример #4
0
class PatternSelectionWidget(QWidget, DockMixin):
    """A wdiget to select patterns in the image

    This widget consist of an :class:`EmbededMplCanvas` to display the template
    for the pattern and uses the :func:`skimage.feature.match_template`
    function to identify it in the :attr:`arr`

    See Also
    --------
    straditize.widget.selection_toolbar.SelectionToolbar.start_pattern_selection
    """

    #: The template to look for in the :attr:`arr`
    template = None

    #: The selector to select the template in the original image
    selector = None

    #: The extents of the :attr:`template` in the original image
    template_extents = None

    #: The matplotlib artist of the :attr:`template` in the
    #: :attr:`template_fig`
    template_im = None

    #: The :class:`EmbededMplCanvas` to display the :attr:`template`
    template_fig = None

    axes = None

    #: A QSlider to set the threshold for the template correlation
    sl_thresh = None

    _corr_plot = None

    key_press_cid = None

    def __init__(self, arr, data_obj, remove_selection=False, *args, **kwargs):
        """
        Parameters
        ----------
        arr: np.ndarray of shape ``(Ny, Nx)``
            The labeled selection array
        data_obj: straditize.label_selection.LabelSelection
            The data object whose image shall be selected
        remove_selection: bool
            If True, remove the selection on apply
        """
        super(PatternSelectionWidget, self).__init__(*args, **kwargs)
        self.arr = arr
        self.data_obj = data_obj
        self.remove_selection = remove_selection
        self.template = None

        # the figure to show the template
        self.template_fig = EmbededMplCanvas()
        # the button to select the template
        self.btn_select_template = QPushButton('Select a template')
        self.btn_select_template.setCheckable(True)
        # the checkbox to allow fractions of the template
        self.fraction_box = QGroupBox('Template fractions')
        self.fraction_box.setCheckable(True)
        self.fraction_box.setChecked(False)
        self.fraction_box.setEnabled(False)
        self.sl_fraction = QSlider(Qt.Horizontal)
        self.lbl_fraction = QLabel('0.75')
        self.sl_fraction.setValue(75)
        # the slider to select the increments of the fractions
        self.sl_increments = QSlider(Qt.Horizontal)
        self.sl_increments.setValue(3)
        self.sl_increments.setMinimum(1)
        self.lbl_increments = QLabel('3')
        # the button to perform the correlation
        self.btn_correlate = QPushButton('Find template')
        self.btn_correlate.setEnabled(False)
        # the button to plot the correlation
        self.btn_plot_corr = QPushButton('Plot correlation')
        self.btn_plot_corr.setCheckable(True)
        self.btn_plot_corr.setEnabled(False)
        # slider for subselection
        self.btn_select = QPushButton('Select pattern')
        self.sl_thresh = QSlider(Qt.Horizontal)
        self.lbl_thresh = QLabel('0.5')

        self.btn_select.setCheckable(True)
        self.btn_select.setEnabled(False)
        self.sl_thresh.setValue(75)
        self.sl_thresh.setVisible(False)
        self.lbl_thresh.setVisible(False)

        # cancel and close button
        self.btn_cancel = QPushButton('Cancel')
        self.btn_close = QPushButton('Apply')
        self.btn_close.setEnabled(False)

        vbox = QVBoxLayout()

        vbox.addWidget(self.template_fig)
        hbox = QHBoxLayout()
        hbox.addStretch(0)
        hbox.addWidget(self.btn_select_template)
        vbox.addLayout(hbox)

        fraction_layout = QGridLayout()
        fraction_layout.addWidget(QLabel('Fraction'), 0, 0)
        fraction_layout.addWidget(self.sl_fraction, 0, 1)
        fraction_layout.addWidget(self.lbl_fraction, 0, 2)
        fraction_layout.addWidget(QLabel('Increments'), 1, 0)
        fraction_layout.addWidget(self.sl_increments, 1, 1)
        fraction_layout.addWidget(self.lbl_increments, 1, 2)

        self.fraction_box.setLayout(fraction_layout)

        vbox.addWidget(self.fraction_box)
        vbox.addWidget(self.btn_correlate)
        vbox.addWidget(self.btn_plot_corr)
        vbox.addWidget(self.btn_select)
        thresh_box = QHBoxLayout()
        thresh_box.addWidget(self.sl_thresh)
        thresh_box.addWidget(self.lbl_thresh)
        vbox.addLayout(thresh_box)

        hbox = QHBoxLayout()
        hbox.addWidget(self.btn_cancel)
        hbox.addWidget(self.btn_close)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        self.btn_select_template.clicked.connect(
            self.toggle_template_selection)
        self.sl_fraction.valueChanged.connect(
            lambda i: self.lbl_fraction.setText(str(i / 100.)))
        self.sl_increments.valueChanged.connect(
            lambda i: self.lbl_increments.setText(str(i)))
        self.btn_correlate.clicked.connect(self.start_correlation)
        self.btn_plot_corr.clicked.connect(self.toggle_correlation_plot)
        self.sl_thresh.valueChanged.connect(
            lambda i: self.lbl_thresh.setText(str((i - 50) / 50.)))
        self.sl_thresh.valueChanged.connect(self.modify_selection)
        self.btn_select.clicked.connect(self.toggle_selection)

        self.btn_cancel.clicked.connect(self.cancel)
        self.btn_close.clicked.connect(self.close)

    def toggle_template_selection(self):
        """Enable or disable the template selection"""
        if (not self.btn_select_template.isChecked()
                and self.selector is not None):
            self.selector.set_active(False)
            for a in self.selector.artists:
                a.set_visible(False)
            self.btn_select_template.setText('Select a template')
        elif self.selector is not None and self.template_im is not None:
            self.selector.set_active(True)
            for a in self.selector.artists:
                a.set_visible(True)
            self.btn_select_template.setText('Apply')
        else:
            self.selector = RectangleSelector(self.data_obj.ax,
                                              self.update_image,
                                              interactive=True)
            if self.template_extents is not None:
                self.selector.draw_shape(self.template_extents)
            self.key_press_cid = self.data_obj.ax.figure.canvas.mpl_connect(
                'key_press_event', self.update_image)
            self.btn_select_template.setText('Cancel')
        self.data_obj.draw_figure()
        if self.template is not None:
            self.fraction_box.setEnabled(True)
            self.sl_increments.setMaximum(min(self.template.shape[:2]))
            self.btn_correlate.setEnabled(True)

    def update_image(self, *args, **kwargs):
        """Update the template image based on the :attr:`selector` extents"""
        if self.template_im is not None:
            self.template_im.remove()
            del self.template_im
        elif self.axes is None:
            self.axes = self.template_fig.figure.add_subplot(111)
            self.template_fig.figure.subplots_adjust(bottom=0.3)
        if not self.selector.artists[0].get_visible():
            self.template_extents = None
            self.template = None
            self.btn_select_template.setText('Cancel')
        else:
            self.template_extents = np.round(self.selector.extents).astype(int)
            x, y = self.template_extents.reshape((2, 2))
            if getattr(self.data_obj, 'extent', None) is not None:
                extent = self.data_obj.extent
                x -= int(min(extent[:2]))
                y -= int(min(extent[2:]))
            slx = slice(*sorted(x))
            sly = slice(*sorted(y))
            self.template = template = self.arr[sly, slx]
            if template.ndim == 3:
                self.template_im = self.axes.imshow(template)
            else:
                self.template_im = self.axes.imshow(template, cmap='binary')
            self.btn_select_template.setText('Apply')
        self.template_fig.draw()

    def start_correlation(self):
        """Look for the correlations of template and source"""
        if self.fraction_box.isChecked():
            self._fraction = self.sl_fraction.value() / 100.
            increments = self.sl_increments.value()
        else:
            self._fraction = 0
            increments = 1
        corr = self.correlate_template(self.arr, self.template, self._fraction,
                                       increments)
        if corr is not None:
            self._correlation = corr
        enable = self._correlation is not None
        self.btn_plot_corr.setEnabled(enable)
        self.btn_select.setEnabled(enable)

    def toggle_selection(self):
        """Modifiy the selection (or not) based on the template correlation"""
        obj = self.data_obj
        if self.btn_select.isChecked():
            self._orig_selection_arr = obj._selection_arr.copy()
            self._selected_labels = obj.selected_labels
            self._select_cmap = obj._select_cmap
            self._select_norm = obj._select_norm
            self.btn_select.setText('Reset')
            self.btn_close.setEnabled(True)
            obj.unselect_all_labels()
            self.sl_thresh.setVisible(True)
            self.lbl_thresh.setVisible(True)
            self.modify_selection(self.sl_thresh.value())
        else:
            if obj._selection_arr is not None:
                obj._selection_arr[:] = self._orig_selection_arr
                obj._select_img.set_array(self._orig_selection_arr)
                obj.select_labels(self._selected_labels)
                obj._update_magni_img()
            del self._orig_selection_arr, self._selected_labels
            self.btn_select.setText('Select pattern')
            self.btn_close.setEnabled(False)
            self.sl_thresh.setVisible(False)
            self.lbl_thresh.setVisible(False)
            obj.draw_figure()

    def modify_selection(self, i):
        """Modify the selection based on the correlation threshold

        Parameters
        ----------
        i: int
            An integer between 0 and 100, the value of the :attr:`sl_thresh`
            slider"""
        if not self.btn_select.isChecked():
            return
        obj = self.data_obj
        val = (i - 50.) / 50.
        # select the values above 50
        if not self.remove_selection:
            # clear the selection
            obj._selection_arr[:] = obj._orig_selection_arr.copy()
            select_val = obj._selection_arr.max() + 1
            obj._selection_arr[self._correlation >= val] = select_val
        else:
            obj._selection_arr[:] = self._orig_selection_arr.copy()
            obj._selection_arr[self._correlation >= val] = -1
        obj._select_img.set_array(obj._selection_arr)
        obj._update_magni_img()
        obj.draw_figure()

    def correlate_template(self,
                           arr,
                           template,
                           fraction=False,
                           increment=1,
                           report=True):
        """Correlate a template with the `arr`

        This method uses the :func:`skimage.feature.match_template` function
        to find the given `template` in the source array `arr`.

        Parameters
        ----------
        arr: np.ndarray of shape ``(Ny,Nx)``
            The labeled selection array (see :attr:`arr`), the source of the
            given `template`
        template: np.ndarray of shape ``(nx, ny)``
            The template from ``arr`` that shall be searched
        fraction: float
            If not null, we will look through the given fraction of the
            template to look for partial matches as well
        increment: int
            The increment of the loop with the `fraction`.
        report: bool
            If True and `fraction` is not null, a QProgressDialog is opened
            to inform the user about the progress"""
        from skimage.feature import match_template
        mask = self.data_obj.selected_part
        x = mask.any(axis=0)
        if not x.any():
            raise ValueError("No data selected!")
        y = mask.any(axis=1)
        xmin = x.argmax()
        xmax = len(x) - x[::-1].argmax()
        ymin = y.argmax()
        ymax = len(y) - y[::-1].argmax()
        if arr.ndim == 3:
            mask = np.tile(mask[..., np.newaxis], (1, 1, arr.shape[-1]))
        src = np.where(mask[ymin:ymax, xmin:xmax], arr[ymin:ymax, xmin:xmax],
                       0)
        sny, snx = src.shape
        if not fraction:
            corr = match_template(src, template)
            full_shape = np.array(corr.shape)
        else:  # loop through the template to allow partial hatches
            shp = np.array(template.shape, dtype=int)[:2]
            ny, nx = shp
            fshp = np.round(fraction * shp).astype(int)
            fny, fnx = fshp
            it = list(
                product(range(0, fny, increment), range(0, fnx, increment)))
            ntot = len(it)
            full_shape = fshp - shp + src.shape
            corr = np.zeros(full_shape, dtype=float)
            if report:
                txt = 'Searching template...'
                dialog = QProgressDialog(txt, 'Cancel', 0, ntot)
                dialog.setWindowModality(Qt.WindowModal)
                t0 = dt.datetime.now()
            for k, (i, j) in enumerate(it):
                if report:
                    dialog.setValue(k)
                    if k and not k % 10:
                        passed = (dt.datetime.now() - t0).total_seconds()
                        dialog.setLabelText(txt + ' %1.0f seconds remaning' %
                                            ((passed * (ntot / k - 1.))))
                if report and dialog.wasCanceled():
                    return
                else:
                    y_end, x_start = fshp - (i, j) - 1
                    sly = slice(y_end, full_shape[0])
                    slx = slice(0, -x_start or full_shape[1])
                    corr[sly, slx] = np.maximum(
                        corr[sly, slx],
                        match_template(src, template[:-i or ny, j:]))
        ret = np.zeros_like(arr, dtype=corr.dtype)
        dny, dnx = src.shape - full_shape
        for i, j in product(range(dny + 1), range(dnx + 1)):
            ret[ymin + i:ymax - dny + i, xmin + j:xmax - dnx + j] = np.maximum(
                ret[ymin + i:ymax - dny + i, xmin + j:xmax - dnx + j], corr)
        return np.where(mask, ret, 0)

    def toggle_correlation_plot(self):
        """Toggle the correlation plot between :attr:`template` and :attr:`arr`
        """
        obj = self.data_obj
        if self._corr_plot is None:
            self._corr_plot = obj.ax.imshow(
                self._correlation,
                extent=obj._select_img.get_extent(),
                zorder=obj._select_img.zorder + 0.1)
            self._corr_cbar = obj.ax.figure.colorbar(self._corr_plot,
                                                     orientation='vertical')
            self._corr_cbar.set_label('Correlation')
        else:
            for a in [self._corr_cbar, self._corr_plot]:
                try:
                    a.remove()
                except ValueError:
                    pass
            del self._corr_plot, self._corr_cbar
        obj.draw_figure()

    def to_dock(self,
                main,
                title=None,
                position=None,
                docktype='df',
                *args,
                **kwargs):
        if position is None:
            position = main.dockWidgetArea(main.help_explorer.dock)
        connect = self.dock is None
        ret = super(PatternSelectionWidget, self).to_dock(main,
                                                          title,
                                                          position,
                                                          docktype=docktype,
                                                          *args,
                                                          **kwargs)
        if connect:
            self.dock.toggleViewAction().triggered.connect(self.maybe_tabify)
        return ret

    def maybe_tabify(self):
        main = self.dock.parent()
        if self.is_shown and main.dockWidgetArea(
                main.help_explorer.dock) == main.dockWidgetArea(self.dock):
            main.tabifyDockWidget(main.help_explorer.dock, self.dock)

    def cancel(self):
        if self.btn_select.isChecked():
            self.btn_select.setChecked(False)
            self.toggle_selection()
        self.close()

    def close(self):
        from psyplot_gui.main import mainwindow
        if self.selector is not None:
            self.selector.disconnect_events()
            for a in self.selector.artists:
                try:
                    a.remove()
                except ValueError:
                    pass
            self.data_obj.draw_figure()
            del self.selector
        if self._corr_plot is not None:
            self.toggle_correlation_plot()
        if self.key_press_cid is not None:
            self.data_obj.ax.figure.canvas.mpl_disconnect(self.key_press_cid)
        for attr in ['data_obj', 'arr', 'template', 'key_press_cid']:
            try:
                delattr(self, attr)
            except AttributeError:
                pass
        mainwindow.removeDockWidget(self.dock)
        return super(PatternSelectionWidget, self).close()
Пример #5
0
class DependenciesDialog(QDialog):
    """A dialog for displaying the dependencies"""

    #: description label
    label = None

    #: the QVBoxLayout containing all the widgets
    vbox = None

    #: The :class:`DependenciesTree` that contains the package infos
    tree = None

    #: The QPushButton used for copying selected packages to the clipboard
    bt_copy = None

    #: A simple info label for info messages
    info_label = None

    #: A QTimer that clears the :attr:`info_label` after some time
    timer = None

    @docstrings.dedent
    def __init__(self, versions, *args, **kwargs):
        """
        Parameters
        ----------
        %(DependenciesTree.parameters)s
        """
        super(DependenciesDialog, self).__init__(*args, **kwargs)
        self.setWindowTitle('Dependencies')
        self.versions = versions
        self.vbox = layout = QVBoxLayout()

        self.label = QLabel("""
            psyplot and the plugins depend on several python libraries. The
            tree widget below lists the versions of the plugins and the
            requirements. You can select the items in the tree and copy them to
            clipboard.""", parent=self)

        layout.addWidget(self.label)

        self.tree = DependenciesTree(versions, parent=self)
        self.tree.setSelectionMode(QAbstractItemView.MultiSelection)
        layout.addWidget(self.tree)

        # copy button
        self.bt_copy = QPushButton('Copy selection to clipboard')
        self.bt_copy.setToolTip(
            'Copy the selected packages in the above table to the clipboard.')
        self.bt_copy.clicked.connect(lambda: self.copy_selected())

        self.bbox = QDialogButtonBox(QDialogButtonBox.Ok)
        self.bbox.accepted.connect(self.accept)

        hbox = QHBoxLayout()
        hbox.addWidget(self.bt_copy)
        hbox.addStretch(1)
        hbox.addWidget(self.bbox)
        layout.addLayout(hbox)

        #: A label for simple status update
        self.info_label = QLabel('', self)
        layout.addWidget(self.info_label)
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.clear_label)

        self.setLayout(layout)

    def copy_selected(self, label=None):
        """Copy the selected versions and items to the clipboard"""
        d = {}
        items = self.tree.selectedItems()
        if not items:
            QMessageBox.warning(self, "No packages selected!",
                                "Please select packages in the tree!")
            return
        for item in items:
            d[item.text(0)] = item.text(1)
        if label is None:
            label = QApplication.clipboard()
        label.setText("\n".join(
            '%s: %s' % t for t in d.items()))
        self.info_label.setText('Packages copied to clipboard.')
        self.timer.start(3000)

    def clear_label(self):
        """Clear the info label"""
        self.info_label.setText('')
Пример #6
0
class SelectionToolbar(QToolBar, StraditizerControlBase):
    """A toolbar for selecting features in the straditizer and data image

    The current data object is set in the :attr:`combo` and can be accessed
    through the :attr:`data_obj` attribute. It's either the straditizer or the
    data_reader that is accessed"""

    _idPress = None

    _idRelease = None

    #: A signal that is emitted when something is selected
    selected = QtCore.pyqtSignal()

    set_cursor_id = None

    reset_cursor_id = None

    #: The QCombobox that defines the data object to be used
    combo = None

    @property
    def ax(self):
        """The :class:`matplotlib.axes.Axes` of the :attr:`data_obj`"""
        return self.data_obj.ax

    @property
    def data(self):
        """The np.ndarray of the :attr:`data_obj` image"""
        text = self.combo.currentText()
        if text == 'Reader':
            return self.straditizer.data_reader.binary
        elif text == 'Reader - Greyscale':
            return self.straditizer.data_reader.to_grey_pil(
                self.straditizer.data_reader.image)
        else:
            from straditize.binary import DataReader
            return DataReader.to_grey_pil(self.straditizer.image)

    @property
    def data_obj(self):
        """The data object as set in the :attr:`combo`.

        Either a :class:`~straditize.straditizer.Straditizer` or a
        :class:`straditize.binary.DataReader` instance. """
        text = self.combo.currentText()
        if text in ['Reader', 'Reader - Greyscale']:
            return self.straditizer.data_reader
        else:
            return self.straditizer

    @data_obj.setter
    def data_obj(self, value):
        """The data object as set in the :attr:`combo`.

        Either a :class:`~straditize.straditizer.Straditizer` or a
        :class:`straditize.binary.DataReader` instance. """
        if self.straditizer is None:
            return
        if isinstance(value, six.string_types):
            possible_values = {
                self.combo.itemText(i) for i in range(self.combo.count())}
            if value not in possible_values:
                raise ValueError(
                    'Do not understand %r! Please use one of %r' % (
                        value, possible_values))
            else:
                self.combo.setCurrentText(value)
        else:
            if value is self.straditizer:
                self.combo.setCurrentText('Straditizer')
            elif value and value is self.straditizer.data_reader:
                self.combo.setCurrentText('Reader')
            else:
                raise ValueError('Do not understand %r! Please either use '
                                 'the Straditizer or DataReader instance!' % (
                                     value, ))

    @property
    def fig(self):
        """The :class:`~matplotlib.figure.Figure` of the :attr:`data_obj`"""
        try:
            return self.ax.figure
        except AttributeError:
            return None

    @property
    def canvas(self):
        """The canvas of the :attr:`data_obj`"""
        try:
            return self.fig.canvas
        except AttributeError:
            return None

    @property
    def toolbar(self):
        """The toolbar of the :attr:`canvas`"""
        return self.canvas.toolbar

    @property
    def select_action(self):
        """The rectangle selection tool"""
        return self._actions['select']

    @property
    def wand_action(self):
        """The wand selection tool"""
        return self._actions['wand_select']

    @property
    def new_select_action(self):
        """The action to make new selection with one of the selection tools"""
        return self._type_actions['new_select']

    @property
    def add_select_action(self):
        """The action to add to the current selection with the selection tools
        """
        return self._type_actions['add_select']

    @property
    def remove_select_action(self):
        """
        An action to remove from the current selection with the selection tools
        """
        return self._type_actions['remove_select']

    @property
    def select_all_action(self):
        """An action to select all features in the :attr:`data`"""
        return self._actions['select_all']

    @property
    def expand_select_action(self):
        """An action to expand the current selection to the full feature"""
        return self._actions['expand_select']

    @property
    def invert_select_action(self):
        """An action to invert the current selection"""
        return self._actions['invert_select']

    @property
    def clear_select_action(self):
        """An action to clear the current selection"""
        return self._actions['clear_select']

    @property
    def select_right_action(self):
        """An action to select everything in the data column to the right"""
        return self._actions['select_right']

    @property
    def select_pattern_action(self):
        """An action to start a pattern selection"""
        return self._actions['select_pattern']

    @property
    def widgets2disable(self):
        if not self._actions:
            return []
        elif self._selecting:
            return [self.combo]
        else:
            return list(chain([self.combo],
                              self._actions.values(),
                              self._appearance_actions.values()))

    @property
    def labels(self):
        """The labeled data that is displayed"""
        if self.data_obj._selection_arr is not None:
            return self.data_obj._selection_arr
        text = self.combo.currentText()
        if text == 'Reader':
            return self.straditizer.data_reader.labels.copy()
        elif text == 'Reader - Greyscale':
            return self.straditizer.data_reader.color_labels()
        else:
            return self.straditizer.get_labels()

    @property
    def rect_callbacks(self):
        """The functions to call after the rectangle selection.

        If not set manually, it is the :meth:`select_rect` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept two arguments ``(slx, sly)``:
        the first one is the x-slice, and the second one the y-slice. They both
        correspond to the :attr:`data` attribute."""
        return self._rect_callbacks or [self.select_rect]

    @rect_callbacks.setter
    def rect_callbacks(self, value):
        """The functions to call after the rectangle selection.

        If not set manually, it is the :meth:`select_rect` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept two arguments ``(slx, sly)``:
        the first one is the x-slice, and the second one the y-slice. They both
        correspond to the :attr:`data` attribute."""
        self._rect_callbacks = value

    @property
    def poly_callbacks(self):
        """The functions to call after the polygon selection

        If not set manually, it is the :meth:`select_poly` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept one argument, a ``np.ndarray``
        of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates
        of the points of the polygon"""
        return self._poly_callbacks or [self.select_poly]

    @poly_callbacks.setter
    def poly_callbacks(self, value):
        """The functions to call after the polygon selection.

        If not set manually, it is the :meth:`poly_callbacks` method. Note that
        this is cleared at every call of the :meth:`end_selection`.

        Callables in this list must accept one argument, a ``np.ndarray``
        of shape ``(N, 2)``. This array defines the ``N`` x- and y-coordinates
        of the points of the polygon"""
        self._poly_callbacks = value

    #: A :class:`PointOrRectangleSelector` to select features in the image
    selector = None

    _pattern_selection = None

    def __init__(self, straditizer_widgets, *args, **kwargs):
        super(SelectionToolbar, self).__init__(*args, **kwargs)
        self._actions = {}
        self._wand_actions = {}
        self._pattern_actions = {}
        self._select_actions = {}
        self._appearance_actions = {}
        # Boolean that is True if we are in a selection process
        self._selecting = False
        self.init_straditizercontrol(straditizer_widgets)
        self._ids_select = []
        self._rect_callbacks = []
        self._poly_callbacks = []
        self._selection_mode = None
        self._lastCursor = None
        self.create_actions()
        self._changed_selection = False
        self._connected = []
        self._action_clicked = None
        self.wand_type = 'labels'
        self.select_type = 'rect'
        self.pattern_type = 'binary'
        self.auto_expand = False

    def create_actions(self):
        """Define the actions for the toolbar and set everything up"""
        # Reader toolbar
        self.combo = QComboBox()
        self.combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.addWidget(self.combo)

        select_group = QActionGroup(self)

        # select action
        self._actions['select'] = a = self.addAction(
            QIcon(get_icon('select.png')), 'select', self.toggle_selection)
        a.setToolTip('Select pixels within a rectangle')
        a.setCheckable(True)
        select_group.addAction(a)

        # select menu
        select_menu = QMenu(self)
        self._select_actions['rect_select'] = menu_a = select_menu.addAction(
            QIcon(get_icon('select.png')), 'rectangle',
            self.set_rect_select_mode)
        menu_a.setToolTip('Select a rectangle')
        a.setToolTip(menu_a.toolTip())

        self._select_actions['poly_select'] = menu_a = select_menu.addAction(
            QIcon(get_icon('poly_select.png')), 'polygon',
            self.set_poly_select_mode)
        menu_a.setToolTip('Select a rectangle')
        a.setToolTip(menu_a.toolTip())

        a.setMenu(select_menu)

        # wand_select action
        self._actions['wand_select'] = a = self.addAction(
            QIcon(get_icon('wand_select.png')), 'select',
            self.toggle_selection)
        a.setCheckable(True)
        select_group.addAction(a)

        # wand menu
        tool_menu = QMenu(self)
        self._wand_actions['wand_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('wand_select.png')), 'wand',
            self.set_label_wand_mode)
        menu_a.setToolTip('Select labels within a rectangle')
        a.setToolTip(menu_a.toolTip())

        self._wand_actions['color_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('color_select.png')), 'color wand',
            self.set_color_wand_mode)
        menu_a.setToolTip('Select colors')

        self._wand_actions['row_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('row_select.png')), 'row selection',
            self.set_row_wand_mode)
        menu_a.setToolTip('Select pixel rows')

        self._wand_actions['col_select'] = menu_a = tool_menu.addAction(
            QIcon(get_icon('col_select.png')), 'column selection',
            self.set_col_wand_mode)
        menu_a.setToolTip('Select pixel columns')

        a.setMenu(tool_menu)

        # color_wand widgets
        self.distance_slider = slider = QSlider(Qt.Horizontal)
        slider.setMinimum(0)
        slider.setMaximum(255)
        slider.setValue(30)
        slider.setSingleStep(1)

        self.lbl_slider = QLabel('30')
        slider.valueChanged.connect(lambda i: self.lbl_slider.setText(str(i)))
        slider.setMaximumWidth(self.combo.sizeHint().width())

        self.cb_whole_fig = QCheckBox('Whole plot')
        self.cb_whole_fig.setToolTip('Select the colors on the entire plot')

        self.cb_use_alpha = QCheckBox('Use alpha')
        self.cb_use_alpha.setToolTip('Use the alpha channel, i.e. the '
                                     'transparency of the RGBA image.')

        self.color_wand_actions = [
                self.addWidget(slider), self.addWidget(self.lbl_slider),
                self.addWidget(self.cb_whole_fig),
                self.addWidget(self.cb_use_alpha)]

        self.set_label_wand_mode()

        self.addSeparator()
        type_group = QActionGroup(self)

        self._type_actions = {}

        # new selection action
        self._type_actions['new_select'] = a = self.addAction(
            QIcon(get_icon('new_selection.png')), 'Create a new selection')
        a.setToolTip('Select pixels within a rectangle and ignore the current '
                     'selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # add to selection action
        self._type_actions['add_select'] = a = self.addAction(
            QIcon(get_icon('add_select.png')), 'Add to selection')
        a.setToolTip('Select pixels within a rectangle and add them to the '
                     'current selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # remove action
        self._type_actions['remove_select'] = a = self.addAction(
            QIcon(get_icon('remove_select.png')), 'Remove from selection')
        a.setToolTip('Select pixels within a rectangle and remove them from '
                     'the current selection')
        a.setCheckable(True)
        type_group.addAction(a)

        # info button
        self.addSeparator()
        self.info_button = InfoButton(self, 'selection_toolbar.rst')
        self.addWidget(self.info_button)

        # selection appearence options
        self.addSeparator()
        self.sl_alpha = slider = QSlider(Qt.Horizontal)
        self._appearance_actions['alpha'] = self.addWidget(slider)
        slider.setMinimum(0)
        slider.setMaximum(100)
        slider.setValue(100)
        slider.setSingleStep(1)

        self.lbl_alpha_slider = QLabel('100 %')
        slider.valueChanged.connect(
            lambda i: self.lbl_alpha_slider.setText(str(i) + ' %'))
        slider.valueChanged.connect(self.update_alpha)
        slider.setMaximumWidth(self.combo.sizeHint().width())

        # Select all and invert selection buttons
        self.addSeparator()
        self._actions['select_all'] = a = self.addAction(
            QIcon(get_icon('select_all.png')), 'all', self.select_all)
        a.setToolTip('Select all labels')

        self._actions['expand_select'] = a = self.addAction(
            QIcon(get_icon('expand_select.png')), 'expand',
            self.expand_selection)
        a.setToolTip('Expand the selected areas to select the entire feature')

        self._actions['invert_select'] = a = self.addAction(
            QIcon(get_icon('invert_select.png')), 'invert',
            self.invert_selection)
        a.setToolTip('Invert selection')

        self._actions['clear_select'] = a = self.addAction(
            QIcon(get_icon('clear_select.png')), 'clear',
            self.clear_selection)
        a.setToolTip('Clear selection')

        self._actions['select_right'] = a = self.addAction(
            QIcon(get_icon('select_right.png')), 'right',
            self.select_everything_to_the_right)
        a.setToolTip('Select everything to the right of each column')

        self._actions['select_pattern'] = a = self.addAction(
            QIcon(get_icon('pattern.png')), 'pattern',
            self.start_pattern_selection)
        a.setCheckable(True)
        a.setToolTip(
            'Select a binary pattern/hatch within the current selection')

        # wand menu
        pattern_menu = QMenu(self)
        self._pattern_actions['binary'] = menu_a = pattern_menu.addAction(
            QIcon(get_icon('pattern.png')), 'Binary',
            self.set_binary_pattern_mode)
        menu_a.setToolTip(
            'Select a binary pattern/hatch within the current selection')
        a.setToolTip(menu_a.toolTip())

        self._pattern_actions['grey'] = menu_a = pattern_menu.addAction(
            QIcon(get_icon('pattern_grey.png')), 'Greyscale',
            self.set_grey_pattern_mode)
        menu_a.setToolTip(
            'Select a pattern/hatch within the current selection based on '
            'grey scale colors')

        a.setMenu(pattern_menu)

        self.new_select_action.setChecked(True)
        for a in self._type_actions.values():
            a.toggled.connect(self.add_or_remove_pattern)

        self.refresh()

    def should_be_enabled(self, w):
        if self.straditizer is None:
            return False
        elif (self._actions and
              w in [self.remove_select_action, self.invert_select_action,
                    self.clear_select_action, self.expand_select_action,
                    self.select_right_action] and
              not self._selecting):
            return False
        elif w in self._appearance_actions.values() and not self._selecting:
            return False
        elif (self.combo and not self.combo.currentText().startswith('Reader')
              and w is self.select_right_action):
            return False
        return True

    def disable_actions(self):
        if self._changed_selection:
            return
        for a in self._actions.values():
            if a.isChecked():
                a.setChecked(False)
                self.toggle_selection()
            else:
                a.setChecked(False)

    def select_all(self):
        """Select all features in the image

        See Also
        --------
        straditize.label_selection.LabelSelection.select_all_labels"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        obj.select_all_labels()
        self.canvas.draw()

    def invert_selection(self):
        """Invert the current selection"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        if (obj._selection_arr != obj._orig_selection_arr).any():
            selection = obj.selected_part

            # clear the current selection
            obj._selection_arr[:] = np.where(
                obj._selection_arr.astype(bool) & (~selection),
                obj._orig_selection_arr.max() + 1, obj._orig_selection_arr)
            obj._select_img.set_array(obj._selection_arr)
            obj.unselect_all_labels()
        else:
            obj.select_all_other_labels()
        self.canvas.draw()

    def clear_selection(self):
        """Clear the current selection"""
        obj = self.data_obj
        if obj._selection_arr is None:
            return
        obj._selection_arr[:] = obj._orig_selection_arr.copy()
        obj._select_img.set_array(obj._selection_arr)
        obj.unselect_all_labels()
        self.canvas.draw()

    def expand_selection(self):
        """Expand the selected areas to select the full labels"""
        obj = self.data_obj
        if obj._selection_arr is None:
            return
        arr = obj._orig_selection_arr.copy()
        selected_labels = np.unique(arr[obj.selected_part])
        obj._selection_arr = arr
        obj._select_img.set_array(arr)
        obj.unselect_all_labels()
        obj.select_labels(selected_labels)
        self.canvas.draw()

    def update_alpha(self, i):
        """Set the transparency of the selection image

        Parameters
        ----------
        i: int
            The transparency between 0 and 100"""
        self.data_obj._select_img.set_alpha(i / 100.)
        self.data_obj._update_magni_img()
        self.canvas.draw()

    def select_everything_to_the_right(self):
        """Selects everything to the right of the current selection"""
        reader = self.data_obj
        if reader._selection_arr is None:
            return
        bounds = reader.column_bounds
        selection = reader.selected_part
        new_select = np.zeros_like(selection)
        for start, end in bounds:
            can_be_selected = reader._selection_arr[:, start:end].astype(bool)
            end = start + can_be_selected.shape[1]
            last_in_row = selection[:, start:end].argmax(axis=-1).reshape(
                (-1, 1))
            dist2start = np.tile(np.arange(end - start)[np.newaxis],
                                 (len(selection), 1))
            can_be_selected[dist2start <= last_in_row] = False
            can_be_selected[~np.tile(last_in_row.astype(bool),
                                     (1, end - start))] = False
            new_select[:, start:end] = can_be_selected
        max_label = reader._orig_selection_arr.max()
        reader._selection_arr[new_select] = max_label + 1
        reader._select_img.set_array(reader._selection_arr)
        reader._update_magni_img()
        self.canvas.draw()

    def start_pattern_selection(self):
        """Open the pattern selection dialog

        This method will enable the pattern selection by starting a
        :class:`straditize.widgets.pattern_selection.PatternSelectionWidget`"""
        from straditize.widgets.pattern_selection import PatternSelectionWidget
        if self.select_pattern_action.isChecked():
            from straditize.binary import DataReader
            from psyplot_gui.main import mainwindow
            obj = self.data_obj
            if obj._selection_arr is None:
                if hasattr(obj, 'image_array'):
                    rgba = obj.image_array()
                else:
                    rgba = None
                self.start_selection(self.labels, rgba=rgba)
                self.select_all()
            if not obj.selected_part.any():
                self.select_pattern_action.setChecked(False)
                raise ValueError(
                    "No data in the image is selected. Please select the "
                    "coarse region in which the pattern should be searched.")
            if self.pattern_type == 'binary':
                arr = DataReader.to_binary_pil(obj.image)
            else:
                arr = DataReader.to_grey_pil(obj.image)
            self._pattern_selection = w = PatternSelectionWidget(
                arr, obj)
            w.to_dock(mainwindow, 'Pattern selection')
            w.btn_close.clicked.connect(self.uncheck_pattern_selection)
            w.btn_cancel.clicked.connect(self.uncheck_pattern_selection)
            self.disable_actions()
            pattern_action = self.select_pattern_action
            for a in self._actions.values():
                a.setEnabled(False)
            pattern_action.setEnabled(True)
            pattern_action.setChecked(True)
            w.show_plugin()
            w.maybe_tabify()
            w.raise_()
        elif self._pattern_selection is not None:
            self._pattern_selection.cancel()
            self._pattern_selection.close()
            self.uncheck_pattern_selection()
            del self._pattern_selection

    def uncheck_pattern_selection(self):
        """Disable the pattern selection"""
        self.select_pattern_action.setChecked(False)
        del self._pattern_selection
        for a in self._actions.values():
            a.setEnabled(self.should_be_enabled(a))

    def add_or_remove_pattern(self):
        """Enable the removing or adding of the pattern selection"""
        if getattr(self, '_pattern_selection', None) is None:
            return
        current = self._pattern_selection.remove_selection
        new = self.remove_select_action.isChecked()
        if new is not current:
            self._pattern_selection.remove_selection = new
            if self._pattern_selection.btn_select.isChecked():
                self._pattern_selection.modify_selection(
                    self._pattern_selection.sl_thresh.value())

    def set_rect_select_mode(self):
        """Set the current wand tool to the color wand"""
        self.select_type = 'rect'
        self.select_action.setIcon(QIcon(get_icon('select.png')))
        self._action_clicked = None
        self.toggle_selection()

    def set_poly_select_mode(self):
        """Set the current wand tool to the color wand"""
        self.select_type = 'poly'
        self.select_action.setIcon(QIcon(get_icon('poly_select.png')))
        self._action_clicked = None
        self.toggle_selection()

    def set_label_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'labels'
        self.wand_action.setIcon(QIcon(get_icon('wand_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_color_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'color'
        self.wand_action.setIcon(QIcon(get_icon('color_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(True)
        self._action_clicked = None
        self.toggle_selection()

    def set_row_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'rows'
        self.wand_action.setIcon(QIcon(get_icon('row_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_col_wand_mode(self):
        """Set the current wand tool to the color wand"""
        self.wand_type = 'cols'
        self.wand_action.setIcon(QIcon(get_icon('col_select.png')))
        for a in self.color_wand_actions:
            a.setVisible(False)
        self._action_clicked = None
        self.toggle_selection()

    def set_binary_pattern_mode(self):
        """Set the current pattern mode to the binary pattern"""
        self.pattern_type = 'binary'
        self.select_pattern_action.setIcon(QIcon(get_icon('pattern.png')))

    def set_grey_pattern_mode(self):
        """Set the current pattern mode to the binary pattern"""
        self.pattern_type = 'grey'
        self.select_pattern_action.setIcon(QIcon(get_icon('pattern_grey.png')))

    def disconnect(self):
        if self.set_cursor_id is not None:
            if self.canvas is None:
                self.canvas.mpl_disconnect(self.set_cursor_id)
                self.canvas.mpl_disconnect(self.reset_cursor_id)
            self.set_cursor_id = None
            self.reset_cursor_id = None

        if self.selector is not None:
            self.selector.disconnect_events()
            self.selector = None

    def toggle_selection(self):
        """Activate selection mode"""
        if self.canvas is None:
            return
        self.disconnect()

        key = next((key for key, a in self._actions.items() if a.isChecked()),
                   None)
        if key is None or key == self._action_clicked:
            self._action_clicked = None
            if key is not None:
                self._actions[key].setChecked(False)
        else:
            if self.wand_action.isChecked() and self.wand_type == 'color':
                self.selector = PointOrRectangleSelector(
                    self.ax, self.on_rect_select, rectprops=dict(fc='none'),
                    lineprops=dict(c='none'), useblit=True)
            elif self.select_action.isChecked() and self.select_type == 'poly':
                self.selector = mwid.LassoSelector(
                    self.ax, self.on_poly_select)
            else:
                self.selector = PointOrRectangleSelector(
                    self.ax, self.on_rect_select, useblit=True)
            self.set_cursor_id = self.canvas.mpl_connect(
                'axes_enter_event', self._on_axes_enter)
            self.reset_cursor_id = self.canvas.mpl_connect(
                'axes_leave_event', self._on_axes_leave)
            self._action_clicked = next(key for key, a in self._actions.items()
                                        if a.isChecked())

        self.toolbar.set_message(self.toolbar.mode)

    def enable_or_disable_widgets(self, b):
        super(SelectionToolbar, self).enable_or_disable_widgets(b)
        if not b:
            for w in [self.clear_select_action, self.invert_select_action,
                      self.expand_select_action]:
                w.setEnabled(self.should_be_enabled(w))
        if self._actions and not self.select_action.isEnabled():
            for a in self._actions.values():
                if a.isChecked():
                    a.setChecked(False)
                    self.toggle_selection()

    def refresh(self):
        super(SelectionToolbar, self).refresh()
        combo = self.combo
        if self.straditizer is None:
            combo.clear()
        else:
            if not combo.count():
                combo.addItem('Straditizer')
            if self.straditizer.data_reader is not None:
                if not any(combo.itemText(i) == 'Reader'
                           for i in range(combo.count())):
                    combo.addItem('Reader')
                    combo.addItem('Reader - Greyscale')
            else:
                for i in range(combo.count()):
                    if combo.itemText(i).startswith('Reader'):
                        combo.removeItem(i)

    def _on_axes_enter(self, event):
        ax = self.ax
        if ax is None:
            return
        if (event.inaxes is ax and self.toolbar._active == '' and
                self.selector is not None):
            if self._lastCursor != cursors.SELECT_REGION:
                self.toolbar.set_cursor(cursors.SELECT_REGION)
                self._lastCursor = cursors.SELECT_REGION

    def _on_axes_leave(self, event):
        ax = self.ax
        if ax is None:
            return
        if (event.inaxes is ax and self.toolbar._active == '' and
                self.selector is not None):
            if self._lastCursor != cursors.POINTER:
                self.toolbar.set_cursor(cursors.POINTER)
                self._lastCursor = cursors.POINTER

    def end_selection(self):
        """Finish the selection and disconnect everything"""
        if getattr(self, '_pattern_selection', None) is not None:
            self._pattern_selection.close()
            del self._pattern_selection
        self._selecting = False
        self._action_clicked = None
        self.toggle_selection()
        self.auto_expand = False
        self._labels = None
        self._rect_callbacks.clear()
        self._poly_callbacks.clear()
        self._wand_actions['color_select'].setEnabled(True)

    def get_xy_slice(self, lastx, lasty, x, y):
        """Transform x- and y-coordinates to :class:`slice` objects

        Parameters
        ----------
        lastx: int
            The initial x-coordinate
        lasty: int
            The initial y-coordinate
        x: int
            The final x-coordinate
        y: int
            The final y-coordinate

        Returns
        -------
        slice
            The ``slice(lastx, x)``
        slice
            The ``slice(lasty, y)``"""
        all_x = np.floor(np.sort([lastx, x])).astype(int)
        all_y = np.floor(np.sort([lasty, y])).astype(int)
        extent = getattr(self.data_obj, 'extent', None)
        if extent is not None:
            all_x -= np.ceil(extent[0]).astype(int)
            all_y -= np.ceil(min(extent[2:])).astype(int)
        if self.wand_action.isChecked() and self.wand_type == 'color':
            all_x[0] = all_x[1]
            all_y[0] = all_y[1]
        all_x[all_x < 0] = 0
        all_y[all_y < 0] = 0
        all_x[1] += 1
        all_y[1] += 1
        return slice(*all_x), slice(*all_y)

    def on_rect_select(self, e0, e1):
        """Call the :attr:`rect_callbacks` after a rectangle selection

        Parameters
        ----------
        e0: matplotlib.backend_bases.Event
            The initial event
        e1: matplotlib.backend_bases.Event
            The final event"""
        slx, sly = self.get_xy_slice(e0.xdata, e0.ydata, e1.xdata, e1.ydata)
        for func in self.rect_callbacks:
            func(slx, sly)

    def select_rect(self, slx, sly):
        """Select the data defined by a rectangle

        Parameters
        ----------
        slx: slice
            The x-slice of the rectangle
        sly: slice
            The y-slice of the rectangle

        See Also
        --------
        rect_callbacks"""
        obj = self.data_obj
        if obj._selection_arr is None:
            arr = self.labels
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(arr, rgba=rgba)
        expand = False
        if self.select_action.isChecked():
            arr = self._select_rectangle(slx, sly)
            expand = True
        elif self.wand_type == 'labels':
            arr = self._select_labels(slx, sly)
        elif self.wand_type == 'rows':
            arr = self._select_rows(slx, sly)
        elif self.wand_type == 'cols':
            arr = self._select_cols(slx, sly)
        else:
            arr = self._select_colors(slx, sly)
            expand = True
        if arr is not None:
            obj._selection_arr = arr
            obj._select_img.set_array(arr)
            obj._update_magni_img()
            if expand and self.auto_expand:
                self.expand_selection()
            else:
                self.canvas.draw()

    def on_poly_select(self, points):
        """Call the :attr:`poly_callbacks` after a polygon selection

        Parameters
        ----------
        e0: matplotlib.backend_bases.Event
            The initial event
        e1: matplotlib.backend_bases.Event
            The final event"""
        for func in self.poly_callbacks:
            func(points)

    def select_poly(self, points):
        """Select the data defined by a polygon

        Parameters
        ----------
        points: np.ndarray of shape (N, 2)
            The x- and y-coordinates of the vertices of the polygon

        See Also
        --------
        poly_callbacks"""
        obj = self.data_obj
        if obj._selection_arr is None:
            rgba = obj.image_array() if hasattr(obj, 'image_array') else None
            self.start_selection(self.labels, rgba=rgba)
        arr = self.labels
        mpath = mplp.Path(points)
        x = np.arange(obj._selection_arr.shape[1], dtype=int)
        y = np.arange(obj._selection_arr.shape[0], dtype=int)
        extent = getattr(obj, 'extent', None)
        if extent is not None:
            x += np.ceil(extent[0]).astype(int)
            y += np.ceil(min(extent[2:])).astype(int)
        pointsx, pointsy = np.array(points).T
        x0, x1 = x.searchsorted([pointsx.min(), pointsx.max()])
        y0, y1 = y.searchsorted([pointsy.min(), pointsy.max()])
        X, Y = np.meshgrid(x[x0:x1], y[y0:y1])
        points = np.array((X.flatten(), Y.flatten())).T
        mask = np.zeros_like(obj._selection_arr, dtype=bool)
        mask[y0:y1, x0:x1] = (
            mpath.contains_points(points).reshape(X.shape) &
            obj._selection_arr[y0:y1, x0:x1].astype(bool))
        if self.remove_select_action.isChecked():
            arr[mask] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[mask] = arr.max() + 1
        obj._selection_arr = arr
        obj._select_img.set_array(arr)
        obj._update_magni_img()
        if self.auto_expand:
            self.expand_selection()
        else:
            self.canvas.draw()

    def _select_rectangle(self, slx, sly):
        """Select a rectangle within the array"""
        obj = self.data_obj
        arr = self.labels
        data_mask = obj._selection_arr.astype(bool)
        if self.remove_select_action.isChecked():
            arr[sly, slx][data_mask[sly, slx]] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[sly, slx][data_mask[sly, slx]] = arr.max() + 1
        return arr

    def _select_labels(self, slx, sly):
        """Select the unique labels in the array"""
        obj = self.data_obj
        arr = self.labels
        data_mask = obj._selection_arr.astype(bool)
        current_selected = obj.selected_labels
        new_selected = np.unique(
            arr[sly, slx][data_mask[sly, slx]])
        valid_labels = np.unique(
            obj._orig_selection_arr[sly, slx][data_mask[sly, slx]])
        valid_labels = valid_labels[valid_labels > 0]
        if not len(valid_labels):
            return
        if new_selected[0] == -1 or new_selected[-1] > obj._select_nlabels:
            mask = np.isin(obj._orig_selection_arr, valid_labels)
            current_selected = np.unique(
                np.r_[current_selected,
                      obj._orig_selection_arr[sly, slx][
                          arr[sly, slx] > obj._select_nlabels]])
            arr[mask] = obj._orig_selection_arr[mask]
        curr = set(current_selected)
        valid = set(valid_labels)
        if self.remove_select_action.isChecked():
            new = curr - valid
        elif self.add_select_action.isChecked():
            new = curr | valid
        else:
            new = valid
            arr = obj._orig_selection_arr.copy()
        obj.select_labels(np.array(sorted(new)))
        return arr

    def _select_rows(self, slx, sly):
        """Select the pixel rows defined by `sly`

        Parameters
        ----------
        slx: slice
            The x-slice (is ignored)
        sly: slice
            The y-slice defining the rows to select"""
        obj = self.data_obj
        arr = self.labels
        rows = np.arange(arr.shape[0])[sly]
        if self.remove_select_action.isChecked():
            arr[rows, :] = np.where(arr[rows, :], -1, 0)
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[rows, :] = np.where(arr[rows, :], arr.max() + 1, 0)
        return arr

    def _select_cols(self, slx, sly):
        """Select the pixel columns defined by `slx`

        Parameters
        ----------
        slx: slice
            The x-slice defining the columns to select
        sly: slice
            The y-slice (is ignored)"""
        obj = self.data_obj
        arr = self.labels
        cols = np.arange(arr.shape[1])[slx]
        if self.remove_select_action.isChecked():
            arr[:, cols] = np.where(arr[:, cols], -1, 0)
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[:, cols] = np.where(arr[:, cols], arr.max() + 1, 0)
        return arr

    def _select_colors(self, slx, sly):
        """Select the array based on the colors"""
        if self.cb_use_alpha.isChecked():
            rgba = self._rgba
            n = 4
        else:
            rgba = self._rgba[..., :-1]
            n = 3
        rgba = rgba.astype(int)
        # get the unique colors
        colors = list(
            map(np.array, set(map(tuple, rgba[sly, slx].reshape((-1, n))))))
        obj = self.data_obj
        arr = self.labels
        mask = np.zeros_like(arr, dtype=bool)
        max_dist = self.distance_slider.value()
        data_mask = obj._selection_arr.astype(bool)
        for c in colors:
            mask[np.all(np.abs(rgba - c.reshape((1, 1, -1))) <= max_dist,
                        axis=-1)] = True
        if not self.cb_whole_fig.isChecked():
            import skimage.morphology as skim
            all_labels = skim.label(mask, 8, return_num=False)
            selected_labels = np.unique(all_labels[sly, slx])
            mask[~np.isin(all_labels, selected_labels)] = False
        if self.remove_select_action.isChecked():
            arr[mask & data_mask] = -1
        else:
            if self.new_select_action.isChecked():
                arr = obj._orig_selection_arr.copy()
                obj._select_img.set_cmap(obj._select_cmap)
                obj._select_img.set_norm(obj._select_norm)
            arr[mask & data_mask] = arr.max() + 1
        return arr

    def _remove_selected_labels(self):
        self.data_obj.remove_selected_labels(disable=True)

    def _disable_selection(self):
            return self.data_obj.disable_label_selection()

    def start_selection(self, arr=None, rgba=None,
                        rect_callbacks=None, poly_callbacks=None,
                        apply_funcs=(), cancel_funcs=(), remove_on_apply=True):
        """Start the selection in the current :attr:`data_obj`

        Parameters
        ----------
        arr: np.ndarray
            The labeled selection array that is used. If specified, the
            :meth:`~straditize.label_selection.enable_label_selection` method
            is called of the :attr:`data_obj` with the given `arr`. If this
            parameter is ``None``, then we expect that this method has already
            been called
        rgba: np.ndarray
            The RGBA image that shall be used for the color selection
            (see the :meth:`set_color_wand_mode`)
        rect_callbacks: list
            A list of callbacks that shall be called after a rectangle
            selection has been made by the user (see :attr:`rect_callbacks`)
        poly_callbacks: list
            A list of callbacks that shall be called after a polygon
            selection has been made by the user (see :attr:`poly_callbacks`)
        apply_funcs: list
            A list of callables that shall be connected to the
            :attr:`~straditize.widgets.StraditizerWidgets.apply_button`
        cancel_funcs: list
            A list of callables that shall be connected to the
            :attr:`~straditize.widgets.StraditizerWidgets.cancel_button`
        remove_on_apply: bool
            If True and the
            :attr:`~straditize.widgets.StraditizerWidgets.apply_button` is
            clicked, the selected labels will be removed."""
        obj = self.data_obj
        if arr is not None:
            obj.enable_label_selection(
                arr, arr.max(), set_picker=False,
                zorder=obj.plot_im.zorder + 0.1,
                extent=obj.plot_im.get_extent())
        self._selecting = True
        self._rgba = rgba
        if rgba is None:
            self.set_label_wand_mode()
            self._wand_actions['color_select'].setEnabled(False)
        else:
            self._wand_actions['color_select'].setEnabled(True)
        self.connect2apply(
            (self._remove_selected_labels if remove_on_apply else
             self._disable_selection),
            obj.remove_small_selection_ellipses, obj.draw_figure,
            self.end_selection, *apply_funcs)
        self.connect2cancel(self._disable_selection,
                            obj.remove_small_selection_ellipses,
                            obj.draw_figure,
                            self.end_selection, *cancel_funcs)
        if self.should_be_enabled(self._appearance_actions['alpha']):
            self.update_alpha(self.sl_alpha.value())
        for w in chain(self._actions.values(),
                       self._appearance_actions.values()):
            w.setEnabled(self.should_be_enabled(w))
        if remove_on_apply:
            self.straditizer_widgets.apply_button.setText('Remove')
        if rect_callbacks is not None:
            self._rect_callbacks = rect_callbacks[:]
        if poly_callbacks is not None:
            self._poly_callbacks = poly_callbacks[:]
        del obj