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