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/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..397066730 100644 --- a/glue/core/data.py +++ b/glue/core/data.py @@ -241,7 +241,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 +1659,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) diff --git a/glue/utils/wcs.py b/glue/utils/wcs.py new file mode 100644 index 000000000..3c2c6d368 --- /dev/null +++ b/glue/utils/wcs.py @@ -0,0 +1,23 @@ +from astropy.wcs import WCS + + +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/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..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): + def __init__(self, *args, viewer_state=None, layout=None, **kwargs): self.viewer_state = viewer_state @@ -56,6 +56,7 @@ 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 def sync_sliders_from_state(self, *args): 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 9402bae2d..6bda5776c 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], drawstyle='steps-mid', + color=self.state.layer.style.color)[0] self.mpl_artists = [self.plot_artist] @defer_draw @@ -51,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): @@ -122,7 +120,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 +129,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..64a1186dc 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,24 +35,45 @@ 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_attribute_change(self, *args): + 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() + # 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): 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 world_warning: - self.ui.text_warning.show() - self.ui.text_warning.setText(WARNING_TEXT.format(label=self.viewer_state.x_att.label)) - else: + 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() - 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..80267ca49 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 ' @@ -56,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) @@ -82,7 +88,10 @@ 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): + """ + Defines ``self.x_att_pixel`` in the viewer state. + """ if self.x_att is not None: if self._display_world: if self.x_att in self.reference_data.pixel_component_ids: @@ -187,7 +196,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() @@ -201,6 +210,39 @@ 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 + 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 self.x_att_pixel and 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 +263,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, session=session) - super(ProfileLayerState, self).__init__(layer=layer, viewer_state=viewer_state) + self.viewer_state = viewer_state - self.attribute_att_helper = ComponentIDComboHelper(self, 'attribute', - numeric=True, categorical=False) + 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 +347,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 +358,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 +380,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 +404,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..314054127 100644 --- a/glue/viewers/profile/tests/test_state.py +++ b/glue/viewers/profile/tests/test_state.py @@ -46,17 +46,16 @@ def setup_method(self, method): def test_basic(self): 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_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): - self.viewer_state.x_att = self.data.pixel_component_ids[0] x, y = self.layer_state.profile assert_allclose(x, [0, 1, 2]) @@ -94,6 +93,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 +104,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 +161,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()