diff --git a/CHANGES.rst b/CHANGES.rst index ca3f5c893a..440408d940 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,9 @@ New Features - Adding Data Quality plugin for Imviz and Cubeviz. [#2767, #2817] +- Enable exporting spectral regions to ECSV files readable by ``astropy.table.QTable`` or + ``specutils.SpectralRegion`` [#2843] + Cubeviz ^^^^^^^ @@ -73,6 +76,8 @@ Other Changes and Additions - Line menu in Redshift from Centroid section of Line Analysis now shows values in current units. [#2816, #2831] +- Bump required specutils version to 1.15. [#2843] + 3.9.2 (unreleased) ================== diff --git a/docs/specviz/export_data.rst b/docs/specviz/export_data.rst index 5f7111bad0..886098751c 100644 --- a/docs/specviz/export_data.rst +++ b/docs/specviz/export_data.rst @@ -14,7 +14,7 @@ those data currently back into your Jupyter notebook: specviz.get_spectra() -which yields a either a single `specutils.Spectrum1D` object or a dictionary of +which yields a either a single `specutils.Spectrum1D` object or a dictionary of `specutils.Spectrum1D` (if there are multiple displayed spectra) that you can manipulate however you wish. You can then load the modified spectrum back into the notebook via the API described in :ref:`specviz-import-api`. @@ -70,6 +70,11 @@ To extract the spectral region you want: myregion = regions["Subset 2"] +.. seealso:: + + :ref:`Export From Plugins ` + Spectral region subsets can also be exported to disk as an ECSV file. + .. _specviz-export-model: Model Fits diff --git a/docs/specviz/plugins.rst b/docs/specviz/plugins.rst index 72d0c1ebd3..8770eb7be8 100644 --- a/docs/specviz/plugins.rst +++ b/docs/specviz/plugins.rst @@ -320,4 +320,8 @@ using the |icon-line-select| (line selector) tool in the spectrum viewer. Export ====== -This plugin allows a given viewer's plot to be exported to various image formats. +This plugin allows exporting the contents of a viewer or a plot within a plugin to various image formats. +Additionally, spatial and spectral regions can be exported to files, as astropy regions saved as FITS or REG +files (in the case of spatial regions), or as ECSV files in the case of spectral regions via specutils SpectralRegion. +Note that multiple spectral regions can be saved out to the same file, as long as they are subregions of a single +subset rather than independent subsets. \ No newline at end of file diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index d3af6a8810..73978196d1 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -111,9 +111,6 @@ def __init__(self, *args, **kwargs): self.dataset.filters = ['is_not_wcs_only', 'not_child_layer', 'from_plugin'] - # NOTE: if/when adding support for spectral subsets, update the languange in the UI - self.subset.filters = ['is_spatial'] - viewer_format_options = ['png', 'svg'] if self.config == 'cubeviz': if not self.app.state.settings.get('server_is_remote'): @@ -135,7 +132,9 @@ def __init__(self, *args, **kwargs): selected='plugin_table_format_selected', manual_options=plugin_table_format_options) - subset_format_options = ['fits', 'reg'] + subset_format_options = [{'label': 'fits', 'value': 'fits', 'disabled': False}, + {'label': 'reg', 'value': 'reg', 'disabled': False}, + {'label': 'ecsv', 'value': 'ecsv', 'disabled': True}] self.subset_format = SelectPluginComponent(self, items='subset_format_items', selected='subset_format_selected', @@ -226,6 +225,8 @@ def _sync_singleselect(self, event): if name != attr: setattr(self, attr, '') if attr == 'subset_selected': + if self.subset.selected != '': + self._update_subset_format_disabled() self._set_subset_not_supported_msg() if attr == 'dataset_selected': self._set_dataset_not_supported_msg() @@ -258,6 +259,46 @@ def _is_filename_changed(self, event): # Clear overwrite warning when user changes filename self.overwrite_warn = False + def _update_subset_format_disabled(self): + new_items = [] + if self.subset.selected is not None: + subset = self.app.get_subsets(self.subset.selected) + if self.app._is_subset_spectral(subset[0]): + good_formats = ["ecsv"] + else: + good_formats = ["fits", "reg"] + for item in self.subset_format_items: + if item["label"] in good_formats: + item["disabled"] = False + else: + item["disabled"] = True + if item["label"] == self.subset_format.selected: + self.subset_format.selected = good_formats[0] + new_items.append(item) + self.subset_format_items = [] + self.subset_format_items = new_items + + @observe('subset_format_selected') + def _disable_subset_format_combo(self, event): + # Disable selecting a bad subset+format combination from the API + if self.subset.selected == '' or self.subset.selected is None: + return + subset = self.app.get_subsets(self.subset.selected) + bad_combo = False + if self.app._is_subset_spectral(subset[0]): + if event['new'] != "ecsv": + bad_combo = True + elif event['new'] == "ecsv": + bad_combo = True + + if bad_combo: + # Set back to a good value and raise error + good_format = [format["label"] for format in self.subset_format_items if + format["disabled"] is False][0] + self.subset_format.selected = good_format + raise ValueError(f"Cannot export {self.subset.selected} in {event['new']}" + f" format, reverting selection to {self.subset_format.selected}") + def _set_subset_not_supported_msg(self, msg=None): """ Check if selected subset is spectral or composite, and warn and @@ -269,7 +310,7 @@ def _set_subset_not_supported_msg(self, msg=None): if self.subset.selected == '': self.subset_invalid_msg = '' elif self.app._is_subset_spectral(subset[0]): - self.subset_invalid_msg = 'Export for spectral subsets not yet supported.' + self.subset_invalid_msg = '' elif len(subset) > 1: self.subset_invalid_msg = 'Export for composite subsets not yet supported.' else: @@ -422,7 +463,11 @@ def export(self, filename=None, show_dialog=None, overwrite=False, if raise_error_for_overwrite: raise FileExistsError(f"{filename} exists but overwrite=False") return - self.save_subset_as_region(selected_subset_label, filename) + + if self.subset_format.selected in ('fits', 'reg'): + self.save_subset_as_region(selected_subset_label, filename) + elif self.subset_format.selected == 'ecsv': + self.save_subset_as_table(filename) elif len(self.dataset.selected): filetype = self.dataset_format.selected @@ -655,6 +700,10 @@ def save_subset_as_region(self, selected_subset_label, filename): region.write(filename, overwrite=True) + def save_subset_as_table(self, filename): + region = self.app.get_subsets(subset_name=self.subset.selected) + region.write(filename) + def vue_interrupt_recording(self, *args): # pragma: no cover self.movie_interrupt = True # TODO: this will need updating when batch/multiselect support is added diff --git a/jdaviz/configs/default/plugins/export/export.vue b/jdaviz/configs/default/plugins/export/export.vue index 979738f5f1..16354a61bb 100644 --- a/jdaviz/configs/default/plugins/export/export.vue +++ b/jdaviz/configs/default/plugins/export/export.vue @@ -33,7 +33,7 @@ :items="viewer_format_items.map(i => i.label)" label="Format" hint="Image format for exporting viewers." - :disabled="viewer_selected.length == 0" + :disabled="viewer_selected.length == 0" persistent-hint > @@ -126,9 +126,9 @@
- Spatial Subsets + Subsets - Export spatial subset as astropy region. + Export spatial subset as astropy region or spectral subset as specutils SpectralRegion. - + :widget="plugin_plot_selected_widget"/>
- + =8.0.6", "voila>=0.4,<0.5", "pyyaml>=5.4.1", - "specutils>=1.9", + "specutils>=1.15", "specreduce>=1.3.0,<1.4.0", "photutils>=1.4", "glue-astronomy>=0.10",