From 18a86e10919ba57f0341a94b55b8f1d6b99ec8b9 Mon Sep 17 00:00:00 2001 From: Kris Stern Date: Thu, 7 May 2020 03:28:26 +0800 Subject: [PATCH 1/5] Update 1D Profile viewer to use wcsaxes for plotting and add sliders --- .qt_for_python/uic/options_widget.py | 250 ++++++++++++++++++ glue/app/qt/layer_tree_widget.py | 4 +- glue/core/data.py | 8 +- glue/utils/wcs.py | 12 + glue/viewers/common/qt/data_slice_widget.py | 3 +- glue/viewers/image/qt/data_viewer.py | 3 +- glue/viewers/image/qt/options_widget.py | 4 +- glue/viewers/image/qt/profile_viewer_tool.py | 1 - glue/viewers/image/qt/slice_widget.py | 8 +- glue/viewers/profile/layer_artist.py | 16 +- glue/viewers/profile/qt/data_viewer.py | 2 +- glue/viewers/profile/qt/options_widget.py | 41 ++- glue/viewers/profile/qt/options_widget.ui | 6 + glue/viewers/profile/qt/slice_widget.py | 131 +++++++++ .../profile/qt/tests/test_python_export.py | 4 + glue/viewers/profile/state.py | 90 +++++-- glue/viewers/profile/tests/test_state.py | 15 +- glue/viewers/profile/viewer.py | 50 +++- 18 files changed, 581 insertions(+), 67 deletions(-) create mode 100644 .qt_for_python/uic/options_widget.py create mode 100644 glue/utils/wcs.py create mode 100644 glue/viewers/profile/qt/slice_widget.py diff --git a/.qt_for_python/uic/options_widget.py b/.qt_for_python/uic/options_widget.py new file mode 100644 index 000000000..a6e475263 --- /dev/null +++ b/.qt_for_python/uic/options_widget.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'options_widget.ui' +## +## Created by: Qt User Interface Compiler version 5.15.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + +from glue.viewers.matplotlib.qt.axes_editor import AxesEditorWidget +from glue.viewers.matplotlib.qt.legend_editor import LegendEditorWidget + + +class Ui_Widget(object): + def setupUi(self, Widget): + if not Widget.objectName(): + Widget.setObjectName(u"Widget") + Widget.resize(269, 418) + self.gridLayout_5 = QGridLayout(Widget) + self.gridLayout_5.setSpacing(6) + self.gridLayout_5.setContentsMargins(11, 11, 11, 11) + self.gridLayout_5.setObjectName(u"gridLayout_5") + self.gridLayout_5.setVerticalSpacing(5) + self.gridLayout_5.setContentsMargins(5, 5, 5, 5) + self.tab_widget = QTabWidget(Widget) + self.tab_widget.setObjectName(u"tab_widget") + self.tab = QWidget() + self.tab.setObjectName(u"tab") + self.gridLayout_2 = QGridLayout(self.tab) + self.gridLayout_2.setSpacing(6) + self.gridLayout_2.setContentsMargins(11, 11, 11, 11) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout_2.setHorizontalSpacing(10) + self.gridLayout_2.setVerticalSpacing(5) + self.gridLayout_2.setContentsMargins(10, 10, 10, 10) + self.label_6 = QLabel(self.tab) + self.label_6.setObjectName(u"label_6") + font = QFont() + font.setBold(True) + font.setWeight(75) + self.label_6.setFont(font) + self.label_6.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout_2.addWidget(self.label_6, 1, 0, 1, 1) + + self.combosel_function = QComboBox(self.tab) + self.combosel_function.setObjectName(u"combosel_function") + + self.gridLayout_2.addWidget(self.combosel_function, 0, 1, 1, 2) + + self.combosel_x_att = QComboBox(self.tab) + self.combosel_x_att.setObjectName(u"combosel_x_att") + self.combosel_x_att.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) + + self.gridLayout_2.addWidget(self.combosel_x_att, 2, 1, 1, 2) + + self.label = QLabel(self.tab) + self.label.setObjectName(u"label") + self.label.setFont(font) + self.label.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) + + self.label_3 = QLabel(self.tab) + self.label_3.setObjectName(u"label_3") + self.label_3.setFont(font) + self.label_3.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1) + + self.bool_normalize = QCheckBox(self.tab) + self.bool_normalize.setObjectName(u"bool_normalize") + + self.gridLayout_2.addWidget(self.bool_normalize, 4, 1, 1, 1) + + self.horizontalSpacer = QSpacerItem(40, 5, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.gridLayout_2.addItem(self.horizontalSpacer, 6, 1, 1, 2) + + self.label_7 = QLabel(self.tab) + self.label_7.setObjectName(u"label_7") + self.label_7.setFont(font) + self.label_7.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) + + self.gridLayout_2.addWidget(self.label_7, 4, 0, 1, 1) + + self.text_warning = QLabel(self.tab) + self.text_warning.setObjectName(u"text_warning") + self.text_warning.setStyleSheet(u"color: rgb(255, 33, 28)") + self.text_warning.setAlignment(Qt.AlignCenter) + self.text_warning.setWordWrap(True) + + self.gridLayout_2.addWidget(self.text_warning, 5, 1, 1, 2) + + self.layout_slices = QVBoxLayout() + self.layout_slices.setSpacing(6) + self.layout_slices.setObjectName(u"layout_slices") + + self.gridLayout_2.addLayout(self.layout_slices, 6, 0, 1, 3) + + self.combosel_reference_data = QComboBox(self.tab) + self.combosel_reference_data.setObjectName(u"combosel_reference_data") + self.combosel_reference_data.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) + + self.gridLayout_2.addWidget(self.combosel_reference_data, 1, 1, 1, 2) + + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.gridLayout_2.addItem(self.verticalSpacer_2, 6, 0, 1, 1) + + self.tab_widget.addTab(self.tab, "") + self.tab_2 = QWidget() + self.tab_2.setObjectName(u"tab_2") + self.gridLayout = QGridLayout(self.tab_2) + self.gridLayout.setSpacing(6) + self.gridLayout.setContentsMargins(11, 11, 11, 11) + self.gridLayout.setObjectName(u"gridLayout") + self.gridLayout.setHorizontalSpacing(10) + self.gridLayout.setVerticalSpacing(5) + self.gridLayout.setContentsMargins(10, 10, 10, 10) + self.button_flip_x = QToolButton(self.tab_2) + self.button_flip_x.setObjectName(u"button_flip_x") + self.button_flip_x.setStyleSheet(u"padding: 0px") + + self.gridLayout.addWidget(self.button_flip_x, 0, 2, 1, 1) + + self.valuetext_y_min = QLineEdit(self.tab_2) + self.valuetext_y_min.setObjectName(u"valuetext_y_min") + + self.gridLayout.addWidget(self.valuetext_y_min, 1, 1, 1, 1) + + self.valuetext_x_max = QLineEdit(self.tab_2) + self.valuetext_x_max.setObjectName(u"valuetext_x_max") + + self.gridLayout.addWidget(self.valuetext_x_max, 0, 3, 1, 1) + + self.valuetext_x_min = QLineEdit(self.tab_2) + self.valuetext_x_min.setObjectName(u"valuetext_x_min") + + self.gridLayout.addWidget(self.valuetext_x_min, 0, 1, 1, 1) + + self.label_2 = QLabel(self.tab_2) + self.label_2.setObjectName(u"label_2") + self.label_2.setFont(font) + + self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1) + + self.label_5 = QLabel(self.tab_2) + self.label_5.setObjectName(u"label_5") + self.label_5.setFont(font) + + self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) + + self.gridLayout.addItem(self.verticalSpacer, 3, 3, 1, 1) + + self.valuetext_y_max = QLineEdit(self.tab_2) + self.valuetext_y_max.setObjectName(u"valuetext_y_max") + + self.gridLayout.addWidget(self.valuetext_y_max, 1, 3, 1, 1) + + self.horizontalSpacer_2 = QSpacerItem(40, 5, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.gridLayout.addItem(self.horizontalSpacer_2, 2, 1, 1, 4) + + self.bool_y_log = QToolButton(self.tab_2) + self.bool_y_log.setObjectName(u"bool_y_log") + self.bool_y_log.setCheckable(True) + + self.gridLayout.addWidget(self.bool_y_log, 1, 4, 1, 1) + + self.bool_x_log = QToolButton(self.tab_2) + self.bool_x_log.setObjectName(u"bool_x_log") + self.bool_x_log.setCheckable(True) + + self.gridLayout.addWidget(self.bool_x_log, 0, 4, 1, 1) + + self.tab_widget.addTab(self.tab_2, "") + self.bool_x_log.raise_() + self.valuetext_x_max.raise_() + self.button_flip_x.raise_() + self.valuetext_x_min.raise_() + self.valuetext_y_min.raise_() + self.valuetext_y_max.raise_() + self.bool_y_log.raise_() + self.label_2.raise_() + self.label_5.raise_() + self.tab_3 = QWidget() + self.tab_3.setObjectName(u"tab_3") + self.horizontalLayout = QHBoxLayout(self.tab_3) + self.horizontalLayout.setSpacing(6) + self.horizontalLayout.setContentsMargins(11, 11, 11, 11) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(5, 5, 5, 5) + self.axes_editor = AxesEditorWidget(self.tab_3) + self.axes_editor.setObjectName(u"axes_editor") + + self.horizontalLayout.addWidget(self.axes_editor) + + self.tab_widget.addTab(self.tab_3, "") + self.tab_4 = QWidget() + self.tab_4.setObjectName(u"tab_4") + self.horizontalLayout1 = QHBoxLayout(self.tab_4) + self.horizontalLayout1.setSpacing(6) + self.horizontalLayout1.setContentsMargins(11, 11, 11, 11) + self.horizontalLayout1.setObjectName(u"horizontalLayout1") + self.horizontalLayout1.setContentsMargins(5, 5, 5, 5) + self.legend_editor = LegendEditorWidget(self.tab_4) + self.legend_editor.setObjectName(u"legend_editor") + + self.horizontalLayout1.addWidget(self.legend_editor) + + self.tab_widget.addTab(self.tab_4, "") + + self.gridLayout_5.addWidget(self.tab_widget, 9, 2, 1, 1) + + + self.retranslateUi(Widget) + + self.tab_widget.setCurrentIndex(0) + + + QMetaObject.connectSlotsByName(Widget) + # setupUi + + def retranslateUi(self, Widget): + Widget.setWindowTitle(QCoreApplication.translate("Widget", u"1D Profile", None)) + self.label_6.setText(QCoreApplication.translate("Widget", u"reference", None)) + self.label.setText(QCoreApplication.translate("Widget", u"function", None)) + self.label_3.setText(QCoreApplication.translate("Widget", u"x axis", None)) + self.bool_normalize.setText("") + self.label_7.setText(QCoreApplication.translate("Widget", u"normalize", None)) + self.text_warning.setText(QCoreApplication.translate("Widget", u"Warning", None)) + self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab), QCoreApplication.translate("Widget", u"General", None)) + self.button_flip_x.setText(QCoreApplication.translate("Widget", u"\u21c4", None)) + self.label_2.setText(QCoreApplication.translate("Widget", u"x axis", None)) + self.label_5.setText(QCoreApplication.translate("Widget", u"y axis", None)) + self.bool_y_log.setText(QCoreApplication.translate("Widget", u"log", None)) + self.bool_x_log.setText(QCoreApplication.translate("Widget", u"log", None)) + self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_2), QCoreApplication.translate("Widget", u"Limits", None)) + self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_3), QCoreApplication.translate("Widget", u"Axes", None)) + self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_4), QCoreApplication.translate("Widget", u"Legend", None)) + # retranslateUi + diff --git a/glue/app/qt/layer_tree_widget.py b/glue/app/qt/layer_tree_widget.py index f170434ac..4a9d482f6 100644 --- a/glue/app/qt/layer_tree_widget.py +++ b/glue/app/qt/layer_tree_widget.py @@ -197,14 +197,14 @@ class DeleteAction(LayerAction): def _can_trigger(self): selection = self.selected_layers() - return all(isinstance(s, (core.Data, core.SubsetGroup)) + return all(isinstance(s, (core.BaseData, core.SubsetGroup)) for s in selection) def _do_action(self): assert self._can_trigger() selection = self.selected_layers() for s in selection: - if isinstance(s, core.Data): + if isinstance(s, core.BaseData): self._layer_tree.data_collection.remove(s) else: assert isinstance(s, core.SubsetGroup) diff --git a/glue/core/data.py b/glue/core/data.py index 92e7bdd4e..94fe4f48e 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -27,7 +27,8 @@ from glue.core.contracts import contract from glue.core.joins import get_mask_with_key_joins from glue.config import settings, data_translator, subset_state_translator -from glue.utils import (compute_statistic, unbroadcast, iterate_chunks, +from glue.utils import (compute_statistic, + unbroadcast, iterate_chunks, datetime64_to_mpl, broadcast_to, categorical_ndarray, format_choices, random_views_for_dask_array) from glue.core.coordinate_helpers import axis_label @@ -241,7 +242,7 @@ def add_subset(self, subset, label=None): if subset.data is not self: subset.do_broadcast(False) subset.data = self - subset.label = subset.label # hacky. disambiguates name if needed + subset.label = subset.label # hacky, disambiguates name if needed if self.hub is not None: msg = SubsetCreateMessage(subset) @@ -1659,7 +1660,7 @@ def compute_statistic(self, statistic, cid, subset_state=None, axis=None, # from glue.core.link_manager import pixel_cid_to_pixel_cid_matrix # for att in subset_state.attributes: # # TODO: in principle we cold still deal with non-pixel - # # componnet IDs, so this should be fixed. + # # component IDs, so this should be fixed. # if not isinstance(att, PixelComponentID): # break # matrix = pixel_cid_to_pixel_cid_matrix(att.parent, self) @@ -1686,6 +1687,7 @@ def compute_statistic(self, statistic, cid, subset_state=None, axis=None, values = self.compute_statistic(statistic, cid, subset_state=subset_state, axis=axis, finite=finite, positive=positive, percentile=percentile, view=chunk_view) + result[chunk_view[axis_index]] = values return result diff --git a/glue/utils/wcs.py b/glue/utils/wcs.py new file mode 100644 index 000000000..06345d1ea --- /dev/null +++ b/glue/utils/wcs.py @@ -0,0 +1,12 @@ +from astropy.wcs import WCS + + +def get_identity_wcs(naxis): + + wcs = WCS(naxis=naxis) + wcs.wcs.ctype = ['X'] * naxis + wcs.wcs.crval = [0.] * naxis + wcs.wcs.crpix = [1.] * naxis + wcs.wcs.cdelt = [1.] * naxis + + return wcs diff --git a/glue/viewers/common/qt/data_slice_widget.py b/glue/viewers/common/qt/data_slice_widget.py index add7f3999..9fffd1a08 100644 --- a/glue/viewers/common/qt/data_slice_widget.py +++ b/glue/viewers/common/qt/data_slice_widget.py @@ -27,7 +27,7 @@ def __init__(self, label='', world=None, lo=0, hi=10, parent=None, world_unit=None, world_warning=False): - super(SliceWidget, self).__init__(parent) + super(SliceWidget, self).__init__(parent=parent) self.state = SliceState() self.state.label = label @@ -109,6 +109,7 @@ def __init__(self, label='', world=None, lo=0, hi=10, def set_label_from_slider(self): value = self.state.slice_center + if self.state.use_world: value = self._world[value] if self._world_warning: diff --git a/glue/viewers/image/qt/data_viewer.py b/glue/viewers/image/qt/data_viewer.py index c78a443f0..af8f798bc 100644 --- a/glue/viewers/image/qt/data_viewer.py +++ b/glue/viewers/image/qt/data_viewer.py @@ -38,7 +38,8 @@ class ImageViewer(MatplotlibImageMixin, MatplotlibDataViewer): tools = ['select:rectangle', 'select:xrange', 'select:yrange', 'select:circle', - 'select:polygon', 'image:point_selection', 'image:contrast_bias', + 'select:polygon', 'image:point_selection', + 'image:contrast_bias', 'profile-viewer'] def __init__(self, session, parent=None, state=None): diff --git a/glue/viewers/image/qt/options_widget.py b/glue/viewers/image/qt/options_widget.py index 3a005f3bc..4d93d19a1 100644 --- a/glue/viewers/image/qt/options_widget.py +++ b/glue/viewers/image/qt/options_widget.py @@ -28,10 +28,12 @@ def __init__(self, viewer_state, session, parent=None): self.viewer_state = viewer_state + self.session = session + self.slice_helper = MultiSliceWidgetHelper(viewer_state=self.viewer_state, + session=self.session, layout=self.ui.layout_slices) - self.session = session self.ui.axes_editor.button_apply_all.clicked.connect(self._apply_all_viewers) def _apply_all_viewers(self): diff --git a/glue/viewers/image/qt/profile_viewer_tool.py b/glue/viewers/image/qt/profile_viewer_tool.py index 48b110aab..9f6d5f37f 100644 --- a/glue/viewers/image/qt/profile_viewer_tool.py +++ b/glue/viewers/image/qt/profile_viewer_tool.py @@ -65,7 +65,6 @@ def activate(self): # be a pixel attribute or world attribute depending on what information # is available in the coordinates, so we need to be careful about that. - x_att = profile_viewer.state.x_att reference_data = self.viewer.state.reference_data if isinstance(profile_viewer.state.x_att, PixelComponentID): diff --git a/glue/viewers/image/qt/slice_widget.py b/glue/viewers/image/qt/slice_widget.py index 3fc80910b..1e41c8ed1 100644 --- a/glue/viewers/image/qt/slice_widget.py +++ b/glue/viewers/image/qt/slice_widget.py @@ -6,12 +6,13 @@ from glue.viewers.image.state import AggregateSlice from glue.utils.decorators import avoid_circular + __all__ = ['MultiSliceWidgetHelper'] class MultiSliceWidgetHelper(object): - def __init__(self, viewer_state=None, layout=None): + def __init__(self, viewer_state=None, layout=None, *args, **kwargs): self.viewer_state = viewer_state @@ -57,9 +58,10 @@ def sync_state_from_sliders(self, *args): slices.append(self.viewer_state.slices[i]) self.viewer_state.slices = tuple(slices) + self._reference_data = self.viewer_state.reference_data + @avoid_circular def sync_sliders_from_state(self, *args): - if self.data is None or self.viewer_state.x_att is None or self.viewer_state.y_att is None: return @@ -94,6 +96,7 @@ def sync_sliders_from_state(self, *args): pixel_axis=world_axis_index, world_axis=world_axis_index) world_unit = self.data.coords.world_axis_units[world_axis_index] + world_warning = len(dependent_axes(self.data.coords, i)) > 1 world_label = self.data.world_component_ids[i].label else: @@ -107,6 +110,7 @@ def sync_sliders_from_state(self, *args): world_unit=world_unit, world_warning=world_warning) self.slider_state = slider.state + self.slider_state.add_callback('slice_center', self.sync_state_from_sliders) self._sliders.append(slider) self.layout.addWidget(slider) diff --git a/glue/viewers/profile/layer_artist.py b/glue/viewers/profile/layer_artist.py index 9402bae2d..d5b75c826 100644 --- a/glue/viewers/profile/layer_artist.py +++ b/glue/viewers/profile/layer_artist.py @@ -3,7 +3,6 @@ from matplotlib.lines import Line2D - from glue.core import BaseData from glue.utils import defer_draw from glue.viewers.profile.state import ProfileLayerState @@ -27,9 +26,8 @@ def __init__(self, axes, viewer_state, layer_state=None, layer=None): self._viewer_state.add_global_callback(self._update_profile) self.state.add_global_callback(self._update_profile) - drawstyle = 'steps-mid' if self.state.as_steps else 'default' - self.plot_artist = self.axes.plot([1, 2, 3], [3, 4, 5], 'k-', drawstyle=drawstyle)[0] - + self.plot_artist = self.axes.plot([1, 2, 3], [3, 4, 5], 'k-', drawstyle='steps-mid', + color=self.state.layer.style.color)[0] self.mpl_artists = [self.plot_artist] @defer_draw @@ -47,6 +45,7 @@ def _calculate_profile_thread(self, reset=False): # otherwise the thread tries to send these to the glue logger (which # uses Qt), which then results in this kind of error: # QObject::connect: Cannot queue arguments of type 'QTextCursor' + with warnings.catch_warnings(): warnings.simplefilter("ignore") if reset: @@ -122,7 +121,7 @@ def _update_visual_attributes(self): def _update_profile(self, force=False, **kwargs): - if (self._viewer_state.x_att is None or + if (self._viewer_state.x_att_pixel is None or self.state.attribute is None or self.state.layer is None): return @@ -131,9 +130,10 @@ def _update_profile(self, force=False, **kwargs): # of updated properties is up to date after this method has been called. changed = self.pop_changed_properties() - if force or any(prop in changed for prop in ('layer', 'x_att', 'attribute', 'function', 'normalize', 'v_min', 'v_max', 'visible')): - self._calculate_profile(reset=force) - force = True + if force or any(prop in changed for prop in ('layer', 'slices', 'x_att', 'x_att_pixel', 'attribute', + 'function', 'normalize', 'v_min', 'v_max', 'visible')): + self._update_visual_attributes() + self._calculate_profile(reset=True) if force or any(prop in changed for prop in ('alpha', 'color', 'zorder', 'linewidth', 'as_steps')): self._update_visual_attributes() diff --git a/glue/viewers/profile/qt/data_viewer.py b/glue/viewers/profile/qt/data_viewer.py index 38def7c94..73f4bcc28 100644 --- a/glue/viewers/profile/qt/data_viewer.py +++ b/glue/viewers/profile/qt/data_viewer.py @@ -29,5 +29,5 @@ class ProfileViewer(MatplotlibProfileMixin, MatplotlibDataViewer): tools = ['select:xrange', 'profile-analysis'] def __init__(self, session, parent=None, state=None): - MatplotlibDataViewer.__init__(self, session, parent=parent, state=state) + MatplotlibDataViewer.__init__(self, session, parent=parent, wcs=True, state=state) MatplotlibProfileMixin.setup_callbacks(self) diff --git a/glue/viewers/profile/qt/options_widget.py b/glue/viewers/profile/qt/options_widget.py index 261f16cf3..8bee8955c 100644 --- a/glue/viewers/profile/qt/options_widget.py +++ b/glue/viewers/profile/qt/options_widget.py @@ -5,6 +5,8 @@ from glue.core.coordinate_helpers import dependent_axes from echo.qt import autoconnect_callbacks_to_qt from glue.utils.qt import load_ui, fix_tab_widget_fontsize +from glue.viewers.profile.qt.slice_widget import ProfileMultiSliceWidgetHelper +from glue.viewers.matplotlib.state import MatplotlibDataViewerState __all__ = ['ProfileOptionsWidget'] @@ -33,10 +35,28 @@ def __init__(self, viewer_state, session, parent=None): self.session = session + self.profile_slice_helper = None + + self.viewer_state.add_callback('function', self._on_function_change) self.viewer_state.add_callback('x_att', self._on_attribute_change) self.ui.text_warning.hide() + self.ui.axes_editor.button_apply_all.clicked.connect(self._apply_all_viewers) + + def _on_function_change(self, *args): + + if self.viewer_state.function == 'slice': + self.profile_slice_helper = ProfileMultiSliceWidgetHelper(viewer_state=self.viewer_state, + session=self.session, + layout=self.ui.layout_slices) + self.ui.text_warning.hide() + self.ui.text_warning.setText('') + + else: + if self.profile_slice_helper: + self.profile_slice_helper.remove() + def _on_attribute_change(self, *args): if (self.viewer_state.reference_data is None or @@ -45,12 +65,19 @@ def _on_attribute_change(self, *args): self.ui.text_warning.hide() return - world_warning = len(dependent_axes(self.viewer_state.reference_data.coords, - self.viewer_state.x_att_pixel.axis)) > 1 + if self.viewer_state.function != 'slice': + + world_warning = len(dependent_axes(self.viewer_state.reference_data.coords, + self.viewer_state.x_att_pixel.axis)) > 1 - if world_warning: - self.ui.text_warning.show() - self.ui.text_warning.setText(WARNING_TEXT.format(label=self.viewer_state.x_att.label)) - else: self.ui.text_warning.hide() - self.ui.text_warning.setText('') + + if world_warning: + self.ui.text_warning.show() + self.ui.text_warning.setText(WARNING_TEXT.format(label=self.viewer_state.x_att.label)) + + def _apply_all_viewers(self): + for tab in self.session.application.viewers: + for viewer in tab: + if isinstance(viewer.state, MatplotlibDataViewerState): + viewer.state.update_axes_settings_from(self.viewer_state) diff --git a/glue/viewers/profile/qt/options_widget.ui b/glue/viewers/profile/qt/options_widget.ui index cd9bafa99..6a9931133 100644 --- a/glue/viewers/profile/qt/options_widget.ui +++ b/glue/viewers/profile/qt/options_widget.ui @@ -167,6 +167,11 @@ + + + + + @@ -174,6 +179,7 @@ + diff --git a/glue/viewers/profile/qt/slice_widget.py b/glue/viewers/profile/qt/slice_widget.py new file mode 100644 index 000000000..747fff8a4 --- /dev/null +++ b/glue/viewers/profile/qt/slice_widget.py @@ -0,0 +1,131 @@ +from glue.core.coordinate_helpers import dependent_axes, world_axis +from glue.viewers.common.qt.data_slice_widget import SliceWidget +from glue.utils.decorators import avoid_circular + + +__all__ = ['ProfileMultiSliceWidgetHelper'] + + +class ProfileMultiSliceWidgetHelper(object): + + def __init__(self, viewer_state=None, layout=None, session=None, *args, **kwargs): + + self.viewer_state = viewer_state + + self.session = session + + self.profile_layout = layout + self.profile_layout.setSpacing(4) + self.profile_layout.setContentsMargins(0, 3, 0, 3) + + self.viewer_state.add_callback('x_att', self.sync_sliders_from_state) + self.viewer_state.add_callback('slices', self.sync_sliders_from_state) + self.viewer_state.add_callback('reference_data', self.sync_sliders_from_state) + + self._sliders = [] + + self._reference_data = None + self._x_att = None + + self.sync_sliders_from_state() + + @property + def data(self): + return self.viewer_state.reference_data + + def _clear(self): + + for _ in range(self.profile_layout.count()): + self.profile_layout.takeAt(0) + + for s in self._sliders: + if s is not None: + s.close() + + self._sliders = [] + + def remove(self): + + for _ in range(self.profile_layout.count()): + self.profile_layout.takeAt(0) + + for s in self._sliders: + if s is not None: + s.close() + + self._sliders = [] + + @avoid_circular + def sync_state_from_sliders(self, *args): + + slices = [] + for i, slider in enumerate(self._sliders): + if slider is not None: + slices.append(slider.state.slice_center) + else: + slices.append(self.viewer_state.slices[i]) + self.viewer_state.slices = tuple(slices) + + if self.viewer_state.reference_data is not self._reference_data: + self._reference_data = self.viewer_state.reference_data + + @avoid_circular + def sync_sliders_from_state(self, *args): + if self.data is None or self.viewer_state.x_att_pixel is None: + return + + # Update sliders if needed + + if (self.viewer_state.reference_data is not self._reference_data or + self.viewer_state.x_att_pixel is not self._x_att): + + self._reference_data = self.viewer_state.reference_data + self._x_att = self.viewer_state.x_att_pixel + + self._clear() + + for i in range(self.data.ndim): + + if i == self.viewer_state.x_att_pixel.axis: + self._sliders.append(None) + continue + + # TODO: For now we simply pass a single set of world coordinates, + # but we will need to generalize this in future. We deliberately + # check the type of data.coords here since we want to treat + # subclasses differently. + if getattr(self.data, 'coords') is not None: + world = world_axis(self.data.coords, self.data, + pixel_axis=self.data.ndim - 1 - i, + world_axis=self.data.ndim - 1 - i) + world_unit = self.data.coords.world_axis_units[self.data.ndim - 1 - i] + + world_warning = len(dependent_axes(self.data.coords, i)) > 1 + world_label = self.data.world_component_ids[i].label + else: + world = None + world_unit = None + world_warning = False + world_label = self.data.pixel_component_ids[i].label + + slider = SliceWidget(world_label, + hi=self.data.shape[i] - 1, world=world, + world_unit=world_unit, world_warning=world_warning) + + self.slider_state = slider.state + + self.slider_state.add_callback('slice_center', self.sync_state_from_sliders) + self._sliders.append(slider) + self.profile_layout.addWidget(slider) + + slices = [] + for i in range(self.data.ndim): + if self._sliders[i] is not None: + try: + slices.append(self._sliders[i].state.slice_center) + except Exception: + pass + elif self._sliders[i] is None: + slices.append(0) + + self.viewer_state.slices = slices diff --git a/glue/viewers/profile/qt/tests/test_python_export.py b/glue/viewers/profile/qt/tests/test_python_export.py index fa9c3b0c0..4480b4769 100644 --- a/glue/viewers/profile/qt/tests/test_python_export.py +++ b/glue/viewers/profile/qt/tests/test_python_export.py @@ -64,6 +64,10 @@ def test_sum(self, tmpdir): self.viewer.state.function = 'sum' self.assert_same(tmpdir) + def test_slice(self, tmpdir): + self.viewer.state.function = 'slice' + self.assert_same(tmpdir) + def test_normalization(self, tmpdir): self.viewer.state.normalize = True self.assert_same(tmpdir) diff --git a/glue/viewers/profile/state.py b/glue/viewers/profile/state.py index 0d9cd6ba0..a330fe65c 100644 --- a/glue/viewers/profile/state.py +++ b/glue/viewers/profile/state.py @@ -1,3 +1,4 @@ +import copy from collections import OrderedDict from glue.core.hub import HubListener @@ -14,6 +15,8 @@ from glue.core.link_manager import is_convertible_to_single_pixel_cid from glue.core.exceptions import IncompatibleDataException from glue.core.message import SubsetUpdateMessage +from glue.core.state_objects import StateAttributeLimitsHelper + __all__ = ['ProfileViewerState', 'ProfileLayerState'] @@ -22,7 +25,8 @@ ('minimum', 'Minimum'), ('mean', 'Mean'), ('median', 'Median'), - ('sum', 'Sum')]) + ('sum', 'Sum'), + ('slice', 'Slice')]) class ProfileViewerState(MatplotlibDataViewerState): @@ -31,7 +35,7 @@ class ProfileViewerState(MatplotlibDataViewerState): """ x_att_pixel = DDCProperty(docstring='The component ID giving the pixel component ' - 'shown on the x axis') + 'shown on the x axis') x_att = DDSCProperty(docstring='The component ID giving the pixel or world component ' 'shown on the x axis') @@ -41,6 +45,8 @@ class ProfileViewerState(MatplotlibDataViewerState): 'which defines the coordinate frame in ' 'which the images are shown') + slices = DDCProperty(docstring='The current slice along all dimensions') + function = DDSCProperty(docstring='The function to use for collapsing data') normalize = DDCProperty(False, docstring='Whether to normalize all profiles ' @@ -83,6 +89,9 @@ def _display_world(self): @defer_draw def _update_att(self, *args): + """ + Define self.x_att_pixel in viewer state. + """ if self.x_att is not None: if self._display_world: if self.x_att in self.reference_data.pixel_component_ids: @@ -107,18 +116,9 @@ def _reset_x_limits(self, *event): data = self.reference_data - if self.x_att in data.pixel_component_ids: - x_min, x_max = -0.5, data.shape[self.x_att.axis] - 0.5 - else: - axis = data.world_component_ids.index(self.x_att) - axis_view = [0] * data.ndim - axis_view[axis] = slice(None) - axis_values = data[self.x_att, tuple(axis_view)] - x_min, x_max = np.nanmin(axis_values), np.nanmax(axis_values) - with delay_callback(self, 'x_min', 'x_max'): - self.x_min = x_min - self.x_max = x_max + self.x_min = -0.5 + self.x_max = data.shape[self.x_att_pixel.axis] - 0.5 def _reset_y_limits(self, *event): if self.normalize: @@ -180,12 +180,8 @@ def _reference_data_changed(self, before=None, after=None): self.x_att_helper.set_multiple_data([]) else: self.x_att_helper.set_multiple_data([self.reference_data]) - if self._display_world: - self.x_att_helper.world_coord = True - self.x_att = self.reference_data.world_component_ids[0] - else: - self.x_att_helper.world_coord = False - self.x_att = self.reference_data.pixel_component_ids[0] + self.x_att_helper.world_coord = False + self.x_att = self.reference_data.pixel_component_ids[0] self._update_att() @@ -201,6 +197,31 @@ def _update_priority(self, name): else: return 1 + @property + def wcsaxes_slice(self): + """ + Returns slicing information usable by WCSAxes. + + This returns an iterable of slices, and including ``'x'`` and ``'y'`` + for the dimensions along which we are not slicing. + """ + if self.reference_data is None: + return None + slices = [] + for i in range(self.reference_data.ndim): + if i == self.x_att_pixel.axis: + slices.append('x') + else: + slices.append(0) + return slices[::-1] + + def _set_default_slices(self): + # Need to make sure this gets called immediately when reference_data is changed + if self.reference_data is None: + self.slices = () + else: + self.slices = tuple([0] * self.reference_data.ndim) + class ProfileLayerState(MatplotlibLayerState, HubListener): """ @@ -221,12 +242,19 @@ class ProfileLayerState(MatplotlibLayerState, HubListener): _layer_subset_updates_subscribed = False _profile_cache = None - def __init__(self, layer=None, viewer_state=None, **kwargs): + def __init__(self, layer=None, viewer_state=None, session=None, **kwargs): - super(ProfileLayerState, self).__init__(layer=layer, viewer_state=viewer_state) + super(ProfileLayerState, self).__init__(layer=layer, viewer_state=viewer_state, session=session) - self.attribute_att_helper = ComponentIDComboHelper(self, 'attribute', - numeric=True, categorical=False) + self.viewer_state = viewer_state + + self.session = session + + self.attribute_lim_helper = StateAttributeLimitsHelper(self, attribute='attribute', + percentile='percentile', + lower='v_min', upper='v_max') + + self.attribute_att_helper = ComponentIDComboHelper(self, 'attribute') percentile_display = {100: 'Min/Max', 99.5: '99.5%', @@ -298,7 +326,9 @@ def update_profile(self, update_limits=True): if not self._viewer_callbacks_set: self.viewer_state.add_callback('x_att', self.reset_cache, priority=100000) + self.viewer_state.add_callback('x_att_pixel', self.reset_cache, priority=100000) self.viewer_state.add_callback('function', self.reset_cache, priority=100000) + self.viewer_state.add_callback('slices', self.reset_cache, priority=100000) if self.is_callback_property('attribute'): self.add_callback('attribute', self.reset_cache, priority=100000) self._viewer_callbacks_set = True @@ -307,7 +337,7 @@ def update_profile(self, update_limits=True): raise IncompatibleDataException() # Check what pixel axis in the current dataset x_att corresponds to - pix_cid = is_convertible_to_single_pixel_cid(self.layer, self.viewer_state.x_att_pixel) + pix_cid = is_convertible_to_single_pixel_cid(self.layer, self.viewer_state.x_att) if pix_cid is None: raise IncompatibleDataException() @@ -329,7 +359,16 @@ def update_profile(self, update_limits=True): data = self.layer subset_state = None - profile_values = data.compute_statistic(self.viewer_state.function, self.attribute, axis=axes, subset_state=subset_state) + if self.viewer_state.function == 'slice': + data_slice = list(copy.deepcopy(self.viewer_state.slices)) + data_slice[pix_cid.axis] = slice(None) + profile_values = data.get_data(self.attribute, view=tuple(data_slice)) + if subset_state: + subset_profile_mask = data.get_mask(subset_state=subset_state, view=tuple(data_slice)) + profile_values = profile_values[subset_profile_mask] + else: + profile_values = data.compute_statistic(self.viewer_state.function, self.attribute, axis=axes, + subset_state=subset_state) if np.all(np.isnan(profile_values)): self._profile_cache = [], [] @@ -344,6 +383,7 @@ def update_profile(self, update_limits=True): self.update_limits(update_profile=False) def update_limits(self, update_profile=True): + with delay_callback(self, 'v_min', 'v_max'): if update_profile: self.update_profile(update_limits=False) diff --git a/glue/viewers/profile/tests/test_state.py b/glue/viewers/profile/tests/test_state.py index c574558db..d7cc65913 100644 --- a/glue/viewers/profile/tests/test_state.py +++ b/glue/viewers/profile/tests/test_state.py @@ -46,13 +46,7 @@ def setup_method(self, method): def test_basic(self): x, y = self.layer_state.profile - assert_allclose(x, [0, 2, 4]) - assert_allclose(y, [3.5, 11.5, 19.5]) - - def test_basic_world(self): - self.viewer_state.x_att = self.data.world_component_ids[0] - x, y = self.layer_state.profile - assert_allclose(x, [0, 2, 4]) + assert_allclose(x, [0, 1, 2]) assert_allclose(y, [3.5, 11.5, 19.5]) def test_x_att(self): @@ -94,6 +88,9 @@ def test_function(self): x, y = self.layer_state.profile assert_allclose(y, [3.5, 11.5, 19.5]) + self.viewer_state.function = 'slice' + assert_allclose(y, [3.5, 11.5, 19.5]) + def test_subset(self): subset = self.data.new_subset() @@ -102,7 +99,7 @@ def test_subset(self): self.layer_state.layer = subset x, y = self.layer_state.profile - assert_allclose(x, [0, 2, 4]) + assert_allclose(x, [0, 1, 2]) assert_allclose(y, [np.nan, 13., 19.5]) subset.subset_state = self.data.id['x'] > 100 @@ -159,5 +156,5 @@ def test_visible(self): self.layer_state.visible = True x, y = self.layer_state.profile - assert_allclose(x, [0, 2, 4]) + assert_allclose(x, [0, 1, 2]) assert_allclose(y, [3.5, 11.5, 19.5]) diff --git a/glue/viewers/profile/viewer.py b/glue/viewers/profile/viewer.py index 3817a6480..a594da3de 100644 --- a/glue/viewers/profile/viewer.py +++ b/glue/viewers/profile/viewer.py @@ -1,4 +1,8 @@ from glue.core.subset import roi_to_subset_state +from glue.core.coordinates import LegacyCoordinates +from glue.utils.wcs import get_identity_wcs + +from astropy.visualization.wcsaxes.frame import RectangularFrame1D __all__ = ['MatplotlibProfileMixin'] @@ -6,13 +10,22 @@ class MatplotlibProfileMixin(object): def setup_callbacks(self): - self.state.add_callback('x_att', self._update_axes) - self.state.add_callback('normalize', self._update_axes) - def _update_axes(self, *args): + self._changing_slice_requires_wcs_update = None + self.state.add_callback('normalize', self._set_wcs) + self.state.add_callback('x_att_pixel', self._set_wcs) + self.state.add_callback('reference_data', self._set_wcs) + self.state.add_callback('slices', self._set_wcs) + + def update_x_ticklabel(self, *event): + + # We need to overload this here for WCSAxes + axis = 0 - if self.state.x_att is not None: - self.state.x_axislabel = self.state.x_att.label + self.axes.coords[axis].set_ticklabel(size=self.state.x_ticklabel_size) + self.redraw() + + def _update_axes(self, *args): if self.state.normalize: self.state.y_axislabel = 'Normalized data values' @@ -33,5 +46,30 @@ def apply_roi(self, roi, override_mode=None): if len(self.layers) == 0: return - subset_state = roi_to_subset_state(roi, x_att=self.state.x_att) + subset_state = roi_to_subset_state(roi, x_att=self.state.x_att_pixel) self.apply_subset_state(subset_state, override_mode=override_mode) + + def _set_wcs(self, event=None, relim=True): + ref_coords = getattr(self.state.reference_data, 'coords', None) + + self.axes.frame_class = RectangularFrame1D + + if ref_coords is None or isinstance(ref_coords, LegacyCoordinates): + self.axes.reset_wcs(slices=self.state.wcsaxes_slice, + wcs=get_identity_wcs(self.state.reference_data.ndim)) + else: + self.axes.reset_wcs(slices=self.state.wcsaxes_slice, wcs=ref_coords) + + self.axes.yaxis.set_visible(True) + + # Reset the axis labels to match the fact that the new axes have no labels + self.state.x_axislabel = '' + self.state.y_axislabel = 'Data values' + + self._update_appearance_from_settings() + self._update_axes() + + self.update_x_ticklabel() + + if relim: + self.state.reset_limits() From 774c749caad42b0541280d44b1e14c747babaf75 Mon Sep 17 00:00:00 2001 From: Nabil Freij Date: Tue, 9 Nov 2021 11:27:38 -0800 Subject: [PATCH 2/5] minor changes --- glue/core/data.py | 4 +--- glue/utils/wcs.py | 13 ++++++++++++- glue/viewers/common/qt/data_slice_widget.py | 1 - glue/viewers/image/qt/data_viewer.py | 3 +-- glue/viewers/image/qt/slice_widget.py | 4 ---- glue/viewers/image/viewer.py | 14 +------------- glue/viewers/profile/layer_artist.py | 3 +-- glue/viewers/profile/qt/options_widget.py | 2 -- glue/viewers/profile/state.py | 18 +++++++++++++----- 9 files changed, 29 insertions(+), 33 deletions(-) diff --git a/glue/core/data.py b/glue/core/data.py index 94fe4f48e..397066730 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -27,8 +27,7 @@ from glue.core.contracts import contract from glue.core.joins import get_mask_with_key_joins from glue.config import settings, data_translator, subset_state_translator -from glue.utils import (compute_statistic, - unbroadcast, iterate_chunks, +from glue.utils import (compute_statistic, unbroadcast, iterate_chunks, datetime64_to_mpl, broadcast_to, categorical_ndarray, format_choices, random_views_for_dask_array) from glue.core.coordinate_helpers import axis_label @@ -1687,7 +1686,6 @@ def compute_statistic(self, statistic, cid, subset_state=None, axis=None, values = self.compute_statistic(statistic, cid, subset_state=subset_state, axis=axis, finite=finite, positive=positive, percentile=percentile, view=chunk_view) - result[chunk_view[axis_index]] = values return result diff --git a/glue/utils/wcs.py b/glue/utils/wcs.py index 06345d1ea..3c2c6d368 100644 --- a/glue/utils/wcs.py +++ b/glue/utils/wcs.py @@ -2,11 +2,22 @@ def get_identity_wcs(naxis): + """ + Create a WCS object with an identity transform. + Parameters + ---------- + naxis : float + The number of axes. + + Returns + ------- + `astropy.wcs.WCS` + A WCS object with an identity transform. + """ wcs = WCS(naxis=naxis) wcs.wcs.ctype = ['X'] * naxis wcs.wcs.crval = [0.] * naxis wcs.wcs.crpix = [1.] * naxis wcs.wcs.cdelt = [1.] * naxis - return wcs diff --git a/glue/viewers/common/qt/data_slice_widget.py b/glue/viewers/common/qt/data_slice_widget.py index 9fffd1a08..c0c2a23c7 100644 --- a/glue/viewers/common/qt/data_slice_widget.py +++ b/glue/viewers/common/qt/data_slice_widget.py @@ -109,7 +109,6 @@ def __init__(self, label='', world=None, lo=0, hi=10, def set_label_from_slider(self): value = self.state.slice_center - if self.state.use_world: value = self._world[value] if self._world_warning: diff --git a/glue/viewers/image/qt/data_viewer.py b/glue/viewers/image/qt/data_viewer.py index af8f798bc..c78a443f0 100644 --- a/glue/viewers/image/qt/data_viewer.py +++ b/glue/viewers/image/qt/data_viewer.py @@ -38,8 +38,7 @@ class ImageViewer(MatplotlibImageMixin, MatplotlibDataViewer): tools = ['select:rectangle', 'select:xrange', 'select:yrange', 'select:circle', - 'select:polygon', 'image:point_selection', - 'image:contrast_bias', + 'select:polygon', 'image:point_selection', 'image:contrast_bias', 'profile-viewer'] def __init__(self, session, parent=None, state=None): diff --git a/glue/viewers/image/qt/slice_widget.py b/glue/viewers/image/qt/slice_widget.py index 1e41c8ed1..0184b313b 100644 --- a/glue/viewers/image/qt/slice_widget.py +++ b/glue/viewers/image/qt/slice_widget.py @@ -6,7 +6,6 @@ from glue.viewers.image.state import AggregateSlice from glue.utils.decorators import avoid_circular - __all__ = ['MultiSliceWidgetHelper'] @@ -57,7 +56,6 @@ def sync_state_from_sliders(self, *args): else: slices.append(self.viewer_state.slices[i]) self.viewer_state.slices = tuple(slices) - self._reference_data = self.viewer_state.reference_data @avoid_circular @@ -96,7 +94,6 @@ def sync_sliders_from_state(self, *args): pixel_axis=world_axis_index, world_axis=world_axis_index) world_unit = self.data.coords.world_axis_units[world_axis_index] - world_warning = len(dependent_axes(self.data.coords, i)) > 1 world_label = self.data.world_component_ids[i].label else: @@ -110,7 +107,6 @@ def sync_sliders_from_state(self, *args): world_unit=world_unit, world_warning=world_warning) self.slider_state = slider.state - self.slider_state.add_callback('slice_center', self.sync_state_from_sliders) self._sliders.append(slider) self.layout.addWidget(slider) diff --git a/glue/viewers/image/viewer.py b/glue/viewers/image/viewer.py index 70e01ce97..1aa58e086 100644 --- a/glue/viewers/image/viewer.py +++ b/glue/viewers/image/viewer.py @@ -1,7 +1,5 @@ import os -from astropy.wcs import WCS - from glue.core.subset import roi_to_subset_state from glue.core.coordinates import Coordinates, LegacyCoordinates from glue.core.coordinate_helpers import dependent_axes @@ -12,21 +10,11 @@ from glue.viewers.image.frb_artist import imshow from glue.viewers.image.composite_array import CompositeArray +from glue.utils.wcs import get_identity_wcs __all__ = ['MatplotlibImageMixin'] -def get_identity_wcs(naxis): - - wcs = WCS(naxis=naxis) - wcs.wcs.ctype = ['X'] * naxis - wcs.wcs.crval = [0.] * naxis - wcs.wcs.crpix = [1.] * naxis - wcs.wcs.cdelt = [1.] * naxis - - return wcs - - EXTRA_FOOTER = """ # Set tick label size - for now tick_params (called lower down) doesn't work # properly, but these lines won't be needed in future. diff --git a/glue/viewers/profile/layer_artist.py b/glue/viewers/profile/layer_artist.py index d5b75c826..97dd105aa 100644 --- a/glue/viewers/profile/layer_artist.py +++ b/glue/viewers/profile/layer_artist.py @@ -26,7 +26,7 @@ def __init__(self, axes, viewer_state, layer_state=None, layer=None): self._viewer_state.add_global_callback(self._update_profile) self.state.add_global_callback(self._update_profile) - self.plot_artist = self.axes.plot([1, 2, 3], [3, 4, 5], 'k-', drawstyle='steps-mid', + self.plot_artist = self.axes.plot([1, 2, 3], [3, 4, 5], drawstyle='steps-mid', color=self.state.layer.style.color)[0] self.mpl_artists = [self.plot_artist] @@ -45,7 +45,6 @@ def _calculate_profile_thread(self, reset=False): # otherwise the thread tries to send these to the glue logger (which # uses Qt), which then results in this kind of error: # QObject::connect: Cannot queue arguments of type 'QTextCursor' - with warnings.catch_warnings(): warnings.simplefilter("ignore") if reset: diff --git a/glue/viewers/profile/qt/options_widget.py b/glue/viewers/profile/qt/options_widget.py index 8bee8955c..0c750b2c6 100644 --- a/glue/viewers/profile/qt/options_widget.py +++ b/glue/viewers/profile/qt/options_widget.py @@ -41,7 +41,6 @@ def __init__(self, viewer_state, session, parent=None): self.viewer_state.add_callback('x_att', self._on_attribute_change) self.ui.text_warning.hide() - self.ui.axes_editor.button_apply_all.clicked.connect(self._apply_all_viewers) def _on_function_change(self, *args): @@ -52,7 +51,6 @@ def _on_function_change(self, *args): layout=self.ui.layout_slices) self.ui.text_warning.hide() self.ui.text_warning.setText('') - else: if self.profile_slice_helper: self.profile_slice_helper.remove() diff --git a/glue/viewers/profile/state.py b/glue/viewers/profile/state.py index a330fe65c..4a293c3a9 100644 --- a/glue/viewers/profile/state.py +++ b/glue/viewers/profile/state.py @@ -62,7 +62,7 @@ def __init__(self, **kwargs): self.add_callback('layers', self._layers_changed) self.add_callback('reference_data', self._reference_data_changed, echo_old=True) - self.add_callback('x_att', self._update_att) + self.add_callback('x_att', self._update_x_att) self.add_callback('normalize', self._reset_y_limits) self.add_callback('function', self._reset_y_limits) @@ -88,9 +88,9 @@ def _display_world(self): return getattr(self.reference_data, 'coords', None) is not None @defer_draw - def _update_att(self, *args): + def _update_x_att(self, *args): """ - Define self.x_att_pixel in viewer state. + Defines ``self.x_att_pixel`` in the viewer state. """ if self.x_att is not None: if self._display_world: @@ -183,7 +183,7 @@ def _reference_data_changed(self, before=None, after=None): self.x_att_helper.world_coord = False self.x_att = self.reference_data.pixel_component_ids[0] - self._update_att() + self._update_x_att() self.reset_limits() @@ -207,9 +207,17 @@ def wcsaxes_slice(self): """ if self.reference_data is None: return None + elif self.x_att_pixel is None: + # TODO: This should not be here. + # I can not work out how to get this to be set at initialisation. + self.x_att_helper.set_multiple_data([self.reference_data]) + self.x_att_helper.world_coord = False + self.x_att = self.reference_data.pixel_component_ids[0] + self._update_x_att() + slices = [] for i in range(self.reference_data.ndim): - if i == self.x_att_pixel.axis: + if self.x_att_pixel and i == self.x_att_pixel.axis: slices.append('x') else: slices.append(0) From f84b919a46be67380ae04d4b83619ee14cc0b846 Mon Sep 17 00:00:00 2001 From: Nabil Freij Date: Wed, 2 Mar 2022 22:44:36 -0800 Subject: [PATCH 3/5] Added a test back --- .gitignore | 2 + .qt_for_python/uic/options_widget.py | 250 -------------------- glue/viewers/common/qt/data_slice_widget.py | 2 +- glue/viewers/image/qt/slice_widget.py | 3 +- glue/viewers/profile/tests/test_state.py | 7 + 5 files changed, 12 insertions(+), 252 deletions(-) delete mode 100644 .qt_for_python/uic/options_widget.py diff --git a/.gitignore b/.gitignore index e797cbaa7..bd76c5cec 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ glue/qt/glue_qt_resources.py .vscode # vscode plugin .history + +.qt_for_python/ diff --git a/.qt_for_python/uic/options_widget.py b/.qt_for_python/uic/options_widget.py deleted file mode 100644 index a6e475263..000000000 --- a/.qt_for_python/uic/options_widget.py +++ /dev/null @@ -1,250 +0,0 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'options_widget.ui' -## -## Created by: Qt User Interface Compiler version 5.15.2 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide2.QtCore import * -from PySide2.QtGui import * -from PySide2.QtWidgets import * - -from glue.viewers.matplotlib.qt.axes_editor import AxesEditorWidget -from glue.viewers.matplotlib.qt.legend_editor import LegendEditorWidget - - -class Ui_Widget(object): - def setupUi(self, Widget): - if not Widget.objectName(): - Widget.setObjectName(u"Widget") - Widget.resize(269, 418) - self.gridLayout_5 = QGridLayout(Widget) - self.gridLayout_5.setSpacing(6) - self.gridLayout_5.setContentsMargins(11, 11, 11, 11) - self.gridLayout_5.setObjectName(u"gridLayout_5") - self.gridLayout_5.setVerticalSpacing(5) - self.gridLayout_5.setContentsMargins(5, 5, 5, 5) - self.tab_widget = QTabWidget(Widget) - self.tab_widget.setObjectName(u"tab_widget") - self.tab = QWidget() - self.tab.setObjectName(u"tab") - self.gridLayout_2 = QGridLayout(self.tab) - self.gridLayout_2.setSpacing(6) - self.gridLayout_2.setContentsMargins(11, 11, 11, 11) - self.gridLayout_2.setObjectName(u"gridLayout_2") - self.gridLayout_2.setHorizontalSpacing(10) - self.gridLayout_2.setVerticalSpacing(5) - self.gridLayout_2.setContentsMargins(10, 10, 10, 10) - self.label_6 = QLabel(self.tab) - self.label_6.setObjectName(u"label_6") - font = QFont() - font.setBold(True) - font.setWeight(75) - self.label_6.setFont(font) - self.label_6.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) - - self.gridLayout_2.addWidget(self.label_6, 1, 0, 1, 1) - - self.combosel_function = QComboBox(self.tab) - self.combosel_function.setObjectName(u"combosel_function") - - self.gridLayout_2.addWidget(self.combosel_function, 0, 1, 1, 2) - - self.combosel_x_att = QComboBox(self.tab) - self.combosel_x_att.setObjectName(u"combosel_x_att") - self.combosel_x_att.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) - - self.gridLayout_2.addWidget(self.combosel_x_att, 2, 1, 1, 2) - - self.label = QLabel(self.tab) - self.label.setObjectName(u"label") - self.label.setFont(font) - self.label.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) - - self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) - - self.label_3 = QLabel(self.tab) - self.label_3.setObjectName(u"label_3") - self.label_3.setFont(font) - self.label_3.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) - - self.gridLayout_2.addWidget(self.label_3, 2, 0, 1, 1) - - self.bool_normalize = QCheckBox(self.tab) - self.bool_normalize.setObjectName(u"bool_normalize") - - self.gridLayout_2.addWidget(self.bool_normalize, 4, 1, 1, 1) - - self.horizontalSpacer = QSpacerItem(40, 5, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.gridLayout_2.addItem(self.horizontalSpacer, 6, 1, 1, 2) - - self.label_7 = QLabel(self.tab) - self.label_7.setObjectName(u"label_7") - self.label_7.setFont(font) - self.label_7.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter) - - self.gridLayout_2.addWidget(self.label_7, 4, 0, 1, 1) - - self.text_warning = QLabel(self.tab) - self.text_warning.setObjectName(u"text_warning") - self.text_warning.setStyleSheet(u"color: rgb(255, 33, 28)") - self.text_warning.setAlignment(Qt.AlignCenter) - self.text_warning.setWordWrap(True) - - self.gridLayout_2.addWidget(self.text_warning, 5, 1, 1, 2) - - self.layout_slices = QVBoxLayout() - self.layout_slices.setSpacing(6) - self.layout_slices.setObjectName(u"layout_slices") - - self.gridLayout_2.addLayout(self.layout_slices, 6, 0, 1, 3) - - self.combosel_reference_data = QComboBox(self.tab) - self.combosel_reference_data.setObjectName(u"combosel_reference_data") - self.combosel_reference_data.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) - - self.gridLayout_2.addWidget(self.combosel_reference_data, 1, 1, 1, 2) - - self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - - self.gridLayout_2.addItem(self.verticalSpacer_2, 6, 0, 1, 1) - - self.tab_widget.addTab(self.tab, "") - self.tab_2 = QWidget() - self.tab_2.setObjectName(u"tab_2") - self.gridLayout = QGridLayout(self.tab_2) - self.gridLayout.setSpacing(6) - self.gridLayout.setContentsMargins(11, 11, 11, 11) - self.gridLayout.setObjectName(u"gridLayout") - self.gridLayout.setHorizontalSpacing(10) - self.gridLayout.setVerticalSpacing(5) - self.gridLayout.setContentsMargins(10, 10, 10, 10) - self.button_flip_x = QToolButton(self.tab_2) - self.button_flip_x.setObjectName(u"button_flip_x") - self.button_flip_x.setStyleSheet(u"padding: 0px") - - self.gridLayout.addWidget(self.button_flip_x, 0, 2, 1, 1) - - self.valuetext_y_min = QLineEdit(self.tab_2) - self.valuetext_y_min.setObjectName(u"valuetext_y_min") - - self.gridLayout.addWidget(self.valuetext_y_min, 1, 1, 1, 1) - - self.valuetext_x_max = QLineEdit(self.tab_2) - self.valuetext_x_max.setObjectName(u"valuetext_x_max") - - self.gridLayout.addWidget(self.valuetext_x_max, 0, 3, 1, 1) - - self.valuetext_x_min = QLineEdit(self.tab_2) - self.valuetext_x_min.setObjectName(u"valuetext_x_min") - - self.gridLayout.addWidget(self.valuetext_x_min, 0, 1, 1, 1) - - self.label_2 = QLabel(self.tab_2) - self.label_2.setObjectName(u"label_2") - self.label_2.setFont(font) - - self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1) - - self.label_5 = QLabel(self.tab_2) - self.label_5.setObjectName(u"label_5") - self.label_5.setFont(font) - - self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1) - - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - - self.gridLayout.addItem(self.verticalSpacer, 3, 3, 1, 1) - - self.valuetext_y_max = QLineEdit(self.tab_2) - self.valuetext_y_max.setObjectName(u"valuetext_y_max") - - self.gridLayout.addWidget(self.valuetext_y_max, 1, 3, 1, 1) - - self.horizontalSpacer_2 = QSpacerItem(40, 5, QSizePolicy.Expanding, QSizePolicy.Minimum) - - self.gridLayout.addItem(self.horizontalSpacer_2, 2, 1, 1, 4) - - self.bool_y_log = QToolButton(self.tab_2) - self.bool_y_log.setObjectName(u"bool_y_log") - self.bool_y_log.setCheckable(True) - - self.gridLayout.addWidget(self.bool_y_log, 1, 4, 1, 1) - - self.bool_x_log = QToolButton(self.tab_2) - self.bool_x_log.setObjectName(u"bool_x_log") - self.bool_x_log.setCheckable(True) - - self.gridLayout.addWidget(self.bool_x_log, 0, 4, 1, 1) - - self.tab_widget.addTab(self.tab_2, "") - self.bool_x_log.raise_() - self.valuetext_x_max.raise_() - self.button_flip_x.raise_() - self.valuetext_x_min.raise_() - self.valuetext_y_min.raise_() - self.valuetext_y_max.raise_() - self.bool_y_log.raise_() - self.label_2.raise_() - self.label_5.raise_() - self.tab_3 = QWidget() - self.tab_3.setObjectName(u"tab_3") - self.horizontalLayout = QHBoxLayout(self.tab_3) - self.horizontalLayout.setSpacing(6) - self.horizontalLayout.setContentsMargins(11, 11, 11, 11) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.horizontalLayout.setContentsMargins(5, 5, 5, 5) - self.axes_editor = AxesEditorWidget(self.tab_3) - self.axes_editor.setObjectName(u"axes_editor") - - self.horizontalLayout.addWidget(self.axes_editor) - - self.tab_widget.addTab(self.tab_3, "") - self.tab_4 = QWidget() - self.tab_4.setObjectName(u"tab_4") - self.horizontalLayout1 = QHBoxLayout(self.tab_4) - self.horizontalLayout1.setSpacing(6) - self.horizontalLayout1.setContentsMargins(11, 11, 11, 11) - self.horizontalLayout1.setObjectName(u"horizontalLayout1") - self.horizontalLayout1.setContentsMargins(5, 5, 5, 5) - self.legend_editor = LegendEditorWidget(self.tab_4) - self.legend_editor.setObjectName(u"legend_editor") - - self.horizontalLayout1.addWidget(self.legend_editor) - - self.tab_widget.addTab(self.tab_4, "") - - self.gridLayout_5.addWidget(self.tab_widget, 9, 2, 1, 1) - - - self.retranslateUi(Widget) - - self.tab_widget.setCurrentIndex(0) - - - QMetaObject.connectSlotsByName(Widget) - # setupUi - - def retranslateUi(self, Widget): - Widget.setWindowTitle(QCoreApplication.translate("Widget", u"1D Profile", None)) - self.label_6.setText(QCoreApplication.translate("Widget", u"reference", None)) - self.label.setText(QCoreApplication.translate("Widget", u"function", None)) - self.label_3.setText(QCoreApplication.translate("Widget", u"x axis", None)) - self.bool_normalize.setText("") - self.label_7.setText(QCoreApplication.translate("Widget", u"normalize", None)) - self.text_warning.setText(QCoreApplication.translate("Widget", u"Warning", None)) - self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab), QCoreApplication.translate("Widget", u"General", None)) - self.button_flip_x.setText(QCoreApplication.translate("Widget", u"\u21c4", None)) - self.label_2.setText(QCoreApplication.translate("Widget", u"x axis", None)) - self.label_5.setText(QCoreApplication.translate("Widget", u"y axis", None)) - self.bool_y_log.setText(QCoreApplication.translate("Widget", u"log", None)) - self.bool_x_log.setText(QCoreApplication.translate("Widget", u"log", None)) - self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_2), QCoreApplication.translate("Widget", u"Limits", None)) - self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_3), QCoreApplication.translate("Widget", u"Axes", None)) - self.tab_widget.setTabText(self.tab_widget.indexOf(self.tab_4), QCoreApplication.translate("Widget", u"Legend", None)) - # retranslateUi - diff --git a/glue/viewers/common/qt/data_slice_widget.py b/glue/viewers/common/qt/data_slice_widget.py index c0c2a23c7..add7f3999 100644 --- a/glue/viewers/common/qt/data_slice_widget.py +++ b/glue/viewers/common/qt/data_slice_widget.py @@ -27,7 +27,7 @@ def __init__(self, label='', world=None, lo=0, hi=10, parent=None, world_unit=None, world_warning=False): - super(SliceWidget, self).__init__(parent=parent) + super(SliceWidget, self).__init__(parent) self.state = SliceState() self.state.label = label diff --git a/glue/viewers/image/qt/slice_widget.py b/glue/viewers/image/qt/slice_widget.py index 0184b313b..8223cae7c 100644 --- a/glue/viewers/image/qt/slice_widget.py +++ b/glue/viewers/image/qt/slice_widget.py @@ -11,7 +11,7 @@ class MultiSliceWidgetHelper(object): - def __init__(self, viewer_state=None, layout=None, *args, **kwargs): + def __init__(self, *args, viewer_state=None, layout=None, **kwargs): self.viewer_state = viewer_state @@ -60,6 +60,7 @@ def sync_state_from_sliders(self, *args): @avoid_circular def sync_sliders_from_state(self, *args): + if self.data is None or self.viewer_state.x_att is None or self.viewer_state.y_att is None: return diff --git a/glue/viewers/profile/tests/test_state.py b/glue/viewers/profile/tests/test_state.py index d7cc65913..d061265f0 100644 --- a/glue/viewers/profile/tests/test_state.py +++ b/glue/viewers/profile/tests/test_state.py @@ -49,6 +49,13 @@ def test_basic(self): assert_allclose(x, [0, 1, 2]) assert_allclose(y, [3.5, 11.5, 19.5]) + def test_basic_world(self): + self.viewer_state.x_att = self.data.world_component_ids[0] + x, y = self.layer_state.profile + assert_allclose(x, [0, 1, 2]) + assert_allclose(y, [3.5, 11.5, 19.5]) + + def test_x_att(self): self.viewer_state.x_att = self.data.pixel_component_ids[0] From 6b6bd790039b64a2734fb0584b16d8ef74287c80 Mon Sep 17 00:00:00 2001 From: Nabil Freij Date: Fri, 6 May 2022 15:12:48 -0700 Subject: [PATCH 4/5] Fix issue with sliders appearing again --- glue/viewers/profile/layer_artist.py | 2 +- glue/viewers/profile/qt/options_widget.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/glue/viewers/profile/layer_artist.py b/glue/viewers/profile/layer_artist.py index 97dd105aa..6bda5776c 100644 --- a/glue/viewers/profile/layer_artist.py +++ b/glue/viewers/profile/layer_artist.py @@ -49,7 +49,7 @@ def _calculate_profile_thread(self, reset=False): warnings.simplefilter("ignore") if reset: self.state.reset_cache() - self.state.update_profile(update_limits=False) + self.state.update_profile(update_limits=True) def _calculate_profile_postthread(self): diff --git a/glue/viewers/profile/qt/options_widget.py b/glue/viewers/profile/qt/options_widget.py index 0c750b2c6..64a1186dc 100644 --- a/glue/viewers/profile/qt/options_widget.py +++ b/glue/viewers/profile/qt/options_widget.py @@ -54,9 +54,10 @@ def _on_function_change(self, *args): else: if self.profile_slice_helper: self.profile_slice_helper.remove() + # Has to be set to None otherwise it will re-appear for other functions + self.profile_slice_helper = None def _on_attribute_change(self, *args): - if (self.viewer_state.reference_data is None or self.viewer_state.x_att_pixel is None or self.viewer_state.x_att is self.viewer_state.x_att_pixel): @@ -64,12 +65,9 @@ def _on_attribute_change(self, *args): return if self.viewer_state.function != 'slice': - world_warning = len(dependent_axes(self.viewer_state.reference_data.coords, self.viewer_state.x_att_pixel.axis)) > 1 - self.ui.text_warning.hide() - if world_warning: self.ui.text_warning.show() self.ui.text_warning.setText(WARNING_TEXT.format(label=self.viewer_state.x_att.label)) From 586d19485223e35c0cd82273b3ef8e2d312fffd8 Mon Sep 17 00:00:00 2001 From: Nabil Freij Date: Fri, 6 May 2022 17:13:34 -0700 Subject: [PATCH 5/5] undone some other changes - unsure if this is a good idea --- glue/viewers/profile/state.py | 21 +++++++++++++++++---- glue/viewers/profile/tests/test_state.py | 2 -- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/glue/viewers/profile/state.py b/glue/viewers/profile/state.py index 4a293c3a9..80267ca49 100644 --- a/glue/viewers/profile/state.py +++ b/glue/viewers/profile/state.py @@ -116,9 +116,18 @@ def _reset_x_limits(self, *event): data = self.reference_data + if self.x_att in data.pixel_component_ids: + x_min, x_max = -0.5, data.shape[self.x_att.axis] - 0.5 + else: + axis = data.world_component_ids.index(self.x_att) + axis_view = [0] * data.ndim + axis_view[axis] = slice(None) + axis_values = data[self.x_att, tuple(axis_view)] + x_min, x_max = np.nanmin(axis_values), np.nanmax(axis_values) + with delay_callback(self, 'x_min', 'x_max'): - self.x_min = -0.5 - self.x_max = data.shape[self.x_att_pixel.axis] - 0.5 + self.x_min = x_min + self.x_max = x_max def _reset_y_limits(self, *event): if self.normalize: @@ -180,8 +189,12 @@ def _reference_data_changed(self, before=None, after=None): self.x_att_helper.set_multiple_data([]) else: self.x_att_helper.set_multiple_data([self.reference_data]) - self.x_att_helper.world_coord = False - self.x_att = self.reference_data.pixel_component_ids[0] + if self._display_world: + self.x_att_helper.world_coord = True + self.x_att = self.reference_data.world_component_ids[0] + else: + self.x_att_helper.world_coord = False + self.x_att = self.reference_data.pixel_component_ids[0] self._update_x_att() diff --git a/glue/viewers/profile/tests/test_state.py b/glue/viewers/profile/tests/test_state.py index d061265f0..314054127 100644 --- a/glue/viewers/profile/tests/test_state.py +++ b/glue/viewers/profile/tests/test_state.py @@ -55,9 +55,7 @@ def test_basic_world(self): assert_allclose(x, [0, 1, 2]) assert_allclose(y, [3.5, 11.5, 19.5]) - def test_x_att(self): - self.viewer_state.x_att = self.data.pixel_component_ids[0] x, y = self.layer_state.profile assert_allclose(x, [0, 1, 2])