From 2746b720872a9a5c063cc5291e611a18bfba9fba Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 28 Nov 2023 14:32:48 -0500 Subject: [PATCH] specviz2d/mosviz: matched mouseover markers (#2575) * specviz2d/mosviz matched mouseover markers * update tests to include wave in mouseover info * update tests for extra mouseover mark --- CHANGES.rst | 4 + .../imviz/plugins/coords_info/coords_info.py | 78 ++++++++++++++++--- .../configs/mosviz/tests/test_data_loading.py | 3 +- .../tests/test_spectral_extraction.py | 6 +- .../configs/specviz2d/tests/test_parsers.py | 5 +- jdaviz/core/marks.py | 4 +- 6 files changed, 83 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3a0f51d2bb..e3c633b83c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -44,12 +44,16 @@ Imviz Mosviz ^^^^^^ +- Matched mouseover indicator to show same position in 1d and 2d spectral viewers. [#2575] + Specviz ^^^^^^^ Specviz2d ^^^^^^^^^ +- Matched mouseover indicator to show same position in 1d and 2d spectral viewers. [#2575] + API Changes ----------- diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 126d1856be..ac90178d12 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -3,17 +3,19 @@ from traitlets import Bool, Unicode, observe from astropy import units as u +from bqplot import LinearScale from glue.core import BaseData from glue.core.subset_group import GroupedSubset from glue_jupyter.bqplot.image.layer_artist import BqplotImageSubsetLayerArtist from jdaviz.configs.cubeviz.plugins.viewers import CubevizImageView from jdaviz.configs.imviz.plugins.viewers import ImvizImageView -from jdaviz.configs.mosviz.plugins.viewers import MosvizImageView, MosvizProfile2DView +from jdaviz.configs.mosviz.plugins.viewers import (MosvizImageView, MosvizProfileView, + MosvizProfile2DView) from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView from jdaviz.core.events import ViewerAddedMessage from jdaviz.core.helpers import data_has_valid_wcs -from jdaviz.core.marks import PluginScatter +from jdaviz.core.marks import PluginScatter, PluginLine from jdaviz.core.registries import tool_registry from jdaviz.core.template_mixin import TemplateMixin, DatasetSelectMixin from jdaviz.utils import get_subset_type @@ -31,7 +33,7 @@ class CoordsInfo(TemplateMixin, DatasetSelectMixin): MosvizImageView, MosvizProfile2DView) - _viewer_classes_with_marker = (SpecvizProfileView,) + _viewer_classes_with_marker = (SpecvizProfileView, MosvizProfile2DView) dataset_icon = Unicode("").tag(sync=True) # option for layer (auto, none, or specific layer) icon = Unicode("").tag(sync=True) # currently exposed layer @@ -79,9 +81,25 @@ def _create_marks_for_viewer(self, viewer, id=None): id = viewer.reference_id if id in self._marks: return - self._marks[id] = PluginScatter(viewer, - marker='rectangle', stroke_width=1, - visible=False) + if isinstance(viewer, MosvizProfile2DView): + self._marks[id] = PluginLine(viewer, + x=[0, 0], y=[0, 1], + scales={'x': viewer.scales['x'], + 'y': LinearScale(min=0, max=1)}, + visible=False) + else: + self._marks[id] = PluginScatter(viewer, + marker='rectangle', stroke_width=1, + visible=False) + if isinstance(viewer, MosvizProfileView): + matched_id = f"{id}:matched" + self._marks[matched_id] = PluginLine(viewer, + x=[0, 0], y=[0, 1], + scales={'x': viewer.scales['x'], + 'y': LinearScale(min=0, max=1)}, + visible=False) + viewer.figure.marks = viewer.figure.marks + [self._marks[matched_id]] + viewer.figure.marks = viewer.figure.marks + [self._marks[id]] def _create_viewer_callbacks(self, viewer): @@ -112,6 +130,16 @@ def marks(self): self._create_marks_for_viewer(viewer, id) return self._marks + @property + def _matched_markers(self): + if self.app.config == 'specviz2d': + return {'specviz2d-0': ['specviz2d-1:matched'], + 'specviz2d-1': ['specviz2d-0']} + if self.app.config == 'mosviz': + return {'mosviz-1': ['mosviz-2:matched'], + 'mosviz-2': ['mosviz-1']} + return {} + def as_text(self): return (f"{self.row1a_title} {self.row1a_text} {self.row1b_title} {self.row1b_text}".strip(), # noqa f"{self.row2_title} {self.row2_text}".strip(), @@ -140,9 +168,11 @@ def reset_coords_display(self): def _viewer_mouse_clear_event(self, viewer, data=None): self.reset_coords_display() - marks = self.marks.get(viewer._reference_id) - if marks is not None: - marks.visible = False + marker_ids = [viewer._reference_id] + self._matched_markers.get(viewer._reference_id, []) + for marker_id in marker_ids: + marks = self.marks.get(marker_id) + if marks is not None: + marks.visible = False def _viewer_mouse_event(self, viewer, data): if data['event'] in ('mouseleave', 'mouseenter'): @@ -336,6 +366,17 @@ def _image_viewer_update(self, viewer, x, y): # TODO: use sky directly, but need to figure out how to have a compatible "blank" entry self._dict['world'] = (sky.ra.value, sky.dec.value) self._dict['world:unreliable'] = unreliable_world + elif isinstance(viewer, MosvizProfile2DView) and hasattr(getattr(image, 'coords', None), + 'pixel_to_world_values'): + # use WCS to expose the wavelength for a 2d spectrum shown in pixel space + wave, pixel = image.coords.pixel_to_world(x, y) + self.row2_title = 'Wave' + self.row2_text = f'{wave.value:10.5e} {wave.unit.to_string()}' + self.row2_unreliable = False + + self.row3_title = '\u00A0' + self.row3_text = "" + self.row3_unreliable = False else: self.row2_title = '\u00A0' self.row2_text = "" @@ -397,6 +438,20 @@ def _image_viewer_update(self, viewer, x, y): self.row1b_title = '' self.row1b_text = '' + if isinstance(viewer, MosvizProfile2DView): + self.marks[viewer._reference_id].update_xy([x, x], [0, 1]) + self.marks[viewer._reference_id].visible = True + + for matched_marker_id in self._matched_markers.get(viewer._reference_id, []): + if hasattr(getattr(image, 'coords', None), 'pixel_to_world_values'): + # should already have wave computed from setting the coords-info + matched_viewer = self.app.get_viewer(matched_marker_id.split(':matched')[0]) + wave = wave.to_value(matched_viewer.state.x_display_unit) + self.marks[matched_marker_id].update_xy([wave, wave], [0, 1]) + self.marks[matched_marker_id].visible = True + else: + self.marks[matched_marker_id].visible = False + def _spectrum_viewer_update(self, viewer, x, y): def _cursor_fallback(): self._dict['axes_x'] = x @@ -541,4 +596,9 @@ def _copy_axes_to_spectral(): self.marks[viewer._reference_id].update_xy([closest_wave], [closest_flux]) self.marks[viewer._reference_id].visible = True + for matched_marker_id in self._matched_markers.get(viewer._reference_id, []): + # NOTE: this currently assumes the the matched marker is a vertical line with a + # normalized y-scale + self.marks[matched_marker_id].update_xy([closest_i, closest_i], [0, 1]) + self.marks[matched_marker_id].visible = True _copy_axes_to_spectral() diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index 67d73ffe1c..21b0865bc9 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -222,7 +222,8 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ label_mouseover._viewer_mouse_event(spec2d_viewer, {'event': 'mousemove', 'domain': {'x': 10, 'y': 100}}) - assert label_mouseover.as_text() == ('Pixel x=00010.0 y=00100.0 Value +8.12986e-01', '', '') + assert label_mouseover.as_text() == ('Pixel x=00010.0 y=00100.0 Value +8.12986e-01', + 'Wave 1.10000e-05 m', '') assert label_mouseover.icon == 'c' # need to trigger a mouseleave or mouseover to reset the traitlets diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py index 9f3dfa4963..c641b4a055 100644 --- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -22,10 +22,10 @@ def test_plugin(specviz2d_helper): # test trace marks - won't be created until after opening the plugin sp2dv = specviz2d_helper.app.get_viewer('spectrum-2d-viewer') - assert len(sp2dv.figure.marks) == 2 + assert len(sp2dv.figure.marks) == 3 pext.keep_active = True - assert len(sp2dv.figure.marks) == 11 + assert len(sp2dv.figure.marks) == 12 assert pext.marks['trace'].visible is True assert len(pext.marks['trace'].x) > 0 @@ -66,7 +66,7 @@ def test_plugin(specviz2d_helper): # TODO: Investigate extra hidden mark from glue-jupyter, see # https://github.com/spacetelescope/jdaviz/pull/2478#issuecomment-1731864411 # 3 new trace objects should have been loaded and plotted in the spectrum-2d-viewer - assert len(sp2dv.figure.marks) == 15 + assert len(sp2dv.figure.marks) == 16 # interact with background section, check marks pext.trace_trace_selected = 'New Trace' diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py index cf7140975f..2f24c8b3eb 100644 --- a/jdaviz/configs/specviz2d/tests/test_parsers.py +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -32,7 +32,7 @@ def test_2d_parser_jwst(specviz2d_helper): label_mouseover._viewer_mouse_event(viewer_2d, {'event': 'mousemove', 'domain': {'x': 350, 'y': 30}}) assert label_mouseover.as_text() == ('Pixel x=0350.0 y=0030.0 Value +3.22142e+04 MJy / sr', - '', '') + 'Wave 6.24985e+00 um', '') @pytest.mark.remote_data @@ -72,7 +72,8 @@ def test_2d_parser_no_unit(specviz2d_helper, mos_spectrum2d): label_mouseover = specviz2d_helper.app.session.application._tools['g-coords-info'] label_mouseover._viewer_mouse_event(viewer_2d, {'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) - assert label_mouseover.as_text() == ('Pixel x=00000.0 y=00000.0 Value +3.74540e-01', '', '') + assert label_mouseover.as_text() == ('Pixel x=00000.0 y=00000.0 Value +3.74540e-01', + 'Wave 1.00000e-06 m', '') assert label_mouseover.icon == 'a' viewer_1d = specviz2d_helper.app.get_viewer('spectrum-viewer') diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py index edf5d46d90..344445d598 100644 --- a/jdaviz/core/marks.py +++ b/jdaviz/core/marks.py @@ -581,7 +581,7 @@ def __init__(self, viewer, x=[], y=[], **kwargs): self.viewer = viewer # color is same blue as import button kwargs.setdefault('colors', [accent_color]) - super().__init__(x=x, y=y, scales=viewer.scales, **kwargs) + super().__init__(x=x, y=y, scales=kwargs.pop('scales', viewer.scales), **kwargs) class PluginScatter(Scatter, PluginMark, HubListener): @@ -589,7 +589,7 @@ def __init__(self, viewer, x=[], y=[], **kwargs): self.viewer = viewer # default color is same blue as import button kwargs.setdefault('colors', [accent_color]) - super().__init__(x=x, y=y, scales=viewer.scales, **kwargs) + super().__init__(x=x, y=y, scales=kwargs.pop('scales', viewer.scales), **kwargs) class LineAnalysisContinuum(PluginLine):