From 5b3e32de71fba2179840598f413dcafde909e1fa Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Thu, 12 May 2022 13:26:07 -0400 Subject: [PATCH 1/2] Cubeviz image viewer coordinates display --- CHANGES.rst | 2 + docs/cubeviz/displaycubes.rst | 10 ++++ docs/imviz/displayimages.rst | 2 + jdaviz/configs/cubeviz/cubeviz.yaml | 2 + jdaviz/configs/cubeviz/plugins/viewers.py | 72 +++++++++++++++++++++++ 5 files changed, 88 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4db97dc98d..7c92efe55d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,8 @@ New Features Cubeviz ^^^^^^^ +- Cubeviz image viewer now has coordinates info panel like Imviz. [#1315] + Imviz ^^^^^ diff --git a/docs/cubeviz/displaycubes.rst b/docs/cubeviz/displaycubes.rst index 4c31dec13d..517193c112 100644 --- a/docs/cubeviz/displaycubes.rst +++ b/docs/cubeviz/displaycubes.rst @@ -195,3 +195,13 @@ The spectrum of colors used to visualize data can be changed using this drop dow :ref:`Plot Settings (Specviz) ` Plot settings for the spectrum 1D viewer. + +.. _cubeviz_cursor_info: + +Cursor Information +================== + +By moving your cursor along the image viewer, you will be able to see information on the +cursor's location in pixel space (X and Y), the RA and Dec at that point, and the value +of the data there. This information is located in the top bar of the UI, on the +middle-right side. diff --git a/docs/imviz/displayimages.rst b/docs/imviz/displayimages.rst index ed7ce9ec7d..774e48dee2 100644 --- a/docs/imviz/displayimages.rst +++ b/docs/imviz/displayimages.rst @@ -54,6 +54,8 @@ and bias. Moving along the X-axis will change the bias and moving along the Y-ax contrast. If you would like to reset to the default contrast and bias settings, you can double-click on the display while the mode is active. +.. _imviz_cursor_info: + Cursor Information ================== diff --git a/jdaviz/configs/cubeviz/cubeviz.yaml b/jdaviz/configs/cubeviz/cubeviz.yaml index eb93e867a0..4660ed783d 100644 --- a/jdaviz/configs/cubeviz/cubeviz.yaml +++ b/jdaviz/configs/cubeviz/cubeviz.yaml @@ -8,12 +8,14 @@ settings: toolbar: true tray: true tab_headers: false + dense_toolbar: false context: notebook: max_height: 600px toolbar: - g-data-tools - g-subset-tools + - g-coords-info tray: - g-plot-options - g-subset-plugin diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index f2ade4002e..c88c889d56 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -1,9 +1,11 @@ +import numpy as np from glue.core import BaseData from glue_jupyter.bqplot.image import BqplotImageView from jdaviz.core.registries import viewer_registry from jdaviz.core.marks import SliceIndicator from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin +from jdaviz.configs.imviz.helper import data_has_valid_wcs from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView __all__ = ['CubevizImageView', 'CubevizProfileView'] @@ -37,6 +39,76 @@ def __init__(self, *args, **kwargs): self._initialize_toolbar_nested() self.state.add_callback('reference_data', self._initial_x_axis) + self.label_mouseover = None + self.add_event_callback(self.on_mouse_or_key_event, events=['mousemove', 'mouseenter', + 'mouseleave']) + + def on_mouse_or_key_event(self, data): + + # Find visible layers + visible_layers = [layer for layer in self.state.layers if layer.visible] + + if len(visible_layers) == 0: + return + + if self.label_mouseover is None: + if 'g-coords-info' in self.session.application._tools: + self.label_mouseover = self.session.application._tools['g-coords-info'] + else: + return + + if data['event'] == 'mousemove': + # Display the current cursor coordinates (both pixel and world) as + # well as data values. For now we use the first dataset in the + # viewer for the data values. + + # Extract first dataset from visible layers and use this for coordinates - the choice + # of dataset shouldn't matter if the datasets are linked correctly + image = visible_layers[0].layer + + # Extract data coordinates - these are pixels in the reference image + x = data['domain']['x'] + y = data['domain']['y'] + + if x is None or y is None: # Out of bounds + self.label_mouseover.pixel = "" + self.label_mouseover.reset_coords_display() + self.label_mouseover.value = "" + return + + maxsize = int(np.ceil(np.log10(np.max(image.shape[:2])))) + 3 + fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' + self.label_mouseover.pixel = (fmt.format(x, y)) + + if data_has_valid_wcs(image): + try: + coo = image.coords.pixel_to_world(x, y, self.state.slices[-1])[-1].icrs + except Exception: + self.label_mouseover.reset_coords_display() + else: + self.label_mouseover.set_coords(coo) + else: + self.label_mouseover.reset_coords_display() + + # Extract data values at this position. + # Assume shape is [x, y, z] and not [y, x] like Imviz. + if (x > -0.5 and y > -0.5 + and x < image.shape[0] - 0.5 and y < image.shape[1] - 0.5 + and hasattr(visible_layers[0], 'attribute')): + attribute = visible_layers[0].attribute + value = image.get_data(attribute)[int(round(x)), int(round(y)), + self.state.slices[-1]] + unit = image.get_component(attribute).units + self.label_mouseover.value = f'{value:+10.5e} {unit}' + else: + self.label_mouseover.value = '' + + elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': + + self.label_mouseover.pixel = "" + self.label_mouseover.reset_coords_display() + self.label_mouseover.value = "" + def _initial_x_axis(self, *args): # Make sure that the x_att is correct on data load ref_data = self.state.reference_data From 4cf665cf0b2866df6e3dc064e8fa61bb013d4508 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Mon, 16 May 2022 17:13:30 -0400 Subject: [PATCH 2/2] BUG: Fixed data_label not propagated for Spectrum1D in Cubeviz. TST: Add Cubeviz coords info tests. DOC: Update change log. --- CHANGES.rst | 3 ++ .../moment_maps/tests/test_moment_maps.py | 25 ++++++++--- jdaviz/configs/cubeviz/plugins/parsers.py | 28 +++++++----- .../cubeviz/plugins/tests/test_parsers.py | 45 +++++++++++++++++++ 4 files changed, 83 insertions(+), 18 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7c92efe55d..a6d98771a2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -48,6 +48,9 @@ Bug Fixes Cubeviz ^^^^^^^ +- Parser now respects user-provided ``data_label`` when ``Spectrum1D`` + object is loaded. Previously, it only had effect on FITS data. [#1315] + Imviz ^^^^^ diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index 007b8f24f3..8f9a106032 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -8,12 +8,10 @@ @pytest.mark.filterwarnings('ignore:No observer defined on WCS') def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): - app = cubeviz_helper.app - dc = app.data_collection - app.add_data(spectrum1d_cube, 'test[FLUX]') - app.add_data_to_viewer('flux-viewer', 'test[FLUX]') + dc = cubeviz_helper.app.data_collection + cubeviz_helper.load_data(spectrum1d_cube, data_label='test') - mm = MomentMap(app=app) + mm = MomentMap(app=cubeviz_helper.app) mm.dataset_selected = 'test[FLUX]' mm.n_moment = 0 # Collapsed sum, will get back 2D spatial image @@ -30,11 +28,20 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): assert mm.results_label_overwrite is True result = dc[1].get_object(cls=CCDData) - assert result.shape == (2, 4) # Cube shape is (2, 2, 4) + assert result.shape == (4, 2) # Cube shape is (2, 2, 4) # FIXME: Need spatial WCS, see https://github.com/spacetelescope/jdaviz/issues/1025 assert dc[1].coords is None + # Make sure coordinate display still works + flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') + flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert flux_viewer.state.slices == (0, 0, 1) + assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' + assert flux_viewer.label_mouseover.value == '+8.00000e+00 Jy' # Slice 0 has 8 pixels, this is Slice 1 # noqa + assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755346' + assert flux_viewer.label_mouseover.world_dec_deg == '27.0000999998' + assert mm.filename == 'moment0_test_FLUX.fits' # Auto-populated on calculate. mm.filename = str(tmpdir.join(mm.filename)) # But we want it in tmpdir for testing. mm.vue_save_as_fits() @@ -52,3 +59,9 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): assert dc[2].label == 'moment 1' assert len(dc.links) == 10 + + # Coordinate display should be unaffected. + assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' + assert flux_viewer.label_mouseover.value == '+8.00000e+00 Jy' # Slice 0 has 8 pixels, this is Slice 1 # noqa + assert flux_viewer.label_mouseover.world_ra_deg == '204.9997755346' + assert flux_viewer.label_mouseover.world_dec_deg == '27.0000999998' diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 35291a4a44..58daf7a033 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -75,9 +75,9 @@ def parse_data(app, file_obj, data_type=None, data_label=None): # into something glue can understand. elif isinstance(file_obj, Spectrum1D): if file_obj.flux.ndim == 3: - _parse_spectrum1d_3d(app, file_obj) + _parse_spectrum1d_3d(app, file_obj, data_label=data_label) else: - _parse_spectrum1d(app, file_obj) + _parse_spectrum1d(app, file_obj, data_label=data_label) else: raise NotImplementedError(f'Unsupported data format: {file_obj}') @@ -202,8 +202,11 @@ def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', viewer_name='flux-viewe app.add_data_to_viewer('spectrum-viewer', data_label) -def _parse_spectrum1d_3d(app, file_obj): - # Load spectrum1d as a cube +def _parse_spectrum1d_3d(app, file_obj, data_label=None): + """Load spectrum1d as a cube.""" + + if data_label is None: + data_label = "Unknown spectrum object" for attr in ["flux", "mask", "uncertainty"]: val = getattr(file_obj, attr) @@ -224,20 +227,21 @@ def _parse_spectrum1d_3d(app, file_obj): s1d = Spectrum1D(flux=flux, wcs=file_obj.wcs) - data_label = f"Unknown spectrum object[{attr.upper()}]" - app.add_data(s1d, data_label) + cur_data_label = f"{data_label}[{attr.upper()}]" + app.add_data(s1d, cur_data_label) if attr == 'flux': - app.add_data_to_viewer('flux-viewer', data_label) - app.add_data_to_viewer('spectrum-viewer', data_label) + app.add_data_to_viewer('flux-viewer', cur_data_label) + app.add_data_to_viewer('spectrum-viewer', cur_data_label) elif attr == 'mask': - app.add_data_to_viewer('mask-viewer', data_label) + app.add_data_to_viewer('mask-viewer', cur_data_label) else: # 'uncertainty' - app.add_data_to_viewer('uncert-viewer', data_label) + app.add_data_to_viewer('uncert-viewer', cur_data_label) -def _parse_spectrum1d(app, file_obj): - data_label = "Unknown spectrum object" +def _parse_spectrum1d(app, file_obj, data_label=None): + if data_label is None: + data_label = "Unknown spectrum object" # TODO: glue-astronomy translators only look at the flux property of # specutils Spectrum1D objects. Fix to support uncertainties and masks. diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index e0eb9b3b78..4da5089072 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -56,6 +56,29 @@ def test_fits_image_hdu_parse_from_file(tmpdir, image_hdu_obj, cubeviz_helper): assert len(cubeviz_helper.app.data_collection) == 3 assert cubeviz_helper.app.data_collection[0].label.endswith('[FLUX]') + # This tests the same data as test_fits_image_hdu_parse above. + + flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') + flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' + assert flux_viewer.label_mouseover.value == '+1.00000e+00 1e-17 erg / (Angstrom cm2 pix s)' + assert flux_viewer.label_mouseover.world_ra_deg == '205.4433848390' + assert flux_viewer.label_mouseover.world_dec_deg == '26.9996149270' + + unc_viewer = cubeviz_helper.app.get_viewer('uncert-viewer') + unc_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) + assert unc_viewer.label_mouseover.pixel == 'x=-1.0 y=00.0' + assert unc_viewer.label_mouseover.value == '' # Out of bounds + assert unc_viewer.label_mouseover.world_ra_deg == '205.4441642302' + assert unc_viewer.label_mouseover.world_dec_deg == '26.9996148973' + + mask_viewer = cubeviz_helper.app.get_viewer('mask-viewer') + mask_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 9, 'y': 0}}) + assert mask_viewer.label_mouseover.pixel == 'x=09.0 y=00.0' + assert mask_viewer.label_mouseover.value == '+0.00000e+00 ct' # No unit define, ct as fallback + assert mask_viewer.label_mouseover.world_ra_deg == '205.4441642302' + assert mask_viewer.label_mouseover.world_dec_deg == '26.9996148973' + @pytest.mark.filterwarnings('ignore') def test_spectrum3d_parse(image_hdu_obj, cubeviz_helper): @@ -67,6 +90,24 @@ def test_spectrum3d_parse(image_hdu_obj, cubeviz_helper): assert len(cubeviz_helper.app.data_collection) == 1 assert cubeviz_helper.app.data_collection[0].label.endswith('[FLUX]') + # Same as flux viewer data in test_fits_image_hdu_parse_from_file + flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') + flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) + assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' + assert flux_viewer.label_mouseover.value == '+1.00000e+00 1e-17 erg / (Angstrom cm2 pix s)' + assert flux_viewer.label_mouseover.world_ra_deg == '205.4433848390' + assert flux_viewer.label_mouseover.world_dec_deg == '26.9996149270' + + # These viewers have no data. + + unc_viewer = cubeviz_helper.app.get_viewer('uncert-viewer') + unc_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': -1, 'y': 0}}) + assert unc_viewer.label_mouseover is None + + mask_viewer = cubeviz_helper.app.get_viewer('mask-viewer') + mask_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 9, 'y': 0}}) + assert mask_viewer.label_mouseover is None + def test_spectrum1d_parse(spectrum1d, cubeviz_helper): cubeviz_helper.load_data(spectrum1d) @@ -74,6 +115,10 @@ def test_spectrum1d_parse(spectrum1d, cubeviz_helper): assert len(cubeviz_helper.app.data_collection) == 1 assert cubeviz_helper.app.data_collection[0].label.endswith('[FLUX]') + # Coordinate display is only for spatial image, which is missing here. + flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') + assert flux_viewer.label_mouseover is None + def test_numpy_cube(cubeviz_helper): with pytest.raises(NotImplementedError, match='Unsupported data format'):