diff --git a/CHANGES.rst b/CHANGES.rst index 9b3ab71d2b..fa09350a51 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,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..199fe77634 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py @@ -18,6 +18,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,6 +36,11 @@ def test_spectrum_at_spaxel(cubeviz_helper, spectrum1d_cube): assert len(subsets) == 1 assert isinstance(reg, RectanglePixelRegion) + # 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 diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py index 470de5c30e..c406bc784c 100644 --- a/jdaviz/configs/cubeviz/plugins/tools.py +++ b/jdaviz/configs/cubeviz/plugins/tools.py @@ -1,13 +1,16 @@ import time import os +import bqplot from glue.config import viewer_tool from glue_jupyter.bqplot.image import BqplotImageView from glue.viewers.common.tool import CheckableTool +import numpy as np 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 +86,97 @@ 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._extra_axis = bqplot.Axis(grid_lines='none', label="", num_ticks=8, + orientation='vertical', scale=bqplot.LinearScale(), + side='right', tick_format='0.1e', + tick_style={'font-size': 15, 'font-weight': 600}, + color="#c75d2c", grid_color="#c75d2c") + 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): + # Add callbacks (click is handled by super()) + self.viewer.add_event_callback(self.on_mouse_move, events=['mousemove', 'mouseleave', 'mouseenter']) + # Get the spectrum viewer if activating for first time + if self._spectrum_viewer is None: + self._spectrum_viewer = self.viewer.jdaviz_helper.app.get_viewer('spectrum-viewer') + # Add extra y-axis to show on right hand side of spectrum viewer + if self._extra_axis not in self._spectrum_viewer.figure.axes: + self._spectrum_viewer.figure.axes.append(self._extra_axis) + # Create the mark for the preview spectrum + if self._mark is None: + scales = {} + scales['x'] = self._spectrum_viewer.native_marks[0].scales['x'] + scales['y'] = bqplot.LinearScale() + self._mark = PluginLine(self._spectrum_viewer, visible=False, scales=scales) + 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() + # Fully remove the extra axis rather than just setting to invisible + if self._extra_axis in self._spectrum_viewer.figure.axes: + self._spectrum_viewer.figure.axes.remove(self._extra_axis) + self._spectrum_viewer.figure.fig_margin['right'] = 10 + self._spectrum_viewer.figure.send_state() + super().deactivate() + + def on_mouse_move(self, data): + # Set the mark and extra axis to be invisible + if data['event'] == 'mouseleave': + self._mark.visible = False + self._extra_axis.visible=False + self._extra_axis.send_state("visible") + self._spectrum_viewer.figure.fig_margin['right'] = 10 + self._spectrum_viewer.figure.send_state("fig_margin") + #self._reset_spectrum_viewer_bounds() + return + + elif data['event'] == 'mouseenter': + # Make room for the extra axis + self._spectrum_viewer.figure.fig_margin['right'] = 80 + self._extra_axis.visible = True + + x = int(np.round(data['domain']['x'])) + y = int(np.round(data['domain']['y'])) + + # Use first visible layer for now + cube_data = [layer.layer for layer in self.viewer.layers if layer.state.visible][0] + spectrum = cube_data.get_object(statistic=None) + + # Nothing to show if we're out of bounds of the data + if x >= spectrum.flux.shape[0] or x < 0 or y >= spectrum.flux.shape[1] or y < 0: + self._mark.visible = False + self._extra_axis.visible=False + else: + # Make the mark visible and update its y values + self._mark.visible = True + 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) + # Also update the extra axis to show the correct values + float_y_min = float(np.nanmin(y_values.value)) + float_y_max = float(np.nanmax(y_values.value)) + self._extra_axis.scale.min = float_y_min + self._extra_axis.scale.max = float_y_max + + self._extra_axis.send_state(["scale", "visible"]) + self._spectrum_viewer.figure.send_state(["fig_margin", "axes"])