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

Cubeviz image viewer coordinates display #1315

Merged
merged 2 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I am updating this test anyway, I took the liberty to make it more realistic in loading like a user would.


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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

But in changing from using app.add_data to cubeviz_helper.load_data, the moment map shape changed! @rosteen , since you refactored how Cubeviz swaps dimensions around, does this make sense to you?


# 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is highly annoying to me that data_label is ignored when you pass Spectrum1D into Cubeviz, so I fixed it here. Unless there is a good reason not to do this? I cannot think of any good reason.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for doing this! It's actually bothered me before but I guess not enough to add it 😅

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
Comment on lines +65 to +67
Copy link
Member

Choose a reason for hiding this comment

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

how is visible_layers ordered? If there are multiple layers and they're linked correctly, the coordinates shouldn't care what layer, but the value will... is this guaranteed to be the "top" layer if multiple layers are plotted without changing transparency? Is there any room to squeeze in the layer name for cases where visible_layers includes more than one?

This behavior/situation might be something that is worth adding test coverage, if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The inner workings of visible layers, I'd have to defer to @astrofrog or @maartenbreddels. I wish there is an easier way for me to reliably grab this info too but then Glue lets you overlay this and that, which complicates things. This particular info panel would ignore the semi-transparent overlay use cases.

Re: tests -- the info panel is kinda tested non-interactively in https://github.com/spacetelescope/jdaviz/blob/main/jdaviz/configs/imviz/tests/test_linking.py (the label_mouseover stuff). Not that the tests are exhaustive because they obviously missed #1299 but again adding a test for that would be out of scope here. Still, I guess I can add similar test(s) to Cubeviz...

Copy link
Member

Choose a reason for hiding this comment

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

from trial and error, it seems that this does always show the "top" layer, which is the most sensible (simple) behavior. If there is any room, I do think a label would be useful, but will approve this and that can always be added later if you don't have time to include it in scope here (as I think the same thing could apply to the existing display in imviz).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re: label -- In Imviz, I incorporated that into Compass plugin (because the label just seems to go with Compass naturally). Originally when I was new and naive, I attempted glue-viz/glue-jupyter#244 but failed. I don't think there is any more space for label in the coordinates display, as it is already pretty crowded.

This does bring up a good point... What if someone deselects the cube and have a 2D moment map displayed in the cube viewer instead? Is that a use case we even support?


# 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