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