diff --git a/CHANGES.rst b/CHANGES.rst index b5224d3bec..5492039d89 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ New Features - Histogram plot in Plot Options now includes tool to set stretch vmin and vmax. [#2513] +- User can now remove data from the app completely after removing it from viewers. [#2409] + Cubeviz ^^^^^^^ diff --git a/docs/imviz/displayimages.rst b/docs/imviz/displayimages.rst index db728dfc4c..386aa69b0e 100644 --- a/docs/imviz/displayimages.rst +++ b/docs/imviz/displayimages.rst @@ -20,11 +20,27 @@ Selecting a Data Set Data can be selected and de-selected in each viewer's data menu, opened by clicking the |icon-viewer-data-select| button in the top left of the viewer. Here, you can click a -checkbox next to the listed data to make the data visible (checked) or invisible (unchecked). -The datasets available in each viewer are filtered +checkbox to the left of the listed data to make the data visible (checked) or invisible +(unchecked). The datasets available in each viewer are filtered to include only compatible data, so you may not see all loaded data in the menu for every viewer. For example, 1D spectra will not be available in the image viewers. +In addition to selecting and de-selecting data to toggle its visibility in the viewer, you +can also unload the data from the viewer completely by clicking the ``X`` to the right of the +data label. Any data that still exists in Imviz but has been unloaded from the viewer +is listed in a separate section that is hidden by default but can can be expanded by clicking +on the section header: + +.. image:: img/imviz_removed_data.png + +This section can be hidden by clicking the section header again. Unloaded data will be available +to re-load into the viewer (by clicking the ``+`` icon) or remove permanently from the app (by +clicking the trashcan icon). + +.. warning:: + Deleting the first image that was loaded into Imviz may be slow, as deleting this image + requires Imviz to re-link any remaining data together and redefine any existing subsets. + .. _imviz_cursor_info: Cursor Information diff --git a/docs/imviz/img/imviz_removed_data.png b/docs/imviz/img/imviz_removed_data.png new file mode 100644 index 0000000000..c799ae631e Binary files /dev/null and b/docs/imviz/img/imviz_removed_data.png differ diff --git a/docs/specviz/displaying.rst b/docs/specviz/displaying.rst index a84581af04..1e54a61afb 100644 --- a/docs/specviz/displaying.rst +++ b/docs/specviz/displaying.rst @@ -33,13 +33,15 @@ Data can be selected and de-selected in each viewer's data menu, opened by click |icon-viewer-data-select| button in the top left of the viewer. Here, you can click a checkbox next to the listed data to make the data visible (checked) or invisible (unchecked). +.. image:: img/data_tab.png + In addition to toggling the visibility of a data layer, the data can be unloaded from a viewer -by clicking the "x" button on the right. Data unloaded from the viewer will also be excluded +by clicking the ``X`` button on the right. Data unloaded from the viewer will also be excluded as options from dataset dropdown menus in the various plugins. Unloaded data will be available -to re-load into the viewer or remove permanently from the app from an expandable section in the -data menu. +to re-load into the viewer (by clicking the ``+`` icon) or remove permanently from the app (by +clicking the trashcan icon) from an expandable section in the data menu: -.. image:: img/data_tab.png +.. image:: img/specviz_remove_data.png .. _specviz_cursor_info: diff --git a/docs/specviz/img/specviz_remove_data.png b/docs/specviz/img/specviz_remove_data.png new file mode 100644 index 0000000000..e1bc57d3b6 Binary files /dev/null and b/docs/specviz/img/specviz_remove_data.png differ diff --git a/jdaviz/app.py b/jdaviz/app.py index 2eab6848fd..dfd57f050b 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -35,11 +35,13 @@ from glue.core.state_objects import State from glue.core.subset import (Subset, RangeSubsetState, RoiSubsetState, CompositeSubsetState, InvertState) +from glue.core.roi import CircularROI, CircularAnnulusROI, EllipticalROI, RectangularROI from glue.core.units import unit_converter from glue_astronomy.spectral_coordinates import SpectralCoordinates from glue_astronomy.translators.regions import roi_subset_state_to_region from glue_jupyter.app import JupyterApplication from glue_jupyter.common.toolbar_vuetify import read_icon +from glue_jupyter.bqplot.common.tools import TrueCircularROI from glue_jupyter.state_traitlets_helpers import GlueState from glue_jupyter.bqplot.profile import BqplotProfileView from ipyvuetify import VuetifyTemplate @@ -1594,6 +1596,7 @@ def remove_data_from_viewer(self, viewer_reference, data_label): viewer.remove_data(data) viewer._layers_with_defaults_applied = [layer_info for layer_info in viewer._layers_with_defaults_applied # noqa if layer_info['data_label'] != data.label] # noqa + remove_data_message = RemoveDataMessage(data, viewer, viewer_id=viewer_id, sender=self) @@ -1891,6 +1894,99 @@ def _get_viewer_item(self, ref_or_id): viewer_item = self._viewer_item_by_id(ref_or_id) return viewer_item + def _reparent_subsets(self, old_parent, new_parent=None): + ''' + Re-parent subsets that belong to the specified data + + Parameters + ---------- + old_parent : glue.core.Data, str + The item from the data collection off of which to move the subset definitions. + + new_parent : glue.core.Data, str + The item from the data collection to make the new parent. If None, the first + item in the data collection that doesn't match ``old_parent`` will be chosen. + ''' + from astropy.wcs.utils import pixel_to_pixel + + if isinstance(old_parent, str): + old_parent = self.data_collection(old_parent) + + if isinstance(new_parent, str): + new_parent = self.data_collection(new_parent) + elif new_parent is None: + for data in self.data_collection: + if data is not old_parent: + new_parent = data + break + + # Set subset attributes to match a remaining data collection member, using get_subsets to + # get components of composite subsets. + for key, subset_list in self.get_subsets(simplify_spectral=False).items(): + # Get the subset group entry for later. Unfortunately can't just index on label. + [subset_group] = [sg for sg in self.data_collection.subset_groups if sg.label == key] + + for subset in subset_list: + subset_state = subset['subset_state'] + # Only reparent if needed + if subset_state.attributes[0].parent is old_parent: + for att in ("att", "xatt", "yatt", "x_att", "y_att"): + if hasattr(subset_state, att): + subset_att = getattr(subset_state, att) + data_components = new_parent.components + if subset_att not in data_components: + cid = [c for c in data_components if c.label == subset_att.label][0] + setattr(subset_state, att, cid) + + # Translate bounds through WCS if needed + if (self.config == "imviz" and + self._jdaviz_helper.plugins["Links Control"].link_type == "WCS"): + # Get the correct link to use for translation + roi = subset_state.roi + if type(roi) in (CircularROI, CircularAnnulusROI, + EllipticalROI, TrueCircularROI): + old_xc, old_yc = subset_state.center() + # Convert center + x, y = pixel_to_pixel(old_parent.coords, new_parent.coords, + roi.xc, roi.yc) + subset_state.move_to(x, y) + + for att in ("radius", "inner_radius", "outer_radius", + "radius_x", "radius_y"): + # Hacky way to get new radii with point on edge of circle + # Do we need to worry about using x for the radius conversion for + # radius_y if there is distortion? + r = getattr(roi, att, None) + if r is not None: + dummy_x = old_xc + r + x2, y2 = pixel_to_pixel(old_parent.coords, new_parent.coords, + dummy_x, old_yc) + new_radius = np.abs(x2 - x) + setattr(roi, att, new_radius) + + elif type(roi) is RectangularROI: + x_min, y_min = pixel_to_pixel(old_parent.coords, new_parent.coords, + roi.xmin, roi.ymin) + x_max, y_max = pixel_to_pixel(old_parent.coords, new_parent.coords, + roi.xmax, roi.ymax) + roi.xmin = x_min + roi.xmax = x_max + roi.ymin = y_min + roi.ymax = y_max + + elif type(subset_group.subset_state) is RangeSubsetState: + range_state = subset_group.subset_state + cur_unit = old_parent.coords.spectral_axis.unit + new_unit = new_parent.coords.spectral_axis.unit + if cur_unit is not new_unit: + range_state.lo, range_state.hi = cur_unit.to(new_unit, [range_state.lo, + range_state.hi]) + + # Force subset plugin to update bounds and such + for subset in subset_group.subsets: + subset_message = SubsetUpdateMessage(sender=subset) + self.hub.broadcast(subset_message) + def vue_destroy_viewer_item(self, cid): """ Callback for when viewer area tabs are destroyed. Finds the viewer item @@ -2022,7 +2118,42 @@ def set_data_visibility(self, viewer_reference, data_label, visible=True, replac viewer.on_limits_change() # Trigger compass redraw def vue_data_item_remove(self, event): - self.data_collection.remove(self.data_collection[event['item_name']]) + + data_label = event['item_name'] + data = self.data_collection[data_label] + self._reparent_subsets(data) + + # Make sure the data isn't loaded in any viewers + for viewer_id in self._viewer_store: + self.remove_data_from_viewer(viewer_id, data_label) + + # Imviz has some extra logic below that can be skipped after data removal if we're not + # removing the reference data, so we check that here. + if self.config == "imviz": + imviz_refdata = False + ref_data, iref = self._jdaviz_helper.get_ref_data() + if data is ref_data: + imviz_refdata = True + + self.data_collection.remove(self.data_collection[data_label]) + + # If there are two or more datasets left we need to link them back together after removing + # the reference data (which would leave 0 external_links). + if len(self.data_collection) > 1 and len(self.data_collection.external_links) == 0: + if self.config == "imviz" and imviz_refdata: + link_type = self._jdaviz_helper.plugins["Links Control"].link_type.selected.lower() + self._jdaviz_helper.link_data(link_type=link_type, error_on_fail=True) + # Hack to restore responsiveness to imviz layers + for viewer_ref in self.get_viewer_reference_names(): + viewer = self.get_viewer(viewer_ref) + loaded_layers = [layer.layer.label for layer in viewer.layers if + "Subset" not in layer.layer.label] + if len(loaded_layers): + self.remove_data_from_viewer(viewer_ref, loaded_layers[-1]) + self.add_data_to_viewer(viewer_ref, loaded_layers[-1]) + else: + for i in range(1, len(self.data_collection)): + self._link_new_data(data_to_be_linked=i) def vue_close_snackbar_message(self, event): """ diff --git a/jdaviz/components/tooltip.vue b/jdaviz/components/tooltip.vue index e31cb349d4..0a71d9802e 100644 --- a/jdaviz/components/tooltip.vue +++ b/jdaviz/components/tooltip.vue @@ -66,7 +66,7 @@ const tooltips = { 'viewer-data-radio': 'Switch visibility to layers associated with this data entry', 'viewer-data-enable': 'Load data entry into this viewer', 'viewer-data-disable': 'Disable data within this viewer (will be hidden and unavailable from plugins until re-enabled)', - 'viewer-data-delete': 'Remove data entry across entire app', + 'viewer-data-delete': 'Remove data entry across entire app (might affect existing subsets)', '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 index fb1bb56e82..161923b305 100644 --- a/jdaviz/components/viewer_data_select.vue +++ b/jdaviz/components/viewer_data_select.vue @@ -17,7 +17,7 @@ - + {{viewerTitleCase}} @@ -50,6 +50,7 @@ :icon="layer_icons[item.name]" :viewer="viewer" :multi_select="multi_select" + :n_data_entries="nDataEntries" @data-item-visibility="$emit('data-item-visibility', $event)" @data-item-unload="$emit('data-item-unload', $event)" @data-item-remove="$emit('data-item-remove', $event)" @@ -79,6 +80,7 @@ :icon="layer_icons[item.name]" :viewer="viewer" :multi_select="multi_select" + :n_data_entries="nDataEntries" @data-item-visibility="$emit('data-item-visibility', $event)" @data-item-remove="$emit('data-item-remove', $event)" > @@ -226,6 +228,10 @@ module.exports = { extraDataItems() { return this.$props.data_items.filter((item) => this.itemIsVisible(item, true)) }, + nDataEntries() { + // return number of data entries in the entire plugin that were NOT created by a plugin + return this.$props.data_items.filter((item) => item.meta.Plugin === undefined).length + }, } }; diff --git a/jdaviz/components/viewer_data_select_item.vue b/jdaviz/components/viewer_data_select_item.vue index f0f74e170d..e687705e85 100644 --- a/jdaviz/components/viewer_data_select_item.vue +++ b/jdaviz/components/viewer_data_select_item.vue @@ -22,8 +22,8 @@ - - + +
{{itemNamePrefix}} @@ -34,7 +34,7 @@
-
+
-
+
mdi-delete
@@ -60,7 +60,7 @@