Skip to content

Commit

Permalink
Cubeviz image viewer coordinates display (#1315)
Browse files Browse the repository at this point in the history
* Cubeviz image viewer coordinates display

* BUG: Fixed data_label not propagated
for Spectrum1D in Cubeviz.

TST: Add Cubeviz coords info tests.

DOC: Update change log.
  • Loading branch information
pllim authored May 17, 2022
1 parent cac9374 commit 926623e
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ New Features
Cubeviz
^^^^^^^

- Cubeviz image viewer now has coordinates info panel like Imviz. [#1315]

Imviz
^^^^^

Expand Down Expand Up @@ -46,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
^^^^^

Expand Down
10 changes: 10 additions & 0 deletions docs/cubeviz/displaycubes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`
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.
2 changes: 2 additions & 0 deletions docs/imviz/displayimages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
==================

Expand Down
2 changes: 2 additions & 0 deletions jdaviz/configs/cubeviz/cubeviz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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'
28 changes: 16 additions & 12 deletions jdaviz/configs/cubeviz/plugins/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')

Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
45 changes: 45 additions & 0 deletions jdaviz/configs/cubeviz/plugins/tests/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -67,13 +90,35 @@ 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)

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'):
Expand Down
72 changes: 72 additions & 0 deletions jdaviz/configs/cubeviz/plugins/viewers.py
Original file line number Diff line number Diff line change
@@ -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']
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 926623e

Please sign in to comment.