diff --git a/CHANGES.rst b/CHANGES.rst index b46daa84f7..9a8b4d4783 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,8 @@ Cubeviz - Moment map plugin now supports linear per-spaxel continuum subtraction. [#2587] +- Single-pixel subset tool now shows spectrum-at-spaxel on hover. [#2647] + Imviz ^^^^^ diff --git a/docs/cubeviz/displaycubes.rst b/docs/cubeviz/displaycubes.rst index 9cd89ddb2d..7a3ebb01a7 100644 --- a/docs/cubeviz/displaycubes.rst +++ b/docs/cubeviz/displaycubes.rst @@ -99,9 +99,10 @@ the bottom of the UI. Spectrum At Spaxel ================== -This tool allows the user to create a one spaxel subset in an image viewer. This subset will then be +This tool allows the user to create a single-spaxel subset in an image viewer. This subset will then be visualized in the spectrum viewer by showing the spectrum at that spaxel. -Activate this tool and then left-click to create the new region. +While this tool is active, hovering over a pixel in the image viewer will show a preview of the spectrum +at that spaxel in the spectrum viewer, and left-clicking will create a new subset at that spaxel. Click again to move the region to a new location under the cursor. Holding down the alt key (Alt key on Windows, Option key on Mac) while clicking on a spaxel creates a new subset at that point instead of moving the previously created region. @@ -110,6 +111,11 @@ You can also use the subset modes that are explained in the :ref:`Spatial Regions ` section above in the same way you would with the other subset selection tools. +Note that moving the cursor outside of the image viewer or deactivating the spectrum-at-spaxel tool +will revert the spectrum viewer zoom limits from the zoomed-in preview view to the limits set prior +to using the tool. Thus it may be necessary to reset the zoom to see any single-spaxel subset spectra +created using the tool. + .. _cubeviz-display-settings: Display Settings diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py index 7bc9cffa2b..772f5ef2ab 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py @@ -5,10 +5,11 @@ @pytest.mark.filterwarnings('ignore:No observer defined on WCS') -def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube): - cubeviz_helper.load_data(spectrum1d_cube, data_label='test') +def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube_with_uncerts): + cubeviz_helper.load_data(spectrum1d_cube_with_uncerts, data_label='test') flux_viewer = cubeviz_helper.app.get_viewer("flux-viewer") + uncert_viewer = cubeviz_helper.app.get_viewer("uncert-viewer") spectrum_viewer = cubeviz_helper.app.get_viewer("spectrum-viewer") # Set the active tool to spectrumperspaxel @@ -18,6 +19,12 @@ def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube): assert len(flux_viewer.native_marks) == 2 assert len(spectrum_viewer.data()) == 1 + # Move to spaxel location + flux_viewer.toolbar.active_tool.on_mouse_move( + {'event': 'mousemove', 'domain': {'x': x, 'y': y}, 'altKey': False}) + assert flux_viewer.toolbar.active_tool._mark in spectrum_viewer.figure.marks + assert flux_viewer.toolbar.active_tool._mark.visible is True + # Click on spaxel location flux_viewer.toolbar.active_tool.on_mouse_event( {'event': 'click', 'domain': {'x': x, 'y': y}, 'altKey': False}) @@ -30,10 +37,36 @@ def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube): assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) + # Move out of bounds + flux_viewer.toolbar.active_tool.on_mouse_move( + {'event': 'mousemove', 'domain': {'x': -1, 'y': -1}, 'altKey': False}) + assert flux_viewer.toolbar.active_tool._mark.visible is False + + # Mouse leave event + flux_viewer.toolbar.active_tool.on_mouse_move( + {'event': 'mouseleave', 'domain': {'x': x, 'y': y}, 'altKey': False}) + assert flux_viewer.toolbar.active_tool._mark.visible is False + # Deselect tool flux_viewer.toolbar.active_tool = None assert len(flux_viewer.native_marks) == 3 + # Check in uncertainty viewer as well. Set mouseover here + cubeviz_helper.app.session.application._tools['g-coords-info'].dataset.selected = 'none' + uncert_viewer.toolbar.active_tool = uncert_viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + uncert_viewer.toolbar.active_tool.on_mouse_move( + {'event': 'mousemove', 'domain': {'x': x, 'y': y}, 'altKey': False}) + assert uncert_viewer.toolbar.active_tool._mark in spectrum_viewer.figure.marks + assert uncert_viewer.toolbar.active_tool._mark.visible is True + + # Select specific data + cubeviz_helper.app.session.application._tools['g-coords-info'].dataset.selected = 'test[FLUX]' + uncert_viewer.toolbar.active_tool = uncert_viewer.toolbar.tools['jdaviz:spectrumperspaxel'] + uncert_viewer.toolbar.active_tool.on_mouse_move( + {'event': 'mousemove', 'domain': {'x': x, 'y': y}, 'altKey': False}) + assert uncert_viewer.toolbar.active_tool._mark in spectrum_viewer.figure.marks + assert uncert_viewer.toolbar.active_tool._mark.visible is True + def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): cubeviz_helper.load_data(spectrum1d_cube, data_label='test') diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 470de5c30e..d36ab7a42c 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -3,11 +3,15 @@ from glue.config import viewer_tool from glue_jupyter.bqplot.image import BqplotImageView +from glue_jupyter.bqplot.profile import BqplotProfileView from glue.viewers.common.tool import CheckableTool +import numpy as np +from specutils import Spectrum1D from jdaviz.configs.imviz.plugins.tools import _MatchedZoomMixin from jdaviz.core.events import SliceToolStateMessage from jdaviz.core.tools import PanZoom, BoxZoom, SinglePixelRegion +from jdaviz.core.marks import PluginLine __all__ = [] @@ -83,3 +87,90 @@ class SpectrumPerSpaxel(SinglePixelRegion): tool_id = 'jdaviz:spectrumperspaxel' action_text = 'See spectrum at a single spaxel' tool_tip = 'Click on the viewer and see the spectrum at that spaxel in the spectrum viewer' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._spectrum_viewer = None + self._previous_bounds = None + self._mark = None + self._data = None + + def _reset_spectrum_viewer_bounds(self): + sv_state = self._spectrum_viewer.state + sv_state.x_min = self._previous_bounds[0] + sv_state.x_max = self._previous_bounds[1] + sv_state.y_min = self._previous_bounds[2] + sv_state.y_max = self._previous_bounds[3] + + def activate(self): + self.viewer.add_event_callback(self.on_mouse_move, events=['mousemove', 'mouseleave']) + if self._spectrum_viewer is None: + # Get first profile viewer + for _, viewer in self.viewer.jdaviz_helper.app._viewer_store.items(): + if isinstance(viewer, BqplotProfileView): + self._spectrum_viewer = viewer + break + if self._mark is None: + self._mark = PluginLine(self._spectrum_viewer, visible=False) + self._spectrum_viewer.figure.marks = self._spectrum_viewer.figure.marks + [self._mark,] + # Store these so we can revert to previous user-set zoom after preview view + sv_state = self._spectrum_viewer.state + self._previous_bounds = [sv_state.x_min, sv_state.x_max, sv_state.y_min, sv_state.y_max] + super().activate() + + def deactivate(self): + self.viewer.remove_event_callback(self.on_mouse_move) + self._reset_spectrum_viewer_bounds() + super().deactivate() + + def on_mouse_move(self, data): + if data['event'] == 'mouseleave': + self._mark.visible = False + self._reset_spectrum_viewer_bounds() + return + + x = int(np.round(data['domain']['x'])) + y = int(np.round(data['domain']['y'])) + + # Use the selected layer from coords_info as long as it's 3D + coords_dataset = self.viewer.session.application._tools['g-coords-info'].dataset.selected + if coords_dataset == 'auto': + cube_data = self.viewer.active_image_layer.layer + elif coords_dataset == 'none': + if len(self.viewer.layers): + cube_data = self.viewer.layers[0].layer + else: + return + else: + cube_data = self.viewer.session.application._tools['g-coords-info'].dataset.selected_obj + + data_shape = cube_data.ndim if hasattr(cube_data, "ndim") else len(cube_data.shape) + if data_shape != 3: + cube_data = [layer.layer for layer in self.viewer.layers if layer.state.visible + and layer.layer.ndim == 3] + if len(cube_data) == 0: + return + cube_data = cube_data[0] + + if isinstance(cube_data, Spectrum1D): + spectrum = cube_data + else: + spectrum = cube_data.get_object(statistic=None) + # Note: change this when Spectrum1D.with_spectral_axis is fixed. + x_unit = self._spectrum_viewer.state.x_display_unit + if spectrum.spectral_axis.unit != x_unit: + new_spectral_axis = spectrum.spectral_axis.to(x_unit) + spectrum = Spectrum1D(spectrum.flux, new_spectral_axis) + + if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: + self._reset_spectrum_viewer_bounds() + self._mark.visible = False + else: + y_values = spectrum.flux[x, y, :] + if np.all(np.isnan(y_values)): + self._mark.visible = False + return + self._mark.update_xy(spectrum.spectral_axis.value, y_values) + self._mark.visible = True + self._spectrum_viewer.state.y_max = np.nanmax(y_values.value) * 1.2 + self._spectrum_viewer.state.y_min = np.nanmin(y_values.value) * 0.8