diff --git a/CHANGES.rst b/CHANGES.rst index 8ecc95cec6..311c642020 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,8 @@ New Features - Line list plugin now supports exact-text filtering on line names. [#1298] - Added a Subset Tools plugin for viewing information about defined subsets. [#1292] +- Data menus in the viewers are filtered to applicable entries only and support removing generated data from + the app. [#1313] - Offscreen indication for spectral lines and slice indicator. [#1312] diff --git a/jdaviz/app.py b/jdaviz/app.py index ba12d44691..5954a624c5 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -25,6 +25,7 @@ from glue.core.state_objects import State from glue.core.subset import Subset, RangeSubsetState, RoiSubsetState from glue_jupyter.app import JupyterApplication +from glue_jupyter.common.toolbar_vuetify import read_icon from glue_jupyter.state_traitlets_helpers import GlueState from glue_jupyter.bqplot.profile import BqplotProfileView from ipyvuetify import VuetifyTemplate @@ -37,6 +38,7 @@ ViewerAddedMessage, ViewerRemovedMessage) from jdaviz.core.registries import (tool_registry, tray_registry, viewer_registry, data_parser_registry) +from jdaviz.core.tools import ICON_DIR from jdaviz.utils import SnackbarQueue __all__ = ['Application'] @@ -54,57 +56,29 @@ # some glue-core versions glue_settings.DATA_ALPHA = 1 -ipyvue.register_component_from_file(None, 'j-tooltip', - os.path.join(os.path.dirname(__file__), - 'components/tooltip.vue')) +custom_components = {'j-tooltip': 'components/tooltip.vue', + 'j-external-link': 'components/external_link.vue', + 'j-docs-link': 'components/docs_link.vue', + 'j-viewer-data-select': 'components/viewer_data_select.vue', + 'j-viewer-data-select-item': 'components/viewer_data_select_item.vue', + 'j-tray-plugin': 'components/tray_plugin.vue', + 'j-play-pause-widget': 'components/play_pause_widget.vue', + 'j-plugin-section-header': 'components/plugin_section_header.vue', + 'j-number-uncertainty': 'components/number_uncertainty.vue', + 'plugin-dataset-select': 'components/plugin_dataset_select.vue', + 'plugin-subset-select': 'components/plugin_subset_select.vue', + 'plugin-viewer-select': 'components/plugin_viewer_select.vue', + 'plugin-add-results': 'components/plugin_add_results.vue', + 'plugin-auto-label': 'components/plugin_auto_label.vue'} -ipyvue.register_component_from_file(None, 'j-external-link', - os.path.join(os.path.dirname(__file__), - 'components/external_link.vue')) - -ipyvue.register_component_from_file(None, 'j-docs-link', - os.path.join(os.path.dirname(__file__), - 'components/docs_link.vue')) - -ipyvue.register_component_from_file(None, 'j-tray-plugin', - os.path.join(os.path.dirname(__file__), - 'components/tray_plugin.vue')) - -ipyvue.register_component_from_file(None, 'j-plugin-section-header', - os.path.join(os.path.dirname(__file__), - 'components/plugin_section_header.vue')) - -ipyvue.register_component_from_file(None, 'j-number-uncertainty', - os.path.join(os.path.dirname(__file__), - 'components/number_uncertainty.vue')) - -ipyvue.register_component_from_file(None, 'plugin-dataset-select', - os.path.join(os.path.dirname(__file__), - 'components/plugin_dataset_select.vue')) - -ipyvue.register_component_from_file(None, 'plugin-subset-select', - os.path.join(os.path.dirname(__file__), - 'components/plugin_subset_select.vue')) - -ipyvue.register_component_from_file(None, 'plugin-viewer-select', - os.path.join(os.path.dirname(__file__), - 'components/plugin_viewer_select.vue')) - -ipyvue.register_component_from_file(None, 'plugin-add-results', - os.path.join(os.path.dirname(__file__), - 'components/plugin_add_results.vue')) - -ipyvue.register_component_from_file(None, 'plugin-auto-label', - os.path.join(os.path.dirname(__file__), - 'components/plugin_auto_label.vue')) # Register pure vue component. This allows us to do recursive component instantiation only in the # vue component file -ipyvue.register_component_from_file('g-viewer-tab', "container.vue", __file__) +for name, path in custom_components.items(): + ipyvue.register_component_from_file(None, name, + os.path.join(os.path.dirname(__file__), path)) -ipyvue.register_component_from_file(None, 'j-play-pause-widget', - os.path.join(os.path.dirname(__file__), - 'components/play_pause_widget.vue')) +ipyvue.register_component_from_file('g-viewer-tab', "container.vue", __file__) class ApplicationState(State): @@ -151,6 +125,11 @@ class ApplicationState(State): } }, docstring="Top-level application settings.") + icons = DictCallbackProperty({ + 'radialtocheck': read_icon(os.path.join(ICON_DIR, 'radialtocheck.svg'), 'svg+xml'), + 'checktoradial': read_icon(os.path.join(ICON_DIR, 'checktoradial.svg'), 'svg+xml') + }, docstring="Custom application icons") + data_items = ListCallbackProperty( docstring="List of data items parsed from the Glue data collection.") @@ -1124,11 +1103,14 @@ def vue_data_item_selected(self, event): """ viewer_id, item_id, checked = event['id'], event['item_id'], event['checked'] viewer_item = self._viewer_item_by_id(viewer_id) + replace = event.get('replace', False) if viewer_item is None: raise ValueError(f'viewer {viewer_id} not found') - if checked: + if replace: + selected_items = [item_id] + elif checked: selected_items = [*viewer_item['selected_data_items'], item_id] else: selected_items = list(filter( @@ -1136,6 +1118,9 @@ def vue_data_item_selected(self, event): self._update_selected_data_items(viewer_id, selected_items) + def vue_data_item_remove(self, event): + self.data_collection.remove(self.data_collection[event['item_name']]) + def vue_close_snackbar_message(self, event): """ Callback to close a message in the snackbar when the "close" @@ -1225,7 +1210,7 @@ def _on_data_added(self, msg): the new data. """ self._link_new_data() - data_item = self._create_data_item(msg.data.label) + data_item = self._create_data_item(msg.data) self.state.data_items.append(data_item) def _on_data_deleted(self, msg): @@ -1244,11 +1229,39 @@ def _on_data_deleted(self, msg): self.state.data_items.remove(data_item) @staticmethod - def _create_data_item(label): + def _create_data_item(data): + ndims = len(data.shape) + wcsaxes = data.meta.get('WCSAXES', None) + if wcsaxes is None: + # then we'll need to determine type another way, we want to avoid + # this when we can though since its not as cheap + component_ids = [str(c) for c in data.component_ids()] + if data.label == 'MOS Table': + typ = 'table' + elif ndims == 1: + typ = '1d spectrum' + elif ndims == 2 and wcsaxes is not None: + if wcsaxes == 3: + typ = '2d spectrum' + elif wcsaxes == 2: + typ = 'image' + else: + typ = 'unknown' + elif ndims == 2 and wcsaxes is None: + typ = '2d spectrum' if 'Wavelength' in component_ids else 'image' + elif ndims == 3: + typ = 'cube' + else: + typ = 'unknown' + # we'll expose any information we need here. For "meta", not all entries are guaranteed + # to be serializable, so we'll just send those that we need. return { 'id': str(uuid.uuid4()), - 'name': label, + 'name': data.label, 'locked': False, + 'ndims': data.ndim, + 'type': typ, + 'meta': {k: v for k, v in data.meta.items() if k in ['Plugin', 'mosviz_row']}, 'children': []} @staticmethod diff --git a/jdaviz/app.vue b/jdaviz/app.vue index fb45dec8a8..60f58c6ad4 100644 --- a/jdaviz/app.vue +++ b/jdaviz/app.vue @@ -38,10 +38,13 @@ v-for="(stack, index) in state.stack_items" :stack="stack" :key="stack.viewers.map(v => v.id).join('-')" - :data-items="state.data_items" + :data_items="state.data_items" + :app_settings="state.settings" + :icons="state.icons" @resize="relayout" :closefn="destroy_viewer_item" @data-item-selected="data_item_selected($event)" + @data-item-remove="data_item_remove($event)" @call-viewer-method="call_viewer_method($event)" > @@ -134,6 +137,7 @@ export default { secondary: "#007DA4", accent: "#C75109", turquoise: "#007BA1", + lightblue: "#E3F2FD", // matches highlighted row in MOS table spinner: "#163C4C", error: '#FF5252', info: '#2196F3', @@ -147,6 +151,7 @@ export default { secondary: "#007DA4", accent: "#C75109", turquoise: "#007BA1", + lightblue: "#E3F2FD", spinner: "#ACE1FF", error: '#FF5252', info: '#2196F3', diff --git a/jdaviz/components/tooltip.vue b/jdaviz/components/tooltip.vue index 2579040a88..2b80f9d27e 100644 --- a/jdaviz/components/tooltip.vue +++ b/jdaviz/components/tooltip.vue @@ -46,6 +46,11 @@ const tooltips = { 'viewer-toolbar-figure-save': 'Save figure', 'viewer-toolbar-menu': 'Adjust display: contrast, bias, stretch', 'viewer-toolbar-more': 'More options...', + 'viewer-data-select-enabled': 'Allow multiple entries (click to enable replace)', + 'viewer-data-radio-enabled': 'Replace current entry (click to enable multi-select)', + 'viewer-data-select': 'Toggle whether data entry is loaded in the viewer', + 'viewer-data-radio': 'Change viewer to this data entry', + 'viewer-data-delete': 'Remove data entry across entire app', 'table-prev': 'Select previous row in table', 'table-next': 'Select next row in table', diff --git a/jdaviz/components/viewer_data_select.vue b/jdaviz/components/viewer_data_select.vue new file mode 100644 index 0000000000..5dee8ca977 --- /dev/null +++ b/jdaviz/components/viewer_data_select.vue @@ -0,0 +1,192 @@ + + diff --git a/jdaviz/components/viewer_data_select_item.vue b/jdaviz/components/viewer_data_select_item.vue new file mode 100644 index 0000000000..99261c3fce --- /dev/null +++ b/jdaviz/components/viewer_data_select_item.vue @@ -0,0 +1,53 @@ + + + diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index 8f9a106032..6ad2f5adb2 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -16,10 +16,16 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): mm.n_moment = 0 # Collapsed sum, will get back 2D spatial image assert mm.results_label == 'moment 0' + + mm.add_results.viewer.selected = 'mask-viewer' mm.vue_calculate_moment() assert mm.moment_available assert dc[1].label == 'moment 0' + mv_data = cubeviz_helper.app.get_viewer('mask-viewer').data() + # by default, will overwrite the previous entry (so only one data entry) + assert len(mv_data) == 1 + assert mv_data[0].label == 'moment 0' assert len(dc.links) == 8 diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_data_selection.py b/jdaviz/configs/cubeviz/plugins/tests/test_data_selection.py new file mode 100644 index 0000000000..31c3c2fc8e --- /dev/null +++ b/jdaviz/configs/cubeviz/plugins/tests/test_data_selection.py @@ -0,0 +1,39 @@ +import pytest + + +@pytest.mark.filterwarnings('ignore:No observer defined on WCS') +def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmpdir): + app = cubeviz_helper.app + # NOTE: these are the same underlying data. This works fine for the current scope + # of the tests (to make sure checking/unchecking operations change the data exposed + # in the viewer), but will need to be more advanced if we extend tests here to + # cover scrubbing/linking/etc + app.add_data(spectrum1d_cube, 'cube1') + app.add_data(spectrum1d_cube, 'cube2') + app.add_data_to_viewer('flux-viewer', 'cube1') + fv = app.get_viewer('flux-viewer') + + assert len(app.state.data_items) == 2 + assert len(fv.data()) == 1 + assert fv.data()[0].label == app.state.data_items[0]['name'] + + # by default, the image viewers will use replace logic + app.vue_data_item_selected({'id': 'cubeviz-0', + 'item_id': app.state.data_items[1]['id'], + 'checked': True, + 'replace': True}) + + assert len(fv.data()) == 1 + assert fv.data()[0].label == app.state.data_items[1]['name'] + + # but also has the option to display multiple layers + app.vue_data_item_selected({'id': 'cubeviz-0', + 'item_id': app.state.data_items[0]['id'], + 'checked': True, + 'replace': False}) + + assert len(fv.data()) == 2 + + app.vue_data_item_remove({'item_name': app.state.data_items[1]['name']}) + + assert len(fv.data()) == 1 diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py index c2304ba8b4..2f3c3282fc 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/gaussian_smooth.py @@ -43,7 +43,7 @@ def __init__(self, *args, **kwargs): @observe("dataset_selected", "dataset_items", "stddev", "selected_mode") def _set_default_results_label(self, event={}): label_comps = [] - if hasattr(self, 'dataset') and len(self.dataset.labels) > 1: + if hasattr(self, 'dataset') and (len(self.dataset.labels) > 1 or self.app.config == 'mosviz'): # noqa label_comps += [self.dataset_selected] if self.config == "cubeviz": label_comps += [f"{self.selected_mode.lower()}-smooth"] diff --git a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py index d4643933fc..ee9c98568e 100644 --- a/jdaviz/configs/default/plugins/model_fitting/model_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/model_fitting.py @@ -414,7 +414,7 @@ def _model_equation_changed(self, event): @observe("dataset_selected", "dataset_items", "cube_fit") def _set_default_results_label(self, event={}): label_comps = [] - if hasattr(self, 'dataset') and len(self.dataset.labels) > 1: + if hasattr(self, 'dataset') and (len(self.dataset.labels) > 1 or self.app.config == 'mosviz'): # noqa label_comps += [self.dataset_selected] if self.cube_fit: label_comps += ["cube-fit"] diff --git a/jdaviz/configs/mosviz/helper.py b/jdaviz/configs/mosviz/helper.py index de18f625f3..8a21578814 100644 --- a/jdaviz/configs/mosviz/helper.py +++ b/jdaviz/configs/mosviz/helper.py @@ -254,6 +254,8 @@ def _is_world_flipped(self): def _row_click_message_handler(self, msg): self._handle_image_zoom(msg) self._handle_flipped_data() + # expose the row to vue for each of the viewers + self.app.state.settings = {**self.app.state.settings, 'mosviz_row': msg.selected_index} def _handle_image_zoom(self, msg): mos_data = self.app.data_collection['MOS Table'] diff --git a/jdaviz/configs/mosviz/plugins/parsers.py b/jdaviz/configs/mosviz/plugins/parsers.py index ba2bc639cf..ff9fd6a010 100644 --- a/jdaviz/configs/mosviz/plugins/parsers.py +++ b/jdaviz/configs/mosviz/plugins/parsers.py @@ -221,9 +221,10 @@ def mos_spec1d_parser(app, data_obj, data_labels=None): with app.data_collection.delay_link_manager_update(): - for cur_data, cur_label in zip(data_obj, data_labels): + for i, (cur_data, cur_label) in enumerate(zip(data_obj, data_labels)): # Make metadata layout conform with other viz. cur_data.meta = standardize_metadata(cur_data.meta) + cur_data.meta['mosviz_row'] = i app.add_data(cur_data, cur_label, notify_done=False) @@ -296,6 +297,7 @@ def _parse_as_spectrum1d(path): # TODO: this should not be set to nirspec for all datasets data.meta['INSTRUME'] = 'nirspec' + data.meta['mosviz_row'] = index # Get the corresponding label for this data product label = data_labels[index] @@ -382,6 +384,7 @@ def mos_image_parser(app, data_obj, data_labels=None, share_image=0): for i in n_data_range: data_obj[i].label = data_labels[i] + data_obj[i].meta['mosviz_row'] = i app.add_data(data_obj[i], data_labels[i], notify_done=False) if share_image: @@ -646,6 +649,7 @@ def mos_niriss_parser(app, data_dir, obs_label=""): spec2d = Spectrum1D(data * u.one, spectral_axis=wav, meta=meta) spec2d.meta['INSTRUME'] = 'NIRISS' + spec2d.meta['mosviz_row'] = len(spec_labels_2d) label = f"{filter_name} Source {temp[sci].header['SOURCEID']} spec2d {orientation}" # noqa ra, dec = pupil_id_dict[filter_name][temp[sci].header["SOURCEID"]] @@ -684,6 +688,7 @@ def mos_niriss_parser(app, data_dir, obs_label=""): for spec in specs: # Make metadata layout conform with other viz. spec.meta = standardize_metadata(spec.meta) + spec.meta['mosviz_row'] = len(spec_labels_1d) if spec.meta['SPORDER'] == 1 and spec.meta['EXTNAME'] == "EXTRACT1D": label = f"{filter_name} Source {spec.meta['SOURCEID']} spec1d {orientation}" diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py index 0a246cf978..2c9d8dd033 100644 --- a/jdaviz/configs/mosviz/plugins/viewers.py +++ b/jdaviz/configs/mosviz/plugins/viewers.py @@ -230,6 +230,16 @@ def _on_row_selected(self, event): selected_index = event['new'] mos_data = self.session.data_collection['MOS Table'] + # plugin data entries: select all in new row, deselect all others + for data_item in self.jdaviz_app.data_collection: + if data_item.meta.get('Plugin') is not None: + if data_item.meta.get('mosviz_row') == selected_index: + self.session.hub.broadcast(AddDataToViewerMessage( + 'spectrum-viewer', data_item.label, sender=self)) + else: + self.session.hub.broadcast(RemoveDataFromViewerMessage( + 'spectrum-viewer', data_item.label, sender=self)) + for component in mos_data.components: comp_data = mos_data.get_component(component).data selected_data = comp_data[selected_index] diff --git a/jdaviz/container.vue b/jdaviz/container.vue index 10a4c9b198..6f7bf48b41 100644 --- a/jdaviz/container.vue +++ b/jdaviz/container.vue @@ -4,10 +4,13 @@ v-for="(child, index) in stack.children" :stack="child" :key="index" - :data-items="dataItems" + :data_items="data_items" + :app_settings="app_settings" + :icons="icons" @resize="$emit('resize')" :closefn="closefn" @data-item-selected="$emit('data-item-selected', $event)" + @data-item-remove="$emit('data-item-remove', $event)" @call-viewer-method="$emit('call-viewer-method', $event)" >
- - - + - - - - - @@ -83,7 +63,7 @@