From cb6c1f8c2a82fceb88b84a2fdd3c00d51f479574 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:46:52 -0500 Subject: [PATCH 1/9] Improved spectrum info panel for spectrum viewers in all the configurations supported by Jdaviz. --- CHANGES.rst | 8 ++ docs/specviz/displaying.rst | 10 ++ .../tests/test_gaussian_smooth.py | 123 +++++++++++++++--- .../imviz/plugins/coords_info/coords_info.py | 6 + .../imviz/plugins/coords_info/coords_info.vue | 6 +- jdaviz/configs/imviz/tests/test_linking.py | 1 + .../configs/mosviz/tests/test_data_loading.py | 24 +++- jdaviz/configs/specviz/plugins/viewers.py | 73 +++++++++-- jdaviz/configs/specviz/tests/test_helper.py | 49 ++++++- .../configs/specviz2d/tests/test_parsers.py | 15 ++- 10 files changed, 273 insertions(+), 42 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3da545acab..e111553568 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -110,6 +110,8 @@ Cubeviz - Added Slice plugin player control buttons. [#1848] +- Improved mouseover info display for spectrum viewer. [#1894] + Imviz ^^^^^ @@ -130,6 +132,8 @@ Mosviz - ``load_data`` method can now load JWST NIRCam and NIRSpec level 2 data. [#1835] +- Improved mouseover info display for spectrum viewer. [#1894] + Specviz ^^^^^^^ @@ -137,6 +141,8 @@ Specviz - Switch to opt-in concatenation for multi-order x1d spectra. [#1659] +- Improved mouseover info display for spectrum viewer. [#1894] + Specviz2d ^^^^^^^^^ @@ -145,6 +151,8 @@ Specviz2d - Add dropdown for choosing background statistic (average or median). [#1922] +- Improved mouseover info display for spectrum viewer. [#1894] + API Changes ----------- diff --git a/docs/specviz/displaying.rst b/docs/specviz/displaying.rst index fd6d6c628a..08bf91879b 100644 --- a/docs/specviz/displaying.rst +++ b/docs/specviz/displaying.rst @@ -41,6 +41,16 @@ data menu. .. image:: img/data_tab.png +.. _specviz_cursor_info: + +Cursor Information +================== + +By moving your cursor along the spectrum viewer, you will be able to see information on the +index, spectral axis value, and flux value of the closest data point to the cursor +(not to be confused with the actual cursor position). +This information is displayed in the top bar of the UI, on the middle-right side. + Home ==== diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py index 4f9ad23781..3b887145a5 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py @@ -1,21 +1,18 @@ import pytest -from jdaviz import Application +from astropy.utils.exceptions import AstropyUserWarning from specutils import Spectrum1D -from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth - -def test_linking_after_spectral_smooth(spectrum1d_cube): - - app = Application(configuration="cubeviz") +def test_linking_after_spectral_smooth(cubeviz_helper, spectrum1d_cube): + app = cubeviz_helper.app dc = app.data_collection - app.add_data(spectrum1d_cube, 'test') - app.add_data_to_viewer('flux-viewer', 'test') + cubeviz_helper.load_data(spectrum1d_cube, data_label='test') + spec_viewer = cubeviz_helper.app.get_viewer('spectrum-viewer') assert len(dc) == 1 - gs = GaussianSmooth(app=app) - gs.dataset_selected = 'test' + gs = cubeviz_helper.plugins['Gaussian Smooth']._obj + gs.dataset_selected = 'test[FLUX]' gs.mode_selected = 'Spectral' gs.stddev = 3.2 gs.add_to_viewer_selected = 'None' @@ -40,8 +37,8 @@ def test_linking_after_spectral_smooth(spectrum1d_cube): # itself is prepended to the default label, and there is no longer # an overwrite warning. assert len(gs.dataset_items) == 2 - assert gs.dataset_selected == 'test' - assert gs.results_label == 'test spectral-smooth stddev-3.2' + assert gs.dataset_selected == 'test[FLUX]' + assert gs.results_label == 'test[FLUX] spectral-smooth stddev-3.2' assert gs.results_label_overwrite is False assert len(dc) == 2 @@ -68,21 +65,109 @@ def test_linking_after_spectral_smooth(spectrum1d_cube): assert dc.external_links[2].cids1[0] == dc[0].pixel_component_ids[2] assert dc.external_links[2].cids2[0] == dc[-1].pixel_component_ids[2] + # Mouseover should automatically jump from one spectrum + # to another, depending on which one is closer. + + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 60}}) + assert spec_viewer.label_mouseover.pixel == 'x=01.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '4.62360e-07' + assert spec_viewer.label_mouseover.world_dec == 'm' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '9.20000e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'a' + + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 20}}) + assert spec_viewer.label_mouseover.pixel == 'x=01.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '4.62360e-07' + assert spec_viewer.label_mouseover.world_dec == 'm' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '1.47943e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'b' + + # Check mouseover behavior when we hide everything. + for lyr in spec_viewer.layers: + lyr.visible = False + + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 60}}) + assert spec_viewer.label_mouseover.pixel == '' + assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' + assert spec_viewer.label_mouseover.world_ra == '' + assert spec_viewer.label_mouseover.world_dec == '' + assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' + assert spec_viewer.label_mouseover.world_ra_deg == '' + assert spec_viewer.label_mouseover.world_dec_deg == '' + assert spec_viewer.label_mouseover.icon == '' + -@pytest.mark.filterwarnings("ignore::UserWarning") def test_spatial_convolution(cubeviz_helper, spectrum1d_cube): dc = cubeviz_helper.app.data_collection - cubeviz_helper.app.add_data(spectrum1d_cube, 'test') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'test') + cubeviz_helper.load_data(spectrum1d_cube, data_label='test') - gs = GaussianSmooth(app=cubeviz_helper.app) - gs.dataset_selected = 'test' + gs = cubeviz_helper.plugins['Gaussian Smooth']._obj + gs.dataset_selected = 'test[FLUX]' gs.mode_selected = 'Spatial' gs.stddev = 3 assert gs.results_label == 'spatial-smooth stddev-3.0' - gs.vue_apply() + with pytest.warns( + AstropyUserWarning, + match='The following attributes were set on the data object, but will be ignored'): + gs.vue_apply() assert len(dc) == 2 assert dc[1].label == "spatial-smooth stddev-3.0" + assert dc[1].shape == (2, 4, 2) # specutils moved spectral axis to last assert (dc["spatial-smooth stddev-3.0"].get_object(cls=Spectrum1D, statistic=None).shape - == (4, 2, 2)) + == (2, 4, 2)) + + +def test_spectrum1d_smooth(specviz_helper, spectrum1d): + dc = specviz_helper.app.data_collection + specviz_helper.load_data(spectrum1d, data_label='test') + spec_viewer = specviz_helper.app.get_viewer('spectrum-viewer') + + gs = specviz_helper.plugins['Gaussian Smooth']._obj + gs.dataset_selected = 'test' + gs.mode_selected = 'Spectral' + gs.stddev = 10 + gs.vue_apply() + + assert len(dc) == 2 + assert dc[1].label == 'smooth stddev-10.0' + + # Mouseover should automatically jump from one spectrum + # to another, depending on which one is closer. + + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6400, 'y': 120}}) + assert spec_viewer.label_mouseover.pixel == 'x=02.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '6.44444e+03' + assert spec_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '1.35366e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'a' + + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6400, 'y': 5}}) + assert spec_viewer.label_mouseover.pixel == 'x=02.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '6.44444e+03' + assert spec_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '5.34688e+00' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'b' + + # Out-of-bounds should lock to closest edge value. + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 5500, 'y': 120}}) + assert spec_viewer.label_mouseover.pixel == 'x=00.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '6.00000e+03' + assert spec_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '1.24967e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'a' diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 46a0c0cfdb..b90d22a38d 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -13,6 +13,7 @@ class CoordsInfo(TemplateMixin): pixel = Unicode("").tag(sync=True) value = Unicode("").tag(sync=True) world_label_prefix = Unicode("\u00A0").tag(sync=True) + world_label_prefix_2 = Unicode("\u00A0").tag(sync=True) world_label_icrs = Unicode("\u00A0").tag(sync=True) world_label_deg = Unicode("\u00A0").tag(sync=True) world_ra = Unicode("").tag(sync=True) @@ -24,6 +25,7 @@ class CoordsInfo(TemplateMixin): def reset_coords_display(self): self.world_label_prefix = '\u00A0' + self.world_label_prefix_2 = '\u00A0' self.world_label_icrs = '\u00A0' self.world_label_deg = '\u00A0' self.world_ra = '' @@ -53,3 +55,7 @@ def set_coords(self, sky, unreliable_world=False, unreliable_pixel=False): self.world_dec_deg = world_dec_deg self.unreliable_world = unreliable_world self.unreliable_pixel = unreliable_pixel + if unreliable_world: + self.world_label_prefix_2 = '(est.)' + else: + self.world_label_prefix_2 = '\u00A0' diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue index d875d80a78..40f917425c 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.vue @@ -13,13 +13,13 @@ {{ world_label_prefix }} - {{ world_ra }} + {{ world_ra }} {{ world_dec }} {{ world_label_icrs }} - {{ unreliable_world ? '(est.)' : '' }} - {{ world_ra_deg }} + {{ world_label_prefix_2 }} + {{ world_ra_deg }} {{ world_dec_deg }} {{ world_label_deg }} diff --git a/jdaviz/configs/imviz/tests/test_linking.py b/jdaviz/configs/imviz/tests/test_linking.py index 096d56b0f6..2ba0ed5a44 100644 --- a/jdaviz/configs/imviz/tests/test_linking.py +++ b/jdaviz/configs/imviz/tests/test_linking.py @@ -271,6 +271,7 @@ def test_wcslink_rotated(self): # but cursor is outside GWCS bounding box assert self.viewer.label_mouseover.unreliable_world assert self.viewer.label_mouseover.unreliable_pixel + assert self.viewer.label_mouseover.world_label_prefix_2 == '(est.)' class TestLink_GWCS_GWCS(BaseImviz_GWCS_GWCS): diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index cb8809142c..9327e0c394 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -152,6 +152,8 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ spectra2d = [mos_spectrum2d] * 3 image_viewer = mosviz_helper.app.get_viewer('image-viewer') + spec1d_viewer = mosviz_helper.app.get_viewer('spectrum-viewer') + spec2d_viewer = mosviz_helper.app.get_viewer('spectrum-2d-viewer') # Coordinates info panel should not crash even when nothing is loaded. image_viewer.on_mouse_or_key_event({'event': 'mouseover'}) @@ -176,14 +178,15 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ assert len(qtable) == 3 # Also check coordinates info panels for Mosviz image viewer. - # 1D spectrum viewer panel is already tested in Specviz. - # 2D spectrum viewer panel is already tested in Specviz2d. + # 1D spectrum viewer panel is also tested in Specviz. + # 2D spectrum viewer panel is also tested in Specviz2d. image_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) assert image_viewer.label_mouseover.pixel == 'x=000.0 y=000.0' assert image_viewer.label_mouseover.value == '+3.74540e-01 Jy' assert image_viewer.label_mouseover.world_ra_deg == '5.0297844783' assert image_viewer.label_mouseover.world_dec_deg == '4.9918991917' + assert image_viewer.label_mouseover.icon == 'a' image_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': None, 'y': 0}}) assert image_viewer.label_mouseover.pixel == '' @@ -203,6 +206,23 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ assert image_viewer.label_mouseover.world_ra_deg == '' assert image_viewer.label_mouseover.world_dec_deg == '' + spec2d_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 10, 'y': 100}}) + assert spec2d_viewer.label_mouseover.pixel == 'x=00010.0 y=00100.0' + assert spec2d_viewer.label_mouseover.value == '+8.12986e-01 ' + assert spec2d_viewer.label_mouseover.world_ra_deg == '' + assert spec2d_viewer.label_mouseover.world_dec_deg == '' + assert spec2d_viewer.label_mouseover.icon == 'b' + + spec1d_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 7000, 'y': 170}}) + assert spec1d_viewer.label_mouseover.pixel == 'x=04.0' + assert spec1d_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec1d_viewer.label_mouseover.world_ra == '6.88889e+03' + assert spec1d_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec1d_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec1d_viewer.label_mouseover.world_ra_deg == '1.35436e+01' + assert spec1d_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec1d_viewer.label_mouseover.icon == 'c' + def test_zip_error(mosviz_helper, tmp_path): ''' diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 5a68c99b61..92d94e640c 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -1,17 +1,16 @@ -import numpy as np +import math import warnings +import numpy as np +from astropy import table +from astropy import units as u from glue.core import BaseData from glue.core.subset import Subset from glue.config import data_translator from glue_jupyter.bqplot.profile import BqplotProfileView from glue.core.exceptions import IncompatibleAttribute - -from astropy import table -from specutils import Spectrum1D from matplotlib.colors import cnames -from astropy import units as u - +from specutils import Spectrum1D from jdaviz.core.events import SpectralMarksChangedMessage, LineIdentifyMessage from jdaviz.core.registries import viewer_registry @@ -66,6 +65,9 @@ def on_mouse_or_key_event(self, data): return if data['event'] == 'mousemove': + if len(self.jdaviz_app.data_collection) < 1: + return + # Extract data coordinates - these are pixels in the reference image x = data['domain']['x'] y = data['domain']['y'] @@ -76,16 +78,61 @@ def on_mouse_or_key_event(self, data): self.label_mouseover.value = "" return - fmt = 'x={:+10.5e} y={:+10.5e}' - self.label_mouseover.pixel = fmt.format(x, y) + # Snap to the closest data point, not the actual mouse location. + sp = None + closest_i = 0 + closest_wave = 0 + closest_flux = 0 + closest_maxsize = 0 + closest_label = '' + closest_distance = None + for lyr in self.state.layers: + if ((not isinstance(lyr.layer, BaseData)) or (lyr.layer.ndim not in (1, 3)) + or (not lyr.visible)): + continue + + try: + # TODO: Is there a way to cache this? + sp = lyr.layer.get_object( + cls=Spectrum1D, statistic=getattr(self.state, 'function', None)) + + cur_i = np.argmin(abs(sp.spectral_axis.value - x)) + cur_wave = sp.spectral_axis[cur_i] + cur_flux = sp.flux[cur_i] + + dx = cur_wave.value - x + dy = cur_flux.value - y + cur_distance = math.sqrt(dx * dx + dy * dy) + if (closest_distance is None) or (cur_distance < closest_distance): + closest_distance = cur_distance + closest_i = cur_i + closest_wave = cur_wave + closest_flux = cur_flux + closest_maxsize = int(np.ceil(np.log10(sp.spectral_axis.size))) + 3 + closest_label = self.jdaviz_app.state.layer_icons.get(lyr.layer.label) + except Exception: + sp = None + + if sp is None: # Something is loaded but not the right thing + self.label_mouseover.icon = "" + self.label_mouseover.pixel = "" + self.label_mouseover.reset_coords_display() + self.label_mouseover.value = "" + return - # We just want cursor position, so these are not used. - self.label_mouseover.icon = '' - self.label_mouseover.reset_coords_display() - self.label_mouseover.value = '' + fmt = 'x={:0' + str(closest_maxsize) + '.1f}' + self.label_mouseover.pixel = fmt.format(closest_i) + self.label_mouseover.world_label_prefix = 'Wave' + self.label_mouseover.world_ra = f'{closest_wave.value:10.5e}' + self.label_mouseover.world_dec = closest_wave.unit.to_string() + self.label_mouseover.world_label_prefix_2 = 'Flux' + self.label_mouseover.world_ra_deg = f'{closest_flux.value:10.5e}' + self.label_mouseover.world_dec_deg = closest_flux.unit.to_string() + self.label_mouseover.icon = closest_label + self.label_mouseover.value = "" # Not used elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': - + self.label_mouseover.icon = "" self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() self.label_mouseover.value = "" diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index abfbe8708e..e871862b9c 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -204,16 +204,43 @@ def test_get_spectral_regions_unit(specviz_helper, spectrum1d): def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): + spec_viewer = specviz_helper.app.get_viewer('spectrum-viewer') + + # Mouseover without data should not crash. + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6100, 'y': 12.5}}) + assert spec_viewer.label_mouseover.pixel == '' + assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' + assert spec_viewer.label_mouseover.world_ra == '' + assert spec_viewer.label_mouseover.world_dec == '' + assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' + assert spec_viewer.label_mouseover.world_ra_deg == '' + assert spec_viewer.label_mouseover.world_dec_deg == '' + assert spec_viewer.label_mouseover.icon == '' + # If the reference (visible) data changes via unit conversion, # check that the region's units convert too specviz_helper.load_spectrum(spectrum1d) - # Also check coordinates info panel - spec_viewer = specviz_helper.app.get_viewer('spectrum-viewer') + # Also check coordinates info panel. + # x=0 -> 6000 A, x=1 -> 6222.222 A spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6100, 'y': 12.5}}) - assert spec_viewer.label_mouseover.pixel == 'x=+6.10000e+03 y=+1.25000e+01' + assert spec_viewer.label_mouseover.pixel == 'x=00.0' # Actual: x=00.4 + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '6.00000e+03' + assert spec_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '1.24967e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'a' spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': None, 'y': 12.5}}) assert spec_viewer.label_mouseover.pixel == '' + assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' + assert spec_viewer.label_mouseover.world_ra == '' + assert spec_viewer.label_mouseover.world_dec == '' + assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' + assert spec_viewer.label_mouseover.world_ra_deg == '' + assert spec_viewer.label_mouseover.world_dec_deg == '' + assert spec_viewer.label_mouseover.icon == 'a' # Convert the wavelength axis to microns new_spectral_axis = "micron" @@ -238,9 +265,23 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): # Coordinates info panel should show new unit spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0.61, 'y': 12.5}}) - assert spec_viewer.label_mouseover.pixel == 'x=+6.10000e-01 y=+1.25000e+01' + assert spec_viewer.label_mouseover.pixel == 'x=00.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '6.00000e-01' + assert spec_viewer.label_mouseover.world_dec == 'micron' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '1.24967e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' + assert spec_viewer.label_mouseover.icon == 'b' spec_viewer.on_mouse_or_key_event({'event': 'mouseleave'}) assert spec_viewer.label_mouseover.pixel == '' + assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' + assert spec_viewer.label_mouseover.world_ra == '' + assert spec_viewer.label_mouseover.world_dec == '' + assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' + assert spec_viewer.label_mouseover.world_ra_deg == '' + assert spec_viewer.label_mouseover.world_dec_deg == '' + assert spec_viewer.label_mouseover.icon == '' def test_subset_default_thickness(specviz_helper, spectrum1d): diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py index 122a10fa29..90b8fe147f 100644 --- a/jdaviz/configs/specviz2d/tests/test_parsers.py +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -68,13 +68,26 @@ def test_2d_parser_no_unit(specviz2d_helper, mos_spectrum2d): assert dc_1.label == 'Spectrum 1D' assert dc_1.get_component('flux').units == dc_0.get_component('flux').units - # Also check the coordinates info panel. + # Also check the coordinates info panels. + viewer_2d = specviz2d_helper.app.get_viewer('spectrum-2d-viewer') viewer_2d.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) assert viewer_2d.label_mouseover.pixel == 'x=00000.0 y=00000.0' assert viewer_2d.label_mouseover.value == '+3.74540e-01 ' assert viewer_2d.label_mouseover.world_ra_deg == '' assert viewer_2d.label_mouseover.world_dec_deg == '' + assert viewer_2d.label_mouseover.icon == 'a' + + viewer_1d = specviz2d_helper.app.get_viewer('spectrum-viewer') + viewer_1d.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 6.5, 'y': 3}}) + assert viewer_1d.label_mouseover.pixel == 'x=006.0' + assert viewer_1d.label_mouseover.world_label_prefix == 'Wave' + assert viewer_1d.label_mouseover.world_ra == '6.00000e+00' + assert viewer_1d.label_mouseover.world_dec == 'pix' + assert viewer_1d.label_mouseover.world_label_prefix_2 == 'Flux' + assert viewer_1d.label_mouseover.world_ra_deg == '-3.59571e+00' + assert viewer_1d.label_mouseover.world_dec_deg == '' + assert viewer_1d.label_mouseover.icon == 'b' def test_1d_parser(specviz2d_helper, spectrum1d): From 5750d6ffbdad0ab0c3d32b4463882a9b4d386fc7 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Thu, 29 Dec 2022 18:08:36 -0500 Subject: [PATCH 2/9] BUG: Fix spectrum mouseover when out of bounds. --- .../tests/test_gaussian_smooth.py | 24 ++++++++-------- jdaviz/configs/specviz/plugins/viewers.py | 14 ++++++---- jdaviz/configs/specviz/tests/test_helper.py | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py index 3b887145a5..be711c342a 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py @@ -68,7 +68,7 @@ def test_linking_after_spectral_smooth(cubeviz_helper, spectrum1d_cube): # Mouseover should automatically jump from one spectrum # to another, depending on which one is closer. - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 60}}) + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) assert spec_viewer.label_mouseover.pixel == 'x=01.0' assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' assert spec_viewer.label_mouseover.world_ra == '4.62360e-07' @@ -78,7 +78,7 @@ def test_linking_after_spectral_smooth(cubeviz_helper, spectrum1d_cube): assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' assert spec_viewer.label_mouseover.icon == 'a' - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 20}}) + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 20}}) assert spec_viewer.label_mouseover.pixel == 'x=01.0' assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' assert spec_viewer.label_mouseover.world_ra == '4.62360e-07' @@ -92,7 +92,7 @@ def test_linking_after_spectral_smooth(cubeviz_helper, spectrum1d_cube): for lyr in spec_viewer.layers: lyr.visible = False - spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.633e-7, 'y': 60}}) + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 60}}) assert spec_viewer.label_mouseover.pixel == '' assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' assert spec_viewer.label_mouseover.world_ra == '' @@ -161,13 +161,13 @@ def test_spectrum1d_smooth(specviz_helper, spectrum1d): assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' assert spec_viewer.label_mouseover.icon == 'b' - # Out-of-bounds should lock to closest edge value. + # Out-of-bounds shows nothing. spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 5500, 'y': 120}}) - assert spec_viewer.label_mouseover.pixel == 'x=00.0' - assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' - assert spec_viewer.label_mouseover.world_ra == '6.00000e+03' - assert spec_viewer.label_mouseover.world_dec == 'Angstrom' - assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' - assert spec_viewer.label_mouseover.world_ra_deg == '1.24967e+01' - assert spec_viewer.label_mouseover.world_dec_deg == 'Jy' - assert spec_viewer.label_mouseover.icon == 'a' + assert spec_viewer.label_mouseover.pixel == '' + assert spec_viewer.label_mouseover.world_label_prefix == '\xa0' + assert spec_viewer.label_mouseover.world_ra == '' + assert spec_viewer.label_mouseover.world_dec == '' + assert spec_viewer.label_mouseover.world_label_prefix_2 == '\xa0' + assert spec_viewer.label_mouseover.world_ra_deg == '' + assert spec_viewer.label_mouseover.world_dec_deg == '' + assert spec_viewer.label_mouseover.icon == '' diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 92d94e640c..25edd790e8 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -80,9 +80,9 @@ def on_mouse_or_key_event(self, data): # Snap to the closest data point, not the actual mouse location. sp = None - closest_i = 0 - closest_wave = 0 - closest_flux = 0 + closest_i = None + closest_wave = None + closest_flux = None closest_maxsize = 0 closest_label = '' closest_distance = None @@ -96,6 +96,10 @@ def on_mouse_or_key_event(self, data): sp = lyr.layer.get_object( cls=Spectrum1D, statistic=getattr(self.state, 'function', None)) + # Out of range in spectral axis. + if x < sp.spectral_axis.value.min() or x > sp.spectral_axis.value.max(): + continue + cur_i = np.argmin(abs(sp.spectral_axis.value - x)) cur_wave = sp.spectral_axis[cur_i] cur_flux = sp.flux[cur_i] @@ -110,10 +114,10 @@ def on_mouse_or_key_event(self, data): closest_flux = cur_flux closest_maxsize = int(np.ceil(np.log10(sp.spectral_axis.size))) + 3 closest_label = self.jdaviz_app.state.layer_icons.get(lyr.layer.label) - except Exception: + except Exception: # Something is loaded but not the right thing # pragma: no cover sp = None - if sp is None: # Something is loaded but not the right thing + if sp is None or closest_wave is None: self.label_mouseover.icon = "" self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index e871862b9c..dbcd138efe 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -1,5 +1,6 @@ from zipfile import ZipFile +import numpy as np import pytest from astropy import units as u from astropy.tests.helper import assert_quantity_allclose @@ -381,3 +382,30 @@ def test_data_label_as_posarg(specviz_helper, spectrum1d): # Passing in data_label keyword as posarg. specviz_helper.load_spectrum(spectrum1d, 'my_spec') assert specviz_helper.app.data_collection[0].label == 'my_spec' + + +def test_spectra_partial_overlap(specviz_helper): + spec_viewer = specviz_helper.app.get_viewer('spectrum-viewer') + + wave_1 = np.linspace(6000, 7000, 10) * u.AA + flux_1 = ([1200] * wave_1.size) * u.nJy + sp_1 = Spectrum1D(flux=flux_1, spectral_axis=wave_1) + + wave_2 = wave_1 + (800 * u.AA) + flux_2 = ([60] * wave_2.size) * u.nJy + sp_2 = Spectrum1D(flux=flux_2, spectral_axis=wave_2) + + specviz_helper.load_spectrum(sp_1, data_label='left') + specviz_helper.load_spectrum(sp_2, data_label='right') + + # Test mouseover outside of left but in range for right. + # Should show right spectrum even when mouse is near left flux. + spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 7022, 'y': 1000}}) + assert spec_viewer.label_mouseover.pixel == 'x=02.0' + assert spec_viewer.label_mouseover.world_label_prefix == 'Wave' + assert spec_viewer.label_mouseover.world_ra == '7.02222e+03' + assert spec_viewer.label_mouseover.world_dec == 'Angstrom' + assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux' + assert spec_viewer.label_mouseover.world_ra_deg == '6.00000e+01' + assert spec_viewer.label_mouseover.world_dec_deg == 'nJy' + assert spec_viewer.label_mouseover.icon == 'b' From 1fdb0770907665aac2500a31e4cf6da83583a290 Mon Sep 17 00:00:00 2001 From: "Pey Lian Lim (Github)" <2090236+pllim@users.noreply.github.com> Date: Fri, 30 Dec 2022 12:54:25 -0500 Subject: [PATCH 3/9] PERF: Decrease lag for Cubeviz spec1d mouseover. Also remove unnecessary re-computation in the internals. A new internal cache on app level is introduced. --- jdaviz/app.py | 7 +++ jdaviz/configs/specviz/plugins/viewers.py | 73 +++++++++++++---------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index ca89b66dba..fcc426deef 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -258,6 +258,10 @@ def __init__(self, configuration=None, *args, **kwargs): # Add a fitted_models dictionary that the helpers (or user) can access self.fitted_models = {} + # Internal cache so we don't have to keep calling get_object for the same Data. + # Key should be (data_label, statistic) and value the translated object. + self._get_object_cache = {} + # Add new and inverse colormaps to Glue global state. Also see ColormapRegistry in # https://github.com/glue-viz/glue/blob/main/glue/config.py new_cms = (['Rainbow', cm.rainbow], @@ -1493,6 +1497,9 @@ def _on_data_deleted(self, msg): if data_item['name'] == msg.data.label: self.state.data_items.remove(data_item) + # Hard to know what is associated with what, so just clear everything. + self._get_object_cache.clear() + @staticmethod def _create_data_item(data): ndims = len(data.shape) diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 25edd790e8..f6868212d6 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -92,9 +92,15 @@ def on_mouse_or_key_event(self, data): continue try: - # TODO: Is there a way to cache this? - sp = lyr.layer.get_object( - cls=Spectrum1D, statistic=getattr(self.state, 'function', None)) + # Cache should have been populated when spectrum was first plotted. + # But if not (maybe user changed statistic), we cache it here too. + statistic = getattr(self.state, 'function', None) + cache_key = (lyr.layer.label, statistic) + if cache_key in self.jdaviz_app._get_object_cache: + sp = self.jdaviz_app._get_object_cache[cache_key] + else: + sp = lyr.layer.get_object(cls=Spectrum1D, statistic=statistic) + self.jdaviz_app._get_object_cache[cache_key] = sp # Out of range in spectral axis. if x < sp.spectral_axis.value.min() or x > sp.spectral_axis.value.max(): @@ -148,40 +154,39 @@ def _expected_subset_layer_default(self, layer_state): def data(self, cls=None): # Grab the user's chosen statistic for collapsing data - if hasattr(self.state, 'function'): - statistic = self.state.function - else: - statistic = None - + statistic = getattr(self.state, 'function', None) data = [] for layer_state in self.state.layers: if hasattr(layer_state, 'layer'): + lyr = layer_state.layer # For raw data, just include the data itself - if isinstance(layer_state.layer, BaseData): + if isinstance(lyr, BaseData): _class = cls or self.default_class if _class is not None: - # If spectrum, collapse via the defined statistic - if _class == Spectrum1D: - layer_data = layer_state.layer.get_object(cls=_class, - statistic=statistic) + cache_key = (lyr.label, statistic) + if cache_key in self.jdaviz_app._get_object_cache: + layer_data = self.jdaviz_app._get_object_cache[cache_key] else: - layer_data = layer_state.layer.get_object(cls=_class) + # If spectrum, collapse via the defined statistic + if _class == Spectrum1D: + layer_data = lyr.get_object(cls=_class, statistic=statistic) + else: + layer_data = lyr.get_object(cls=_class) + self.jdaviz_app._get_object_cache[cache_key] = layer_data data.append(layer_data) - # For subsets, make sure to apply the subset mask to the - # layer data first - elif isinstance(layer_state.layer, Subset): - layer_data = layer_state.layer + # For subsets, make sure to apply the subset mask to the layer data first + elif isinstance(lyr, Subset): + layer_data = lyr if _class is not None: handler, _ = data_translator.get_handler_for(_class) try: - layer_data = handler.to_object(layer_data, - statistic=statistic) + layer_data = handler.to_object(layer_data, statistic=statistic) except IncompatibleAttribute: continue data.append(layer_data) @@ -465,18 +470,20 @@ def _plot_mask(self): # Loop through all active data in the viewer for index, layer_state in enumerate(self.state.layers): - comps = [str(component) for component in layer_state.layer.components] + lyr = layer_state.layer + comps = [str(component) for component in lyr.components] # Skip subsets - if hasattr(layer_state.layer, "subset_state"): + if hasattr(lyr, "subset_state"): continue # Ignore data that does not have a mask component if "mask" in comps: - mask = np.array(layer_state.layer['mask'].data) + mask = np.array(lyr['mask'].data) - data_x = layer_state.layer.data.get_object().spectral_axis - data_y = layer_state.layer.data.get_object().flux.value + data_obj = lyr.data.get_object() + data_x = data_obj.spectral_axis.value + data_y = data_obj.flux.value # For plotting markers only for the masked data # points, erase un-masked data from trace. @@ -507,24 +514,26 @@ def _plot_uncertainties(self): # Loop through all active data in the viewer for index, layer_state in enumerate(self.state.layers): + lyr = layer_state.layer # Skip subsets - if hasattr(layer_state.layer, "subset_state"): + if hasattr(lyr, "subset_state"): continue - comps = [str(component) for component in layer_state.layer.components] + comps = [str(component) for component in lyr.components] # Ignore data that does not have an uncertainty component if "uncertainty" in comps: # noqa - error = np.array(layer_state.layer['uncertainty'].data) + error = np.array(lyr['uncertainty'].data) - data_x = layer_state.layer.data.get_object().spectral_axis - data_y = layer_state.layer.data.get_object().flux.value + data_obj = lyr.data.get_object() + data_x = data_obj.spectral_axis.value + data_y = data_obj.flux.value # The shaded band around the spectrum trace is bounded by # two lines, above and below the spectrum trace itself. - x = [np.ndarray.tolist(data_x), - np.ndarray.tolist(data_x)] + data_x_list = np.ndarray.tolist(data_x) + x = [data_x_list, data_x_list] y = [np.ndarray.tolist(data_y - error), np.ndarray.tolist(data_y + error)] From c6b3912f8e801649a3d4afe72038ced749f6fd59 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 29 Dec 2022 15:10:41 -0500 Subject: [PATCH 4/9] basic implementation of mouseover marker * only used in spectrum profile viewers * currently ALWAYS on --- .../imviz/plugins/coords_info/coords_info.py | 23 ++++++++++++++++++ jdaviz/configs/specviz/plugins/viewers.py | 4 ++++ jdaviz/core/marks.py | 24 ++++++++++++++----- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index b90d22a38d..032c0c0271 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -1,7 +1,9 @@ from traitlets import Bool, Unicode +from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView from jdaviz.core.registries import tool_registry from jdaviz.core.template_mixin import TemplateMixin +from jdaviz.core.marks import PluginScatter __all__ = ['CoordsInfo'] @@ -23,6 +25,27 @@ class CoordsInfo(TemplateMixin): unreliable_world = Bool(False).tag(sync=True) unreliable_pixel = Bool(False).tag(sync=True) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._marks = {} + + @property + def marks(self): + """ + Access the marks created by this plugin. + """ + if self._marks: + # TODO: replace with cache property? + return self._marks + + # create marks for each of the spectral viewers (will need a listener event to create marks + # for new viewers if dynamic creation of spectral viewers is ever supported) + for id, viewer in self.app._viewer_store.items(): + if isinstance(viewer, SpecvizProfileView): + self._marks[id] = PluginScatter(viewer, visible=False) + viewer.figure.marks = viewer.figure.marks + [self._marks[id]] + return self._marks + def reset_coords_display(self): self.world_label_prefix = '\u00A0' self.world_label_prefix_2 = '\u00A0' diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index f6868212d6..5e8011c052 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -128,6 +128,7 @@ def on_mouse_or_key_event(self, data): self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() self.label_mouseover.value = "" + self.label_mouseover.marks[self._reference_id].visible = False return fmt = 'x={:0' + str(closest_maxsize) + '.1f}' @@ -140,12 +141,15 @@ def on_mouse_or_key_event(self, data): self.label_mouseover.world_dec_deg = closest_flux.unit.to_string() self.label_mouseover.icon = closest_label self.label_mouseover.value = "" # Not used + self.label_mouseover.marks[self._reference_id].update_xy([closest_wave.value], [closest_flux.value]) + self.label_mouseover.marks[self._reference_id].visible = True elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': self.label_mouseover.icon = "" self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() self.label_mouseover.value = "" + self.label_mouseover.marks[self._reference_id].visible = False def _expected_subset_layer_default(self, layer_state): super()._expected_subset_layer_default(layer_state) diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py index 110cc87deb..955850e54b 100644 --- a/jdaviz/core/marks.py +++ b/jdaviz/core/marks.py @@ -13,7 +13,7 @@ __all__ = ['OffscreenLinesMarks', 'BaseSpectrumVerticalLine', 'SpectralLine', 'SliceIndicatorMarks', 'ShadowMixin', 'ShadowLine', 'ShadowLabelFixedY', - 'PluginLine', + 'PluginMark', 'PluginLine', 'PluginScatter', 'LineAnalysisContinuum', 'LineAnalysisContinuumCenter', 'LineAnalysisContinuumLeft', 'LineAnalysisContinuumRight', 'LineUncertainties', 'ScatterMask', 'SelectedSpaxel'] @@ -484,19 +484,31 @@ def _on_shadowing_changed(self, change): self._update_align() -class PluginLine(Lines, HubListener): - def __init__(self, viewer, x=[], y=[], **kwargs): - # color is same blue as import button - super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) - +class PluginMark(): def update_xy(self, x, y): self.x = np.asarray(x) self.y = np.asarray(y) + def append_xy(self, x, y): + self.x = np.append(self.x, x) + self.y = np.append(self.y, y) + def clear(self): self.update_xy([], []) +class PluginLine(Lines, PluginMark, HubListener): + def __init__(self, viewer, x=[], y=[], **kwargs): + # color is same blue as import button + super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) + + +class PluginScatter(Scatter, PluginMark, HubListener): + def __init__(self, viewer, x=[], y=[], **kwargs): + # color is same blue as import button + super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) + + class LineAnalysisContinuum(PluginLine): pass From 111746dd9cf9bc839a0423486d7e92ef38686d09 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 30 Dec 2022 09:54:32 -0500 Subject: [PATCH 5/9] only show marker (and nearest spectrum details) if no tool selected (or default tool - slice in cubeviz, for example) --- jdaviz/configs/specviz/plugins/viewers.py | 33 ++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 5e8011c052..7606cfa262 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -131,18 +131,27 @@ def on_mouse_or_key_event(self, data): self.label_mouseover.marks[self._reference_id].visible = False return - fmt = 'x={:0' + str(closest_maxsize) + '.1f}' - self.label_mouseover.pixel = fmt.format(closest_i) - self.label_mouseover.world_label_prefix = 'Wave' - self.label_mouseover.world_ra = f'{closest_wave.value:10.5e}' - self.label_mouseover.world_dec = closest_wave.unit.to_string() - self.label_mouseover.world_label_prefix_2 = 'Flux' - self.label_mouseover.world_ra_deg = f'{closest_flux.value:10.5e}' - self.label_mouseover.world_dec_deg = closest_flux.unit.to_string() - self.label_mouseover.icon = closest_label - self.label_mouseover.value = "" # Not used - self.label_mouseover.marks[self._reference_id].update_xy([closest_wave.value], [closest_flux.value]) - self.label_mouseover.marks[self._reference_id].visible = True + # show the locked marker/coords only if either no tool or the default tool is active + locking_active = self.toolbar.active_tool_id in self.toolbar.default_tool_priority + [None] # noqa + if locking_active: + fmt = 'x={:0' + str(closest_maxsize) + '.1f}' + self.label_mouseover.pixel = fmt.format(closest_i) + self.label_mouseover.world_label_prefix = 'Wave' + self.label_mouseover.world_ra = f'{closest_wave.value:10.5e}' + self.label_mouseover.world_dec = closest_wave.unit.to_string() + self.label_mouseover.world_label_prefix_2 = 'Flux' + self.label_mouseover.world_ra_deg = f'{closest_flux.value:10.5e}' + self.label_mouseover.world_dec_deg = closest_flux.unit.to_string() + self.label_mouseover.icon = closest_label + self.label_mouseover.value = "" # Not used + self.label_mouseover.marks[self._reference_id].update_xy([closest_wave.value], [closest_flux.value]) # noqa + self.label_mouseover.marks[self._reference_id].visible = True + else: + # show exact plot coordinates (useful for drawing spectral subsets or zoom ranges) + fmt = 'x={:+10.5e} y={:+10.5e}' + self.label_mouseover.icon = "" + self.label_mouseover.pixel = fmt.format(x, y) + self.label_mouseover.marks[self._reference_id].visible = False elif data['event'] == 'mouseleave' or data['event'] == 'mouseenter': self.label_mouseover.icon = "" From ae22338481fc61ecab69ba71fe8b724669cf2dbc Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 4 Jan 2023 11:27:29 -0500 Subject: [PATCH 6/9] support for mouseover of spatial subsets in cubeviz * including clearing cache when subset is changed and ignoring hidden layers --- jdaviz/app.py | 14 ++++++++++++-- jdaviz/configs/specviz/plugins/viewers.py | 21 +++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index fcc426deef..cf51194fb5 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -28,6 +28,7 @@ from glue.core.message import (DataCollectionAddMessage, DataCollectionDeleteMessage, SubsetCreateMessage, + SubsetUpdateMessage, SubsetDeleteMessage) from glue.core.state_objects import State from glue.core.subset import Subset, RangeSubsetState, RoiSubsetState @@ -261,6 +262,8 @@ def __init__(self, configuration=None, *args, **kwargs): # Internal cache so we don't have to keep calling get_object for the same Data. # Key should be (data_label, statistic) and value the translated object. self._get_object_cache = {} + self.hub.subscribe(self, SubsetUpdateMessage, + handler=lambda msg: self._clear_object_cache(msg.subset.label)) # Add new and inverse colormaps to Glue global state. Also see ColormapRegistry in # https://github.com/glue-viz/glue/blob/main/glue/config.py @@ -1482,6 +1485,14 @@ def _on_data_added(self, msg): data_item = self._create_data_item(msg.data) self.state.data_items.append(data_item) + def _clear_object_cache(self, data_label=None): + if data_label is None: + self._get_object_cache.clear() + else: + # keys are (data_label, statistic) tuples + self._get_object_cache = {k: v for k, v in self._get_object_cache.items() + if k[0] != data_label} + def _on_data_deleted(self, msg): """ Callback for when data is removed from the internal ``DataCollection``. @@ -1497,8 +1508,7 @@ def _on_data_deleted(self, msg): if data_item['name'] == msg.data.label: self.state.data_items.remove(data_item) - # Hard to know what is associated with what, so just clear everything. - self._get_object_cache.clear() + self._clear_object_cache(msg.data.label) @staticmethod def _create_data_item(data): diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 7606cfa262..fd68528f43 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -5,7 +5,8 @@ from astropy import table from astropy import units as u from glue.core import BaseData -from glue.core.subset import Subset +from glue.core.subset import Subset, RoiSubsetState +from glue.core.subset_group import GroupedSubset from glue.config import data_translator from glue_jupyter.bqplot.profile import BqplotProfileView from glue.core.exceptions import IncompatibleAttribute @@ -87,7 +88,13 @@ def on_mouse_or_key_event(self, data): closest_label = '' closest_distance = None for lyr in self.state.layers: - if ((not isinstance(lyr.layer, BaseData)) or (lyr.layer.ndim not in (1, 3)) + if not lyr.visible: + continue + if isinstance(lyr.layer, GroupedSubset): + if not isinstance(lyr.layer.subset_state, RoiSubsetState): + # then this is a SPECTRAL subset + continue + elif ((not isinstance(lyr.layer, BaseData)) or (lyr.layer.ndim not in (1, 3)) or (not lyr.visible)): continue @@ -99,7 +106,8 @@ def on_mouse_or_key_event(self, data): if cache_key in self.jdaviz_app._get_object_cache: sp = self.jdaviz_app._get_object_cache[cache_key] else: - sp = lyr.layer.get_object(cls=Spectrum1D, statistic=statistic) + sp = self.jdaviz_app.get_data_from_viewer('spectrum-viewer', + lyr.layer.label) self.jdaviz_app._get_object_cache[cache_key] = sp # Out of range in spectral axis. @@ -120,10 +128,11 @@ def on_mouse_or_key_event(self, data): closest_flux = cur_flux closest_maxsize = int(np.ceil(np.log10(sp.spectral_axis.size))) + 3 closest_label = self.jdaviz_app.state.layer_icons.get(lyr.layer.label) - except Exception: # Something is loaded but not the right thing # pragma: no cover - sp = None + except Exception: # nosec + # Something is loaded but not the right thing + continue - if sp is None or closest_wave is None: + if closest_wave is None: self.label_mouseover.icon = "" self.label_mouseover.pixel = "" self.label_mouseover.reset_coords_display() From 463bae985f0f68dc4b8ea69f47373a6b8b4c3647 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 5 Jan 2023 09:26:54 -0500 Subject: [PATCH 7/9] remilestone changelog to 3.3 --- CHANGES.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e111553568..e34136fc60 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,18 +7,26 @@ New Features Cubeviz ^^^^^^^ +- Improved mouseover info display for spectrum viewer. [#1894] + Imviz ^^^^^ Mosviz ^^^^^^ +- Improved mouseover info display for spectrum viewer. [#1894] + Specviz ^^^^^^^ +- Improved mouseover info display for spectrum viewer. [#1894] + Specviz2d ^^^^^^^^^ +- Improved mouseover info display for spectrum viewer. [#1894] + API Changes ----------- @@ -110,8 +118,6 @@ Cubeviz - Added Slice plugin player control buttons. [#1848] -- Improved mouseover info display for spectrum viewer. [#1894] - Imviz ^^^^^ @@ -132,8 +138,6 @@ Mosviz - ``load_data`` method can now load JWST NIRCam and NIRSpec level 2 data. [#1835] -- Improved mouseover info display for spectrum viewer. [#1894] - Specviz ^^^^^^^ @@ -141,8 +145,6 @@ Specviz - Switch to opt-in concatenation for multi-order x1d spectra. [#1659] -- Improved mouseover info display for spectrum viewer. [#1894] - Specviz2d ^^^^^^^^^ @@ -151,8 +153,6 @@ Specviz2d - Add dropdown for choosing background statistic (average or median). [#1922] -- Improved mouseover info display for spectrum viewer. [#1894] - API Changes ----------- From 6cb7ff26e5421ad860008dfe0b36d5a8f483c6e2 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Tue, 17 Jan 2023 16:33:06 -0500 Subject: [PATCH 8/9] creating new setting for showing mouseover marker --- jdaviz/app.py | 1 + .../configs/default/plugins/plot_options/plot_options.py | 4 ++++ .../configs/default/plugins/plot_options/plot_options.vue | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/jdaviz/app.py b/jdaviz/app.py index cf51194fb5..c1e2ebc1a0 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -135,6 +135,7 @@ class ApplicationState(State): 'tab_headers': True, }, 'viewer_labels': True, + 'mouseover_marker': True, 'dense_toolbar': True, 'context': { 'notebook': { diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 862fbd8a4f..9343795e82 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -188,6 +188,8 @@ class PlotOptions(PluginTemplateMixin): show_viewer_labels = Bool(True).tag(sync=True) + show_mouseover_marker = Bool(True).tag(sync=True) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.viewer = ViewerSelect(self, 'viewer_items', 'viewer_selected', 'multiselect') @@ -304,6 +306,7 @@ def line_visible(state): # display_units self.show_viewer_labels = self.app.state.settings['viewer_labels'] + self.show_mouseover_marker = self.app.state.settings['mouseover_marker'] self.app.state.add_callback('settings', self._on_app_settings_changed) @property @@ -331,6 +334,7 @@ def _on_show_viewer_labels_changed(self, event): def _on_app_settings_changed(self, value): self.show_viewer_labels = value['viewer_labels'] + self.show_mouseover_marker = value['mouseover_marker'] def select_all(self, viewers=True, layers=True): """ diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index bfad881c3c..746af73376 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -19,6 +19,14 @@ persistent-hint > + + + From da4a704e29861ca6f1553d1cbf663b4601905fd6 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Tue, 17 Jan 2023 20:37:06 -0500 Subject: [PATCH 9/9] proposal for mouseover marker toggle --- jdaviz/configs/default/plugins/plot_options/plot_options.vue | 2 +- jdaviz/configs/specviz/plugins/viewers.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index 746af73376..91dd82ab39 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -23,7 +23,7 @@ diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index fd68528f43..d0bd439599 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -65,6 +65,11 @@ def on_mouse_or_key_event(self, data): else: # pragma: no cover return + # only handle mouseover markers if the setting is selected: + plot_options_plugin = self.jdaviz_app.get_tray_item_from_name('g-plot-options') + if not plot_options_plugin.show_mouseover_marker: + return + if data['event'] == 'mousemove': if len(self.jdaviz_app.data_collection) < 1: return