Exemplo n.º 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)
Exemplo n.º 2
0
class StraditizerWidgets(QWidget, DockMixin):
    """A widget that contains widgets to control the straditization in a GUI

    This widget is the basis of the straditize GUI and implemented as a
    plugin into the psyplot gui. The open straditizers are handled in the
    :attr:`_straditizer` attribute.

    The central parts of this widget are

    - The combobox to manage the open straditizers
    - The QTreeWidget in the :attr:`tree` attribute that contains all the
      controls to interface the straditizer
    - the tutorial area
    - the :guilabel:`Apply` and :guilabel:`Cancel` button"""

    #: Boolean that is True if all dialogs should be answered with `Yes`
    always_yes = False

    #: The QTreeWidget that contains the different widgets for the digitization
    tree = None

    #: The apply button
    apply_button = None

    #: The cancel button
    cancel_button = None

    #: The button to edit the straditizer attributes
    attrs_button = None

    #: The button to start a tutorial
    tutorial_button = None

    #: An :class:`InfoButton` to display the docs
    info_button = None

    #: A QComboBox to select the current straditizer
    stradi_combo = None

    #: A button to open a new straditizer
    btn_open_stradi = None

    #: A button to close the current straditizer
    btn_close_stradi = None

    #: A button to reload the last autosaved state
    btn_reload_autosaved = None

    #: The :class:`straditize.widgets.progress_widget.ProgressWidget` to
    #: display the progress of the straditization
    progress_widget = None

    #: The :class:`straditize.widgets.data.DigitizingControl` to interface
    #: the :straditize.straditizer.Straditizer.data_reader`
    digitizer = None

    #: The :class:`straditize.widgets.colnames.ColumnNamesManager` to interface
    #: the :straditize.straditizer.Straditizer.colnames_reader`
    colnames_manager = None

    #: The :class:`straditize.widgets.axes_translations.AxesTranslations` to
    #: handle the y- and x-axis conversions
    axes_translations = None

    #: The :class:`straditize.widgets.image_correction.ImageRescaler` class to
    #: rescale the image
    image_rescaler = None

    #: The :class:`straditize.widgets.image_correction.ImageRotator` class to
    #: rotate the image
    image_rotator = None

    #: The :class:`straditize.widgets.plots.PlotControl` to display additional
    #: information on the diagram
    plot_control = None

    #: The :class:`straditize.widgets.marker_control.MarkerControl` to modify
    #: the appearance of the :class:`~straditize.straditizer.Straditizer.marks`
    #: of the current straditizer
    marker_control = None

    #: The :class:`straditize.widgets.selection_toolbar.SelectionToolbar` to
    #: select features in the stratigraphic diagram
    selection_toolbar = None

    #: The :class:`straditize.straditizer.Straditizer` instance
    straditizer = None

    #: open straditizers
    _straditizers = []

    #: The :class:`straditize.widgets.tutorial.Tutorial` class
    tutorial = None

    dock_position = Qt.LeftDockWidgetArea

    #: Auto-saved straditizers
    autosaved = []

    hidden = True

    title = 'Stratigraphic diagram digitization'

    window_layout_action = None

    open_external = QtCore.pyqtSignal(list)

    def __init__(self, *args, **kwargs):
        from straditize.widgets.menu_actions import StraditizerMenuActions
        from straditize.widgets.progress_widget import ProgressWidget
        from straditize.widgets.data import DigitizingControl
        from straditize.widgets.selection_toolbar import SelectionToolbar
        from straditize.widgets.marker_control import MarkerControl
        from straditize.widgets.plots import PlotControl
        from straditize.widgets.axes_translations import AxesTranslations
        from straditize.widgets.image_correction import (ImageRotator,
                                                         ImageRescaler)
        from straditize.widgets.colnames import ColumnNamesManager
        self._straditizers = []
        super(StraditizerWidgets, self).__init__(*args, **kwargs)
        self.tree = QTreeWidget(parent=self)
        self.tree.setSelectionMode(QTreeWidget.NoSelection)
        self.refresh_button = QToolButton(self)
        self.refresh_button.setIcon(QIcon(get_psy_icon('refresh.png')))
        self.refresh_button.setToolTip('Refresh from the straditizer')
        self.apply_button = EnableButton('Apply', parent=self)
        self.cancel_button = EnableButton('Cancel', parent=self)
        self.attrs_button = QPushButton('Attributes', parent=self)
        self.tutorial_button = QPushButton('Tutorial', parent=self)
        self.tutorial_button.setCheckable(True)
        self.error_msg = PyErrorMessage(self)
        self.stradi_combo = QComboBox()
        self.btn_open_stradi = QToolButton()
        self.btn_open_stradi.setIcon(QIcon(get_psy_icon('run_arrow.png')))
        self.btn_close_stradi = QToolButton()
        self.btn_close_stradi.setIcon(QIcon(get_psy_icon('invalid.png')))
        self.btn_reload_autosaved = QPushButton("Reload")
        self.btn_reload_autosaved.setToolTip(
            "Close the straditizer and reload the last autosaved project")

        # ---------------------------------------------------------------------
        # --------------------------- Tree widgets ----------------------------
        # ---------------------------------------------------------------------
        self.tree.setHeaderLabels(['', ''])
        self.tree.setColumnCount(2)

        self.progress_item = QTreeWidgetItem(0)
        self.progress_item.setText(0, 'ToDo list')
        self.progress_widget = ProgressWidget(self, self.progress_item)

        self.menu_actions_item = QTreeWidgetItem(0)
        self.menu_actions_item.setText(0, 'Images import/export')
        self.tree.addTopLevelItem(self.menu_actions_item)
        self.menu_actions = StraditizerMenuActions(self)

        self.digitizer_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Digitization control')
        self.digitizer = DigitizingControl(self, item)

        self.col_names_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Column names')
        self.colnames_manager = ColumnNamesManager(self, item)
        self.add_info_button(item, 'column_names.rst')

        self.axes_translations_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Axes translations')
        self.axes_translations = AxesTranslations(self, item)

        self.image_transform_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Transform source image')

        self.image_rescaler = ImageRescaler(self, item)

        self.image_rotator_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Rotate image')
        self.image_rotator = ImageRotator(self)
        self.image_transform_item.addChild(item)
        self.image_rotator.setup_children(item)

        self.plot_control_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Plot control')
        self.plot_control = PlotControl(self, item)
        self.add_info_button(item, 'plot_control.rst')

        self.marker_control_item = item = QTreeWidgetItem(0)
        item.setText(0, 'Marker control')
        self.marker_control = MarkerControl(self, item)
        self.add_info_button(item, 'marker_control.rst')

        # ---------------------------------------------------------------------
        # ----------------------------- Toolbars ------------------------------
        # ---------------------------------------------------------------------
        self.selection_toolbar = SelectionToolbar(self, 'Selection toolbar')

        # ---------------------------------------------------------------------
        # ----------------------------- InfoButton ----------------------------
        # ---------------------------------------------------------------------
        self.info_button = InfoButton(self, get_doc_file('straditize.rst'))

        # ---------------------------------------------------------------------
        # --------------------------- Layouts ---------------------------------
        # ---------------------------------------------------------------------

        stradi_box = QHBoxLayout()
        stradi_box.addWidget(self.stradi_combo, 1)
        stradi_box.addWidget(self.btn_open_stradi)
        stradi_box.addWidget(self.btn_close_stradi)

        attrs_box = QHBoxLayout()
        attrs_box.addWidget(self.attrs_button)
        attrs_box.addStretch(0)
        attrs_box.addWidget(self.tutorial_button)

        btn_box = QHBoxLayout()
        btn_box.addWidget(self.refresh_button)
        btn_box.addWidget(self.info_button)
        btn_box.addStretch(0)
        btn_box.addWidget(self.apply_button)
        btn_box.addWidget(self.cancel_button)

        reload_box = QHBoxLayout()
        reload_box.addWidget(self.btn_reload_autosaved)
        reload_box.addStretch(0)

        vbox = QVBoxLayout()
        vbox.addLayout(stradi_box)
        vbox.addWidget(self.tree)
        vbox.addLayout(attrs_box)
        vbox.addLayout(btn_box)
        vbox.addLayout(reload_box)

        self.setLayout(vbox)

        self.apply_button.setEnabled(False)
        self.cancel_button.setEnabled(False)
        self.tree.expandItem(self.progress_item)
        self.tree.expandItem(self.digitizer_item)

        # ---------------------------------------------------------------------
        # --------------------------- Connections -----------------------------
        # ---------------------------------------------------------------------
        self.stradi_combo.currentIndexChanged.connect(self.set_current_stradi)
        self.refresh_button.clicked.connect(self.refresh)
        self.attrs_button.clicked.connect(self.edit_attrs)
        self.tutorial_button.clicked.connect(self.start_tutorial)
        self.open_external.connect(self._create_straditizer_from_args)
        self.btn_open_stradi.clicked.connect(
            self.menu_actions.open_straditizer)
        self.btn_close_stradi.clicked.connect(self.close_straditizer)
        self.btn_reload_autosaved.clicked.connect(self.reload_autosaved)

        self.refresh()
        header = self.tree.header()
        header.setStretchLastSection(False)
        header.setSectionResizeMode(0, QHeaderView.Stretch)

    def disable_apply_button(self):
        """Method that is called when the :attr:`cancel_button` is clicked"""
        for w in [self.apply_button, self.cancel_button]:
            try:
                w.clicked.disconnect()
            except TypeError:
                pass
            w.setEnabled(False)
        self.apply_button.setText('Apply')
        self.cancel_button.setText('Cancel')
        self.refresh_button.setEnabled(True)

    def switch_to_straditizer_layout(self):
        """Switch to the straditizer layout

        This method makes this widget visible and stacks it with the psyplot
        content widget"""
        mainwindow = self.dock.parent()
        mainwindow.figures_tree.hide_plugin()
        mainwindow.ds_tree.hide_plugin()
        mainwindow.fmt_widget.hide_plugin()
        self.show_plugin()
        mainwindow.tabifyDockWidget(mainwindow.project_content.dock, self.dock)
        hsize = self.marker_control.sizeHint().width() + 50
        self.menu_actions.setup_shortcuts(mainwindow)
        if with_qt5:
            mainwindow.resizeDocks([self.dock], [hsize], Qt.Horizontal)
            self.tree.resizeColumnToContents(0)
            self.tree.resizeColumnToContents(1)
        self.info_button.click()

    def to_dock(self, main, *args, **kwargs):
        ret = super(StraditizerWidgets, self).to_dock(main, *args, **kwargs)
        if self.menu_actions.window_layout_action is None:
            main.window_layouts_menu.addAction(self.window_layout_action)
            main.callbacks['straditize'] = self.open_external.emit
            main.addToolBar(self.selection_toolbar)
            self.dock.toggleViewAction().triggered.connect(
                self.show_or_hide_toolbar)
            self.menu_actions.setup_menu_actions(main)
            self.menu_actions.setup_children(self.menu_actions_item)
            try:
                main.open_file_options['Straditize project'] = \
                    self.create_straditizer_from_args
            except AttributeError:  # psyplot-gui <= 1.1.0
                pass
        return ret

    def show_or_hide_toolbar(self):
        """Show or hide the toolbar depending on the visibility of this widget
        """
        self.selection_toolbar.setVisible(self.is_shown)

    def _create_straditizer_from_args(self, args):
        """A method that is called when the :attr:`psyplot_gui.main.mainwindow`
        receives a 'straditize' callback"""
        self.create_straditizer_from_args(*args)

    def create_straditizer_from_args(self,
                                     fnames,
                                     project=None,
                                     xlim=None,
                                     ylim=None,
                                     full=False,
                                     reader_type='area'):
        """Create a straditizer from the given file name

        This method is called when the :attr:`psyplot_gui.main.mainwindow`
        receives a 'straditize' callback"""
        fname = fnames[0]
        if fname is not None:
            self.menu_actions.open_straditizer(fname)
            stradi = self.straditizer
            if stradi is None:
                return
            if xlim is not None:
                stradi.data_xlim = xlim
            if ylim is not None:
                stradi.data_ylim = ylim
            if xlim is not None or ylim is not None or full:
                if stradi.data_xlim is None:
                    stradi.data_xlim = [0, np.shape(stradi.image)[1]]
                if stradi.data_ylim is None:
                    stradi.data_ylim = [0, np.shape(stradi.image)[0]]
                stradi.init_reader(reader_type)
                stradi.data_reader.digitize()
            self.refresh()
        if not self.is_shown:
            self.switch_to_straditizer_layout()
        return fname is not None

    def start_tutorial(self, state, tutorial_cls=None):
        """Start or stop the tutorial

        Parameters
        ----------
        state: bool
            If False, the tutorial is stopped. Otherwise it is started
        tutorial_cls: straditize.widgets.tutorial.beginner.Tutorial
            The tutorial class to use. If None, it will be asked in a
            QInputDialog"""
        if self.tutorial is not None or not state:
            self.tutorial.close()
            self.tutorial_button.setText('Tutorial')
        elif state:
            if tutorial_cls is None:
                tutorial_cls, ok = QInputDialog.getItem(
                    self,
                    'Start tutorial',
                    "Select the tutorial type",
                    ["Beginner", "Advanced (Hoya del Castillo)"],
                    editable=False)
                if not ok:
                    self.tutorial_button.blockSignals(True)
                    self.tutorial_button.setChecked(False)
                    self.tutorial_button.blockSignals(False)
                    return
                if tutorial_cls == 'Beginner':
                    from straditize.widgets.tutorial import Tutorial
                else:
                    from straditize.widgets.tutorial import (
                        HoyaDelCastilloTutorial as Tutorial)
            else:
                Tutorial = tutorial_cls
            self.tutorial = Tutorial(self)
            self.tutorial_button.setText('Stop tutorial')

    def edit_attrs(self):
        """Edit the attributes of the current straditizer

        This creates a new dataframe editor to edit the
        :attr:`straditize.straditizer.Straditizer.attrs` meta informations"""
        def add_attr(key):
            model = editor.table.model()
            n = len(attrs)
            model.insertRow(n)
            model.setData(model.index(n, 0), key)
            model.setData(model.index(n, 1), '', change_type=six.text_type)

        from psyplot_gui.main import mainwindow
        from straditize.straditizer import common_attributes
        attrs = self.straditizer.attrs
        editor = mainwindow.new_data_frame_editor(attrs,
                                                  'Straditizer attributes')
        editor.table.resizeColumnToContents(1)
        editor.table.horizontalHeader().setVisible(False)
        editor.table.frozen_table_view.horizontalHeader().setVisible(False)
        combo = QComboBox()
        combo.addItems([''] + common_attributes)
        combo.currentTextChanged.connect(add_attr)
        hbox = QHBoxLayout()
        hbox.addWidget(QLabel('Common attributes:'))
        hbox.addWidget(combo)
        hbox.addStretch(0)
        editor.layout().insertLayout(1, hbox)
        return editor, combo

    def refresh(self):
        """Refresh from the straditizer"""
        for i, stradi in enumerate(self._straditizers):
            self.stradi_combo.setItemText(
                i,
                self.get_attr(stradi, 'project_file')
                or self.get_attr(stradi, 'image_file') or '')
        # toggle visibility of close button and attributes button
        enable = self.straditizer is not None
        self.btn_close_stradi.setVisible(enable)
        self.attrs_button.setEnabled(enable)
        # refresh controls
        self.menu_actions.refresh()
        self.progress_widget.refresh()
        self.digitizer.refresh()
        self.selection_toolbar.refresh()
        self.plot_control.refresh()
        self.marker_control.refresh()
        self.axes_translations.refresh()
        if self.tutorial is not None:
            self.tutorial.refresh()
        self.image_rotator.refresh()
        self.image_rescaler.refresh()
        self.colnames_manager.refresh()
        self.btn_reload_autosaved.setEnabled(bool(self.autosaved))

    def get_attr(self, stradi, attr):
        try:
            return stradi.get_attr(attr)
        except KeyError:
            pass

    docstrings.delete_params('InfoButton.parameters', 'parent')

    @docstrings.get_sectionsf('StraditizerWidgets.add_info_button')
    @docstrings.with_indent(8)
    def add_info_button(self,
                        child,
                        fname=None,
                        rst=None,
                        name=None,
                        connections=[]):
        """Add an infobutton to the :attr:`tree` widget

        Parameters
        ----------
        child: QTreeWidgetItem
            The item to which to add the infobutton
        %(InfoButton.parameters.no_parent)s
        connections: list of QPushButtons
            Buttons that should be clicked when the info button is clicked"""
        button = InfoButton(self, fname=fname, rst=rst, name=name)
        self.tree.setItemWidget(child, 1, button)
        for btn in connections:
            btn.clicked.connect(button.click)
        return button

    def raise_figures(self):
        """Raise the figures of the current straditizer in the GUI"""
        from psyplot_gui.main import mainwindow
        if mainwindow.figures and self.straditizer:
            dock = self.straditizer.ax.figure.canvas.manager.window
            dock.widget().show_plugin()
            dock.raise_()
            if self.straditizer.magni is not None:
                dock = self.straditizer.magni.ax.figure.canvas.manager.window
                dock.widget().show_plugin()
                dock.raise_()

    def set_current_stradi(self, i):
        """Set the i-th straditizer to the current one"""
        if not self._straditizers:
            return
        self.straditizer = self._straditizers[i]
        self.menu_actions.set_stradi_in_console()
        block = self.stradi_combo.blockSignals(True)
        self.stradi_combo.setCurrentIndex(i)
        self.stradi_combo.blockSignals(block)
        self.raise_figures()
        self.refresh()
        self.autosaved.clear()

    def _close_stradi(self, stradi):
        """Close the given straditizer and all it's figures"""
        is_current = stradi is self.straditizer
        if is_current:
            self.selection_toolbar.disconnect()
        stradi.close()
        try:
            i = self._straditizers.index(stradi)
        except ValueError:
            pass
        else:
            del self._straditizers[i]
            self.stradi_combo.removeItem(i)
        if is_current and self._straditizers:
            self.stradi_combo.setCurrentIndex(0)
        elif not self._straditizers:
            self.straditizer = None
            self.refresh()
        self.digitizer.digitize_item.takeChildren()
        self.digitizer.btn_digitize.setChecked(False)
        self.digitizer.btn_digitize.setCheckable(False)
        self.digitizer.toggle_txt_tolerance('')

    def close_straditizer(self):
        """Close the current straditizer"""
        self._close_stradi(self.straditizer)

    def close_all_straditizers(self):
        """Close all straditizers"""
        self.selection_toolbar.disconnect()
        for stradi in self._straditizers:
            stradi.close()
        self._straditizers.clear()
        self.straditizer = None
        self.stradi_combo.clear()
        self.digitizer.digitize_item.takeChildren()
        self.digitizer.btn_digitize.setChecked(False)
        self.digitizer.btn_digitize.setCheckable(False)
        self.digitizer.toggle_txt_tolerance('')
        self.refresh()

    def add_straditizer(self, stradi):
        """Add a straditizer to the list of open straditizers"""
        if stradi and stradi not in self._straditizers:
            self._straditizers.append(stradi)
            self.stradi_combo.addItem(' ')
            self.set_current_stradi(len(self._straditizers) - 1)

    def reset_control(self):
        """Reset the GUI of straditize"""
        if getattr(self.selection_toolbar, '_pattern_selection', None):
            self.selection_toolbar._pattern_selection.remove_plugin()
            del self.selection_toolbar._pattern_selection
        if getattr(self.digitizer, '_samples_editor', None):
            self.digitizer._close_samples_fig()
        tb = self.selection_toolbar
        tb.set_label_wand_mode()
        tb.set_rect_select_mode()
        tb.new_select_action.setChecked(True)
        tb.select_action.setChecked(False)
        tb.wand_action.setChecked(False)
        self.disable_apply_button()
        self.close_all_straditizers()
        self.colnames_manager.reset_control()

    def autosave(self):
        """Autosave the current straditizer"""
        self.autosaved = [self.straditizer.to_dataset().copy(True)] + \
            self.autosaved[:4]

    def reload_autosaved(self):
        """Reload the autosaved straditizer and close the old one"""
        from straditize.straditizer import Straditizer
        if not self.autosaved:
            return
        answer = QMessageBox.question(
            self, 'Reload autosave',
            'Shall I reload the last autosaved stage? This will close the '
            'current figures.')
        if answer == QMessageBox.Yes:
            self.close_straditizer()
            stradi = Straditizer.from_dataset(self.autosaved.pop(0))
            self.menu_actions.finish_loading(stradi)
Exemplo n.º 3
0
class MultiCrossMarksEditor(DockMixin, QWidget):
    """An editor for cross marks in multiple axes"""

    #: The QDockWidget for the :class:`DataFrameEditor`
    dock_cls = DataFrameDock

    #: A :class:`weakref` to the
    #: :attr:`~straditize.widgets.StraditizerWidgets.straditizer`
    straditizer = None

    def __init__(self, straditizer, axes=None, *args, **kwargs):
        """
        Parameters
        ----------
        straditizer: weakref.ref
            The reference to the straditizer
        axes: matplotlib.axes.Axes
            The matplotlib axes corresponding to the marks
        """
        super(MultiCrossMarksEditor, self).__init__(*args, **kwargs)
        self.straditizer = straditizer
        straditizer = straditizer()
        self.error_msg = PyErrorMessage(self)

        #: Plot the reconstructed data
        self.cb_plot_lines = QCheckBox('Plot reconstruction')
        self.cb_plot_lines.setChecked(True)

        # A Checkbox to automatically zoom to the selection
        self.cb_zoom_to_selection = QCheckBox('Zoom to selection')

        # A Checkbox to automaticall hide the other marks
        self.cb_selection_only = QCheckBox('Selection only')

        # A Checkbox to automatically fit the selected cells to the selected
        # data
        self.cb_fit2selection = QCheckBox(
            'Fit selected cells to selected data')
        self.cb_fit2selection.setToolTip(
            'If checked, select cells from the table and click on one of the '
            'plots to update the table with the data at the selected position.'
        )

        # The table to display the DataFrame
        self.table = self.create_view(axes=axes)

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

        self.btn_save = QPushButton('Save')
        self.btn_save.setToolTip('Save the samples and continue editing')

        # ---------------------------------------------------------------------
        # ------------------------ layout --------------------------------
        # ---------------------------------------------------------------------
        vbox = QVBoxLayout()
        self.top_hbox = hbox = QHBoxLayout()
        hbox.addWidget(self.cb_zoom_to_selection)
        hbox.addWidget(self.cb_selection_only)
        hbox.addWidget(self.cb_fit2selection)
        hbox.addWidget(self.cb_plot_lines)
        hbox.addStretch(0)
        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_save)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # ------------------------ Connections --------------------------------
        # ---------------------------------------------------------------------
        self.format_editor.textChanged.connect(self.toggle_fmt_button)
        self.btn_change_format.clicked.connect(self.update_format)
        self.btn_save.clicked.connect(self.save_samples)
        straditizer.mark_added.connect(self.table.model().load_new_marks)
        straditizer.mark_removed.connect(self.table.model().remove_mark)
        self.table.selectionModel().selectionChanged.connect(
            self.maybe_zoom_to_selection)
        self.table.frozen_table_view.selectionModel().selectionChanged.connect(
            self.maybe_zoom_to_selection)
        self.table.selectionModel().selectionChanged.connect(
            self.maybe_show_selection_only)
        self.table.frozen_table_view.selectionModel().selectionChanged.connect(
            self.maybe_show_selection_only)

        self.cb_zoom_to_selection.stateChanged.connect(
            self.toggle_cb_zoom_to_selection)
        self.cb_selection_only.stateChanged.connect(
            self.toggle_cb_selection_only)
        self.cb_fit2selection.stateChanged.connect(self.toggle_fit2selection)
        self.cb_plot_lines.stateChanged.connect(self.toggle_plot_lines)

        self.toggle_plot_lines()

    def create_view(self, axes=None):
        """Create the :class:`MultiCrossMarksView` of the editor

        Parameters
        ----------
        axes: list of :class:`matplotlib.axes.Axes`
            The matplotlib axes for the marks"""
        stradi = self.straditizer()
        reader = stradi.data_reader
        df = getattr(stradi, '_plotted_full_df', reader._full_df).copy()
        df.columns = [
            str(i) if str(i) == colname else '%s (%i)' % (colname, i)
            for i, colname in enumerate(stradi.colnames_reader.column_names +
                                        ['nextrema'])
        ]
        return MultiCrossMarksView(stradi.marks,
                                   df,
                                   df.columns,
                                   self.straditizer,
                                   axes=axes,
                                   occurences_value=reader.occurences_value)

    def save_samples(self):
        """Save the samples to the :attr:`straditizer` without removing them"""
        self.straditizer().update_samples_sep(remove=False)

    def maybe_zoom_to_selection(self):
        if self.cb_zoom_to_selection.isChecked():
            self.table.zoom_to_selection()

    def maybe_show_selection_only(self):
        if self.cb_selection_only.isChecked():
            self.table.show_selected_marks_only()

    def toggle_cb_zoom_to_selection(self):
        if self.cb_zoom_to_selection.isChecked():
            self.table.zoom_to_selection()

    def toggle_cb_selection_only(self):
        if self.cb_selection_only.isChecked():
            self.table.show_selected_marks_only()
        else:
            self.table.show_all_marks()

    def toggle_fit2selection(self):
        """Enable the fitting so selected digitized data"""
        model = self.table.model()
        fig = model.fig
        if self.cb_fit2selection.isChecked():
            self._fit2selection_cid = fig.canvas.mpl_connect(
                'button_press_event', self._fit2selection)
        elif self._fit2selection_cid is not None:
            fig.canvas.mpl_disconnect(self._fit2selection_cid)
            del self._fit2selection_cid

    def _fit2selection(self, event):
        model = self.table.model()
        if (not event.inaxes or event.button != 1
                or model.fig.canvas.manager.toolbar.mode != ''):
            return
        y = int(np.round(event.ydata))
        data = self.table.full_df.loc[y]
        indexes = list(self.table.selectedIndexes())
        mark = None
        for index in indexes:
            row = index.row()
            col = index.column()
            if col == 0:  # index column
                continue
            mark = model.get_cell_mark(row, col)
            old_pos = mark.pos
            xa = data[col - 1]
            if np.isnan(xa):
                xa = 0
            mark.set_pos((xa, mark.ya))
            mark.moved.emit(old_pos, mark)
        if mark is not None:
            mark.fig.canvas.draw()

    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 toggle_plot_lines(self):
        model = self.table.model()
        if self.cb_plot_lines.isChecked():
            model.plot_lines()
        else:
            model.remove_lines()

    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,
                title=None,
                position=None,
                docktype='df',
                *args,
                **kwargs):
        if position is None:
            if main.centralWidget() is not main.help_explorer:
                position = main.dockWidgetArea(main.help_explorer.dock)
            else:
                position = Qt.RightDockWidgetArea
        connect = self.dock is None
        ret = super(MultiCrossMarksEditor, 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)
Exemplo n.º 4
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
Exemplo n.º 5
0
class PlotControl(StraditizerControlBase, QWidget):
    """A widget for controlling the plot

    This widgets holds a :class:`PlotControlTable` to display visual
    diagnostics in the plot. Additionally it contains zoom buttons
    (:attr:`btn_view_global` and :attr:`btn_view_data`) and a widget to plot
    the results (:attr:`results_plot`)"""

    #: A :class:`PlotControlTable` to display visual diagnostics
    table = None

    #: A button to zoom out to the entire stratigraphic diagram
    #: (see :meth:`zoom_global`)
    btn_view_global = None

    #: A button to zoom to the data
    #: (see :meth:`zoom_data`)
    btn_view_data = None

    #: A :class:`ResultsPlot` to plot the digitized data in a new diagram
    results_plot = None

    def __init__(self, straditizer_widgets, item, *args, **kwargs):
        super(PlotControl, self).__init__(*args, **kwargs)
        self.btn_view_global = QPushButton('Zoom out')
        self.btn_view_data = QPushButton('Zoom to data')
        self.table = PlotControlTable(straditizer_widgets)

        self.results_plot = ResultsPlot(straditizer_widgets)

        self.init_straditizercontrol(straditizer_widgets, item)

        # ---------------------------------------------------------------------
        # ------------------------------ Layout -------------------------------
        # ---------------------------------------------------------------------

        hbox = QHBoxLayout()
        hbox.addWidget(self.btn_view_global)
        hbox.addWidget(self.btn_view_data)

        vbox = QVBoxLayout()
        vbox.addLayout(hbox)
        vbox.addWidget(self.table)

        self.setLayout(vbox)

        # ---------------------------------------------------------------------
        # --------------------------- Connections -----------------------------
        # ---------------------------------------------------------------------

        self.btn_view_global.clicked.connect(self.zoom_global)
        self.btn_view_data.clicked.connect(self.zoom_data)

    def setup_children(self, item):
        super().setup_children(item)
        child = QTreeWidgetItem(0)
        item.addChild(child)
        self.results_plot.setup_children(child)

    def zoom_global(self):
        """Zoom out to the full straditgraphic diagram

        See Also
        --------
        straditize.straditizer.Straditizer.show_full_image"""
        self.straditizer.show_full_image()
        self.straditizer.draw_figure()

    def zoom_data(self):
        """Zoom to the data part

        See Also
        --------
        straditize.straditizer.Straditizer.show_data_diagram"""
        self.straditizer.show_data_diagram()
        self.straditizer.draw_figure()

    def refresh(self):
        self.table.refresh()
        self.results_plot.refresh()
        if self.straditizer is None:
            self.btn_view_global.setEnabled(False)
            self.btn_view_data.setEnabled(False)
        else:
            self.btn_view_global.setEnabled(True)
            self.btn_view_data.setEnabled(
                self.straditizer.data_xlim is not None
                and self.straditizer.data_ylim is not None)
Exemplo n.º 6
0
class ResultsPlot(StraditizerControlBase):
    """A widget for plotting the final results

    This widgets contains a QPushButton :attr:`btn_plot` to plot the results
    using the :meth:`straditize.binary.DataReader.plot_results` method"""

    #: The QPushButton to call the :meth:`plot_results` method
    btn_plot = None

    #: A QCheckBox whether x- and y-axis should be translated from pixel to
    #: data units
    cb_transformed = None

    #: A QCheckBox whether the samples or the full digitized data shall be
    #: plotted
    cb_final = None

    def __init__(self, straditizer_widgets):
        self.init_straditizercontrol(straditizer_widgets)

        self.btn_plot = QPushButton("Plot results")

        self.cb_final = QCheckBox("Samples")
        self.cb_final.setToolTip(
            "Create the diagram based on the samples only, not on the full "
            "digized data")
        self.cb_final.setChecked(True)
        self.cb_final.setEnabled(False)

        self.cb_transformed = QCheckBox("Translated")
        self.cb_transformed.setToolTip("Use the x-axis and y-axis translation")
        self.cb_transformed.setChecked(True)
        self.cb_transformed.setEnabled(False)

        self.btn_plot.clicked.connect(self.plot_results)

    def setup_children(self, item):
        tree = self.straditizer_widgets.tree
        tree.setItemWidget(item, 0, self.btn_plot)

        child = QTreeWidgetItem(0)
        item.addChild(child)
        widget = QWidget()
        vbox = QVBoxLayout()
        vbox.addWidget(self.cb_final)
        vbox.addWidget(self.cb_transformed)
        widget.setLayout(vbox)

        tree.setItemWidget(child, 0, widget)

    def refresh(self):
        try:
            self.straditizer.yaxis_px
            self.straditizer.data_reader.xaxis_px
        except (AttributeError, ValueError):
            self.cb_transformed.setEnabled(False)
        else:
            self.cb_transformed.setEnabled(True)
        try:
            assert self.straditizer.data_reader.sample_locs is not None
        except (AssertionError, AttributeError):
            self.cb_final.setEnabled(False)
        else:
            self.cb_final.setEnabled(True)
        try:
            self.btn_plot.setEnabled(
                self.straditizer.data_reader._full_df is not None)
        except AttributeError:
            self.btn_plot.setEnabled(False)

    def plot_results(self):
        """Plot the results

        What is plotted depends on the :attr:`cb_transformed` and the
        :attr:`cb_final`

        :attr:`cb_transformed` and :attr:`cb_final` are checked
            Plot the :attr:`straditize.straditizer.Straditizer.final_df`
        :attr:`cb_transformed` is checked but not :attr:`cb_final`
            Plot the :attr:`straditize.straditizer.Straditizer.full_df`
        :attr:`cb_transformed` is not checked but :attr:`cb_final`
            Plot the :attr:`straditize.binary.DataReader.sample_locs`
        :attr:`cb_transformed` and :attr:`cb_final` are both not checked
            Plot the :attr:`straditize.binary.DataReader.full_df`"""
        transformed = self.cb_transformed.isEnabled() and \
            self.cb_transformed.isChecked()
        if self.cb_final.isEnabled() and self.cb_final.isChecked():
            df = self.straditizer.final_df if transformed else \
                self.straditizer.data_reader.sample_locs
        else:
            df = self.straditizer.full_df if transformed else \
                self.straditizer.data_reader._full_df
        return self.straditizer.data_reader.plot_results(
            df, transformed=transformed)
Exemplo n.º 7
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)
Exemplo n.º 8
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()