Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve coordinates display panel for spectrum viewer #1894

Merged
merged 10 commits into from
Jan 19, 2023
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ New Features
Cubeviz
^^^^^^^

- Improved mouseover info display for spectrum viewer. [#1894]

Imviz
^^^^^

Mosviz
^^^^^^
- Reliably retrieves identifier using each datasets' metadata entry. [#1851]

- 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
-----------

Expand Down
10 changes: 10 additions & 0 deletions docs/specviz/displaying.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
cursor position as well as the spectral axis value, pixel, and flux of the closest data point
to the cursor.
This information is displayed in the top bar of the UI, on the middle-right side.

Home
====

Expand Down
17 changes: 17 additions & 0 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -258,6 +259,12 @@ 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 = {}
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
new_cms = (['Rainbow', cm.rainbow],
Expand Down Expand Up @@ -1486,6 +1493,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``.
Expand All @@ -1501,6 +1516,8 @@ def _on_data_deleted(self, msg):
if data_item['name'] == msg.data.label:
self.state.data_items.remove(data_item)

self._clear_object_cache(msg.data.label)

@staticmethod
def _create_data_item(data):
ndims = len(data.shape)
Expand Down
Original file line number Diff line number Diff line change
@@ -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]'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is slight behavior change when you use the proper route to load the data into Cubeviz. I think this is more correct as a test because this is how user will actually load it.

gs.mode_selected = 'Spectral'
gs.stddev = 3.2
gs.add_to_viewer_selected = 'None'
Expand All @@ -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
Expand All @@ -68,21 +65,100 @@ 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.6236e-7, 'y': 60}})
assert spec_viewer.label_mouseover.pixel == '4.62360e-07, 6.00000e+01'
assert spec_viewer.label_mouseover.world_label_prefix == 'Wave'
assert spec_viewer.label_mouseover.world_ra == '4.62360e-07 m (1 pix)'
assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux'
assert spec_viewer.label_mouseover.world_ra_deg == '9.20000e+01 Jy'
assert spec_viewer.label_mouseover.icon == 'a'

spec_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 4.6236e-7, 'y': 20}})
assert spec_viewer.label_mouseover.pixel == '4.62360e-07, 2.00000e+01'
assert spec_viewer.label_mouseover.world_label_prefix == 'Wave'
assert spec_viewer.label_mouseover.world_ra == '4.62360e-07 m (1 pix)'
assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux'
assert spec_viewer.label_mouseover.world_ra_deg == '1.47943e+01 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.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 == ''
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")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the warning check to the affected line. This way, we won't accidentally ignore warning that we're not supposed to ignore.

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 == '6.40000e+03, 1.20000e+02'
assert spec_viewer.label_mouseover.world_label_prefix == 'Wave'
assert spec_viewer.label_mouseover.world_ra == '6.44444e+03 Angstrom (2 pix)'
assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux'
assert spec_viewer.label_mouseover.world_ra_deg == '1.35366e+01 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.world_label_prefix == 'Wave'
assert spec_viewer.label_mouseover.world_ra == '6.44444e+03 Angstrom (2 pix)'
assert spec_viewer.label_mouseover.world_label_prefix_2 == 'Flux'
assert spec_viewer.label_mouseover.world_ra_deg == '5.34688e+00 Jy'
assert spec_viewer.label_mouseover.icon == 'b'

# 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 == ''
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 == ''
34 changes: 34 additions & 0 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
@@ -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']

Expand All @@ -10,9 +12,11 @@
class CoordsInfo(TemplateMixin):
template_file = __file__, "coords_info.vue"
icon = Unicode("").tag(sync=True)
pixel_prefix = Unicode("Pixel").tag(sync=True)
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)
Expand All @@ -22,8 +26,33 @@ 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,
marker='rectangle', stroke_width=1,
visible=False)
viewer.figure.marks = viewer.figure.marks + [self._marks[id]]
return self._marks

def reset_coords_display(self):
self.pixel_prefix = "Pixel"
self.world_label_prefix = '\u00A0'
self.world_label_prefix_2 = '\u00A0'
self.world_label_icrs = '\u00A0'
self.world_label_deg = '\u00A0'
self.world_ra = ''
Expand All @@ -44,6 +73,7 @@ def set_coords(self, sky, unreliable_world=False, unreliable_pixel=False):
if "nan" in (world_ra, world_dec, world_ra_deg, world_dec_deg):
self.reset_coords_display()
else:
self.pixel_prefix = 'Pixel'
self.world_label_prefix = 'World'
self.world_label_icrs = '(ICRS)'
self.world_label_deg = '(deg)'
Expand All @@ -53,3 +83,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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this statement always true? 😏

self.world_label_prefix_2 = '(est.)'
else:
self.world_label_prefix_2 = '\u00A0'
8 changes: 4 additions & 4 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
<table>
<tr>
<td colspan="4" :style="unreliable_pixel ? 'color: #B8B8B8' : ''">
<b v-if="pixel">Pixel </b>{{ pixel }}&nbsp;&nbsp;
<b v-if="pixel">{{ pixel_prefix }} </b>{{ pixel }}&nbsp;&nbsp;
<b v-if="value">Value </b>{{ value }}
</td>
</tr>
<tr :style="unreliable_world ? 'color: #B8B8B8' : ''">
<td width="42"><b>{{ world_label_prefix }}</b></td>
<td width="115">{{ world_ra }}</td>
<td :width="world_label_prefix == 'Wave' ? 85 : 115">{{ world_ra }}</td>
<td width="120">{{ world_dec }}</td>
<td>{{ world_label_icrs }}</td>
</tr>
<tr :style="unreliable_world ? 'color: #B8B8B8' : ''">
<td width="42">{{ unreliable_world ? '(est.)' : '' }}</td>
<td width="115">{{ world_ra_deg }}</td>
<td width="42" :style="unreliable_world ? '' : 'font-weight: bold'">{{ world_label_prefix_2 }}</td>
<td :width="world_label_prefix == 'Wave' ? 85 : 115">{{ world_ra_deg }}</td>
<td width="120">{{ world_dec_deg }}</td>
<td>{{ world_label_deg }}</td>
</tr>
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/imviz/tests/test_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
22 changes: 20 additions & 2 deletions jdaviz/configs/mosviz/tests/test_data_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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'})
Expand All @@ -180,14 +182,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 == ''
Expand All @@ -207,6 +210,21 @@ 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 == '7.00000e+03, 1.70000e+02'
assert spec1d_viewer.label_mouseover.world_label_prefix == 'Wave'
assert spec1d_viewer.label_mouseover.world_ra == '6.88889e+03 Angstrom (4 pix)'
assert spec1d_viewer.label_mouseover.world_label_prefix_2 == 'Flux'
assert spec1d_viewer.label_mouseover.world_ra_deg == '1.35436e+01 Jy'
assert spec1d_viewer.label_mouseover.icon == 'c'


def test_zip_error(mosviz_helper, tmp_path):
'''
Expand Down
Loading