From 3afe01085e57f92ff9016495df1bd639a4ced33c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 22 Apr 2024 08:07:40 -0400 Subject: [PATCH] TPF viewer: marker/mouseover support (#104) * rename flux -> value/value:unit and support from TPF viewer * expose information and exact slice time for image viewer * update tests * add basic test coverage * store/expose time unit in mouseover/markers --- lcviz/plugins/coords_info/coords_info.py | 90 +++++++++++++++++++++++- lcviz/plugins/markers/markers.py | 8 ++- lcviz/tests/test_plugin_markers.py | 51 +++++++++++++- 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/lcviz/plugins/coords_info/coords_info.py b/lcviz/plugins/coords_info/coords_info.py index 6e442b39..1679043a 100644 --- a/lcviz/plugins/coords_info/coords_info.py +++ b/lcviz/plugins/coords_info/coords_info.py @@ -51,8 +51,10 @@ def _cursor_fallback(): self._dict['data_label'] = '' self._dict['time'] = x if not is_phase else np.nan + self._dict['time:unit'] = x_unit self._dict['phase'] = x if is_phase else np.nan - self._dict['flux'] = y + self._dict['value'] = y + self._dict['value:unit'] = y_unit self._dict['ephemeris'] = '' self.row2_title = '' @@ -123,11 +125,13 @@ def _cursor_fallback(): time_comp = closest_lyr.layer.components[component_labels.index('World 0')] times = closest_lyr.layer.get_data(time_comp) self._dict['time'] = float(times[closest_i]) + self._dict['time:unit'] = str(viewer.time_unit) # x_unit is phase self._dict['phase'] = closest_x self._dict['ephemeris'] = viewer.reference.split(':')[1] else: self.row2_text = f'{closest_x:10.5e} {x_unit}' self._dict['time'] = closest_x + self._dict['time:unit'] = x_unit self._dict['phase'] = np.nan self._dict['ephemeris'] = '' @@ -139,13 +143,95 @@ def _cursor_fallback(): self.row3_text = f'{closest_y:10.5e} {y_unit}' self._dict['axes_y'] = closest_y self._dict['axes_y:unit'] = y_unit - self._dict['flux'] = closest_y + self._dict['value'] = closest_y + self._dict['value:unit'] = y_unit self.icon = closest_icon self.marks[viewer._reference_id].update_xy([closest_x], [closest_y]) # noqa self.marks[viewer._reference_id].visible = True + def _image_viewer_update(self, viewer, x, y): + # Extract first dataset from visible layers and use this for coordinates - the choice + # of dataset shouldn't matter if the datasets are linked correctly + active_layer = viewer.active_image_layer + if active_layer is None: + self._viewer_mouse_clear_event(viewer) + return + + # TODO: refactor this code block (to retrieve the active layer) + # upstream to make it resuable + from glue_jupyter.bqplot.image.layer_artist import BqplotImageSubsetLayerArtist + + if self.dataset.selected == 'auto': + image = active_layer.layer + elif self.dataset.selected == 'none': + active_layer = viewer.layers[0].state + image = viewer.layers[0].layer + else: + for layer in viewer.layers: + if layer.layer.label == self.dataset.selected and layer.visible: + if isinstance(layer, BqplotImageSubsetLayerArtist): + # cannot expose info for spatial subset layers + continue + active_layer = layer.state + image = layer.layer + break + else: + image = None + + self._dict['axes_x'] = x + self._dict['axes_x:unit'] = 'pix' + self._dict['axes_y'] = y + self._dict['axes_y:unit'] = 'pix' + + # set default empty values + if self.dataset.selected != 'none' and image is not None: + self.icon = self.app.state.layer_icons.get(image.label, '') # noqa + self._dict['data_label'] = image.label + + if self.dataset.selected == 'none' or image is None: + self.icon = 'mdi-cursor-default' + self._dict['data_label'] = '' + else: + time = viewer.slice_value + # TODO: store slice unit within image viewer to avoid this assumption? + time_unit = str(self.app._jdaviz_helper.default_time_viewer._obj.time_unit) + self.row2_title = 'Time' + self.row2_text = f'{time:0.5f} {time_unit}' + self._dict['time'] = time + self._dict['time:unit'] = time_unit + + maxsize = int(np.ceil(np.log10(np.max(active_layer.layer.shape)))) + 3 + fmt = 'x={0:0' + str(maxsize) + '.1f} y={1:0' + str(maxsize) + '.1f}' + self.row1a_title = 'Pixel' + self.row1a_text = (fmt.format(x, y)) + self._dict['pixel'] = (float(x), float(y)) + + if self.dataset.selected == 'none' or image is None: + # no data values to extract + self.row1b_title = '' + self.row1b_text = '' + return + + # Extract data values at this position. + # Check if shape is [x, y, z] or [y, x] and show value accordingly. + ix_shape, iy_shape = self._image_shape_inds(image) + + if (-0.5 < x < image.shape[ix_shape] - 0.5 and -0.5 < y < image.shape[iy_shape] - 0.5 + and hasattr(active_layer, 'attribute')): + attribute = active_layer.attribute + arr = image.get_component(attribute).data + unit = image.get_component(attribute).units + value = self._get_cube_value(image, arr, x, y, viewer) + self.row1b_title = 'Value' + self.row1b_text = f'{value:+10.5e} {unit}' + self._dict['value'] = float(value) + self._dict['value:unit'] = unit + else: + self.row1b_title = '' + self.row1b_text = '' + def update_display(self, viewer, x, y): self._dict = {} diff --git a/lcviz/plugins/markers/markers.py b/lcviz/plugins/markers/markers.py index 78dc2d8f..07cd798e 100644 --- a/lcviz/plugins/markers/markers.py +++ b/lcviz/plugins/markers/markers.py @@ -21,12 +21,16 @@ class Markers(Markers): * :meth:`~jdaviz.core.template_mixin.TableMixin.export_table` """ _default_table_values = {'time': np.nan, + 'time:unit': '', 'phase': np.nan, 'ephemeris': '', - 'flux': np.nan} + 'pixel': (np.nan, np.nan), + 'value': np.nan, + 'value:unit': ''} def __init__(self, *args, **kwargs): - kwargs['headers'] = ['time', 'phase', 'ephemeris', 'flux', 'viewer'] + kwargs['headers'] = ['time', 'time:unit', 'phase', 'ephemeris', + 'pixel', 'value', 'value:unit', 'viewer'] super().__init__(*args, **kwargs) self.docs_link = f"https://lcviz.readthedocs.io/en/{self.vdocs}/plugins.html#markers" diff --git a/lcviz/tests/test_plugin_markers.py b/lcviz/tests/test_plugin_markers.py index b96fee1a..c6af3635 100644 --- a/lcviz/tests/test_plugin_markers.py +++ b/lcviz/tests/test_plugin_markers.py @@ -1,3 +1,4 @@ +import pytest import numpy as np from numpy.testing import assert_allclose @@ -52,6 +53,7 @@ def test_plugin_markers(helper, light_curve_like_kepler_quarter): _assert_dict_allclose(label_mouseover.as_dict(), {'data_label': 'Light curve', 'time': 5.4583335, + 'time:unit': 'd', 'phase': np.nan, 'ephemeris': '', 'axes_x': 5.4583335, @@ -59,7 +61,8 @@ def test_plugin_markers(helper, light_curve_like_kepler_quarter): 'index': 262.0, 'axes_y': 0.96758735, 'axes_y:unit': '', - 'flux': 0.96758735}) + 'value': 0.96758735, + 'value:unit': ''}) mp._obj._on_viewer_key_event(tv, {'event': 'keydown', 'key': 'm'}) @@ -80,6 +83,7 @@ def test_plugin_markers(helper, light_curve_like_kepler_quarter): _assert_dict_allclose(label_mouseover.as_dict(), {'data_label': 'Light curve', 'time': 5.458333374001086, + 'time:unit': 'd', 'phase': 0.4583333730697632, 'ephemeris': 'default', 'axes_x': 0.4583333730697632, @@ -87,7 +91,8 @@ def test_plugin_markers(helper, light_curve_like_kepler_quarter): 'index': 262.0, 'axes_y': 0.9675873517990112, 'axes_y:unit': '', - 'flux': 0.9675873517990112}) + 'value': 0.9675873517990112, + 'value:unit': ''}) mp._obj._on_viewer_key_event(pv, {'event': 'keydown', 'key': 'm'}) @@ -112,6 +117,46 @@ def test_plugin_markers(helper, light_curve_like_kepler_quarter): 'axes_y:unit': '', 'data_label': '', 'time': np.nan, + 'time:unit': '', 'phase': 0.6, - 'flux': 0, + 'value': 0, + 'value:unit': '', 'ephemeris': ''}) + + +@pytest.mark.remote_data +def test_tpf_markers(helper, light_curve_like_kepler_quarter): + helper.load_data(light_curve_like_kepler_quarter) + + # TODO: replace with test fixture + from lightkurve import search_targetpixelfile + tpf = search_targetpixelfile("KIC 001429092", + mission="Kepler", + cadence="long", + quarter=10).download() + helper.load_data(tpf) + + mp = helper.plugins['Markers'] + label_mouseover = mp._obj.coords_info + mp.open_in_tray() + + # test event in image (TPF) viewer + iv = helper.viewers['image']._obj + label_mouseover._viewer_mouse_event(iv, + {'event': 'mousemove', + 'domain': {'x': 0, 'y': 0}}) + + assert label_mouseover.as_text() == ('Pixel x=00000.0 y=00000.0 Value +1.28035e+01 electron / s', # noqa + 'Time 47.00689 d', + '') + + _assert_dict_allclose(label_mouseover.as_dict(), {'data_label': 'KIC 1429092[TPF]', + 'time': 47.00688790508866, + 'time:unit': 'd', + 'pixel': (0.0, 0.0), + 'axes_x': 0, + 'axes_x:unit': 'pix', + 'axes_y': 0, + 'axes_y:unit': 'pix', + 'value': 12.803528785705566, + 'value:unit': 'electron / s'})