diff --git a/CHANGES.rst b/CHANGES.rst
index d23f107741..31761344e8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -11,6 +11,8 @@ New Features
- Opacity for spatial subsets is now adjustable from within Plot Options. [#2663]
+- Live-preview of aperture selection in plugins. [#2664]
+
Cubeviz
^^^^^^^
@@ -49,6 +51,9 @@ API Changes
Cubeviz
^^^^^^^
+- ``spatial_subset`` in the spectral extraction plugin is now renamed to ``aperture`` and the deprecated name will
+ be removed in a future release. [#2664]
+
Imviz
^^^^^
diff --git a/docs/dev/ui_style_guide.rst b/docs/dev/ui_style_guide.rst
index e970b72da2..f1709b03f5 100644
--- a/docs/dev/ui_style_guide.rst
+++ b/docs/dev/ui_style_guide.rst
@@ -22,7 +22,7 @@ try to adhere to the following principles:
components are necessary in a single row. Always emphasize readability at the default/minimum
width of the plugin tray, rather than using columns that result in a ton of text overflow.
* Use ```` to align content to the right (such as action buttons).
-* Action buttons should use ``text``
+* Action buttons should use ``text``
to control the color depending on whether the button affects things outside the plugin itself
(adding/modifying data collection or subset entries, etc) or are isolated to within the plugin
(adding model components in model fitting, etc). These buttons can be wrapped in tooltip components
diff --git a/jdaviz/configs/cubeviz/plugins/slice/slice.py b/jdaviz/configs/cubeviz/plugins/slice/slice.py
index 425afbafa6..1b98e6458c 100644
--- a/jdaviz/configs/cubeviz/plugins/slice/slice.py
+++ b/jdaviz/configs/cubeviz/plugins/slice/slice.py
@@ -11,7 +11,8 @@
from specutils.spectra.spectrum1d import Spectrum1D
from jdaviz.core.events import (AddDataMessage, SliceToolStateMessage,
- SliceSelectSliceMessage, GlobalDisplayUnitChanged)
+ SliceSelectSliceMessage, SliceWavelengthUpdatedMessage,
+ GlobalDisplayUnitChanged)
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin
from jdaviz.core.user_api import PluginUserApi
@@ -217,6 +218,10 @@ def _on_slider_updated(self, event):
for viewer in self._indicator_viewers:
viewer._update_slice_indicator(value)
+ self.hub.broadcast(SliceWavelengthUpdatedMessage(slice=value,
+ wavelength=self.wavelength,
+ sender=self))
+
def vue_goto_first(self, *args):
if self.is_playing:
return
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
index acaee9a9ed..c8380a09e4 100644
--- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
+++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
@@ -5,17 +5,19 @@
import numpy as np
import astropy
import astropy.units as u
+from astropy.utils.decorators import deprecated
from astropy.nddata import (
NDDataArray, StdDevUncertainty, NDUncertainty
)
-from traitlets import Bool, List, Unicode, observe
+from traitlets import Bool, Float, List, Unicode, observe
-from jdaviz.core.events import SnackbarMessage
+from jdaviz.core.custom_traitlets import FloatHandleEmpty
+from jdaviz.core.events import SnackbarMessage, SliceWavelengthUpdatedMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin,
DatasetSelectMixin,
SelectPluginComponent,
- SpatialSubsetSelectMixin,
+ ApertureSubsetSelectMixin,
AddResultsMixin,
with_spinner)
from jdaviz.core.user_api import PluginUserApi
@@ -30,8 +32,8 @@
@tray_registry(
'cubeviz-spectral-extraction', label="Spectral Extraction", viewer_requirements='spectrum'
)
-class SpectralExtraction(PluginTemplateMixin, DatasetSelectMixin,
- SpatialSubsetSelectMixin, AddResultsMixin):
+class SpectralExtraction(PluginTemplateMixin, ApertureSubsetSelectMixin,
+ DatasetSelectMixin, AddResultsMixin):
"""
See the :ref:`Spectral Extraction Plugin Documentation ` for more details.
@@ -41,12 +43,20 @@ class SpectralExtraction(PluginTemplateMixin, DatasetSelectMixin,
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
- * ``spatial_subset`` (:class:`~jdaviz.core.template_mixin.SubsetSelect`):
- Subset to use for the spectral extraction, or ``No Subset``.
+ * ``aperture`` (:class:`~jdaviz.core.template_mixin.SubsetSelect`):
+ Subset to use for the spectral extraction, or ``Entire Cube``.
* ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`)
* :meth:`collapse`
"""
template_file = __file__, "spectral_extraction.vue"
+ uses_active_status = Bool(True).tag(sync=True)
+
+ # feature flag for cone support
+ dev_cone_support = Bool(False).tag(sync=True)
+ wavelength_dependent = Bool(False).tag(sync=True)
+ reference_wavelength = FloatHandleEmpty().tag(sync=True)
+ slice_wavelength = Float().tag(sync=True)
+
function_items = List().tag(sync=True)
function_selected = Unicode('Sum').tag(sync=True)
filename = Unicode().tag(sync=True)
@@ -68,6 +78,12 @@ def __init__(self, *args, **kwargs):
self.extracted_spec = None
+ # TODO: in the future this could be generalized with support in SelectPluginComponent
+ self.aperture._default_text = 'Entire Cube'
+ self.aperture._manual_options = ['Entire Cube']
+ self.aperture.items = [{"label": "Entire Cube"}]
+ self.aperture.select_default()
+
self.function = SelectPluginComponent(
self,
items='function_items',
@@ -77,6 +93,9 @@ def __init__(self, *args, **kwargs):
self._set_default_results_label()
self.add_results.viewer.filters = ['is_spectrum_viewer']
+ self.session.hub.subscribe(self, SliceWavelengthUpdatedMessage,
+ handler=self._on_slice_changed)
+
if ASTROPY_LT_5_3_2:
self.disabled_msg = "Spectral Extraction in Cubeviz requires astropy>=5.3.2"
@@ -95,11 +114,44 @@ def user_api(self):
return PluginUserApi(
self,
expose=(
- 'function', 'spatial_subset',
+ 'function', 'spatial_subset', 'aperture',
'add_results', 'collapse_to_spectrum'
)
)
+ @property
+ @deprecated(since="3.9", alternative="aperture")
+ def spatial_subset(self):
+ return self.user_api.aperture
+
+ @property
+ def slice_plugin(self):
+ return self.app._jdaviz_helper.plugins['Slice']
+
+ @observe('wavelength_dependent')
+ def _wavelength_dependent_changed(self, *args):
+ if self.wavelength_dependent:
+ self.reference_wavelength = self.slice_plugin.wavelength
+ # NOTE: this can be redundant in the case where reference_wavelength changed and triggers
+ # the observe, but we need to ensure it is updated if reference_wavelength is unchanged
+ self._update_mark_scale()
+
+ def _on_slice_changed(self, msg):
+ self.slice_wavelength = msg.wavelength
+
+ def vue_goto_reference_wavelength(self, *args):
+ self.slice_plugin.wavelength = self.reference_wavelength
+
+ def vue_adopt_slice_as_reference(self, *args):
+ self.reference_wavelength = self.slice_plugin.wavelength
+
+ @observe('reference_wavelength', 'slice_wavelength')
+ def _update_mark_scale(self, *args):
+ if not self.wavelength_dependent:
+ self.aperture.scale_factor = 1.0
+ return
+ self.aperture.scale_factor = self.slice_wavelength/self.reference_wavelength
+
@with_spinner()
def collapse_to_spectrum(self, add_data=True, **kwargs):
"""
@@ -121,12 +173,12 @@ def collapse_to_spectrum(self, add_data=True, **kwargs):
# defaults to ``No Subset``). Since the Cubeviz parser puts the fluxes
# and uncertainties in different glue Data objects, we translate the spectral
# cube and its uncertainties into separate NDDataArrays, then combine them:
- if self.spatial_subset_selected != self.spatial_subset.default_text:
+ if self.aperture.selected != self.aperture.default_text:
nddata = spectral_cube.get_subset_object(
- subset_id=self.spatial_subset_selected, cls=NDDataArray
+ subset_id=self.aperture.selected, cls=NDDataArray
)
uncertainties = uncert_cube.get_subset_object(
- subset_id=self.spatial_subset_selected, cls=StdDevUncertainty
+ subset_id=self.aperture.selected, cls=StdDevUncertainty
)
else:
nddata = spectral_cube.get_object(cls=NDDataArray)
@@ -242,15 +294,15 @@ def _save_extracted_spec_to_fits(self, overwrite=False, *args):
f"Extracted spectrum saved to {os.path.abspath(filename)}",
sender=self, color="success"))
- @observe('spatial_subset_selected')
+ @observe('aperture_selected')
def _set_default_results_label(self, event={}):
label = "Spectral extraction"
if (
- hasattr(self, 'spatial_subset') and
- self.spatial_subset.selected != self.spatial_subset.default_text
+ hasattr(self, 'aperture') and
+ self.aperture.selected != self.aperture.default_text
):
- label += f' ({self.spatial_subset_selected})'
+ label += f' ({self.aperture_selected})'
self.results_label_default = label
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue
index 553945daa8..41695b4510 100644
--- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue
+++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.vue
@@ -2,6 +2,9 @@
@@ -18,13 +21,53 @@
+
+
cone support is under active development and hidden from users
+
+
+
+
+
+
+
+
+ Adopt Current Slice
+
+
+
+
+
+
+
+
+
+ Slice to Reference Wavelength
+
+
+
+
+
+
+
0
+
+ # sample cube only has 2 slices with wavelengths [4.62280007e-07 4.62360028e-07] m
+ slice_plg.slice = 1
+ assert mark.x[1] == before_x[1]
+
+ slice_plg.slice = 0
+ extract_plg._obj.dev_cone_support = True
+ extract_plg._obj.wavelength_dependent = True
+ assert mark.x[1] == before_x[1]
+
+ slice_plg.slice = 1
+ assert mark.x[1] != before_x[1]
+
+ extract_plg._obj.vue_goto_reference_wavelength()
+ assert slice_plg.slice == 0
+
+ slice_plg.slice = 1
+ extract_plg._obj.vue_adopt_slice_as_reference()
+ extract_plg._obj.vue_goto_reference_wavelength()
+ assert slice_plg.slice == 1
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
index 4a654a85c2..0266860be1 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
@@ -27,6 +27,9 @@ def test_plugin_user_apis(cubeviz_helper):
for plugin_name, plugin_api in cubeviz_helper.plugins.items():
plugin = plugin_api._obj
for attr in plugin_api._expose:
+ if plugin_name == 'Spectral Extraction' and attr == 'spatial_subset':
+ # deprecated, so would raise a deprecation warning
+ continue
assert hasattr(plugin, attr)
diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py
index beaad83648..c909534f7d 100644
--- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py
+++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py
@@ -22,7 +22,8 @@
from jdaviz.core.region_translators import regions2aperture, _get_region_from_spatial_subset
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetMultiSelectMixin,
- SubsetSelect, TableMixin, PlotMixin, with_spinner)
+ SubsetSelect, ApertureSubsetSelectMixin,
+ TableMixin, PlotMixin, with_spinner)
from jdaviz.core.tools import ICON_DIR
from jdaviz.utils import PRIHDR_KEY
@@ -32,7 +33,8 @@
@tray_registry('imviz-aper-phot-simple', label="Aperture Photometry")
-class SimpleAperturePhotometry(PluginTemplateMixin, DatasetMultiSelectMixin, TableMixin, PlotMixin):
+class SimpleAperturePhotometry(PluginTemplateMixin, ApertureSubsetSelectMixin,
+ DatasetMultiSelectMixin, TableMixin, PlotMixin):
"""
The Aperture Photometry plugin performs aperture photometry for drawn regions.
See the :ref:`Aperture Photometry Plugin Documentation ` for more details.
@@ -45,10 +47,9 @@ class SimpleAperturePhotometry(PluginTemplateMixin, DatasetMultiSelectMixin, Tab
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
"""
template_file = __file__, "aper_phot_simple.vue"
+ uses_active_status = Bool(True).tag(sync=True)
multiselect = Bool(False).tag(sync=True)
- aperture_items = List([]).tag(sync=True)
- aperture_selected = Any('').tag(sync=True)
aperture_area = Integer().tag(sync=True)
background_items = List().tag(sync=True)
background_selected = Unicode("").tag(sync=True)
@@ -75,14 +76,6 @@ class SimpleAperturePhotometry(PluginTemplateMixin, DatasetMultiSelectMixin, Tab
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.aperture = SubsetSelect(self,
- 'aperture_items',
- 'aperture_selected',
- multiselect='multiselect',
- dataset='dataset',
- default_text=None,
- filters=['is_spatial', 'is_not_composite', 'is_not_annulus'])
-
self.background = SubsetSelect(self,
'background_items',
'background_selected',
diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue
index cf022a417c..9d4236fe15 100644
--- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue
+++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue
@@ -2,6 +2,9 @@
diff --git a/jdaviz/core/events.py b/jdaviz/core/events.py
index 7e4440ebed..c8008e3a1e 100644
--- a/jdaviz/core/events.py
+++ b/jdaviz/core/events.py
@@ -4,7 +4,7 @@
__all__ = ['NewViewerMessage', 'ViewerAddedMessage', 'ViewerRemovedMessage', 'LoadDataMessage',
'AddDataMessage', 'SnackbarMessage', 'RemoveDataMessage',
'AddLineListMessage', 'RowLockMessage',
- 'SliceSelectSliceMessage',
+ 'SliceSelectSliceMessage', 'SliceWavelengthUpdatedMessage',
'SliceToolStateMessage',
'TableClickMessage', 'LinkUpdatedMessage', 'ExitBatchLoadMessage',
'AstrowidgetMarkersChangedMessage', 'MarkersPluginUpdate',
@@ -315,6 +315,14 @@ def slice(self):
return self._slice
+class SliceWavelengthUpdatedMessage(Message):
+ '''Message generated by the slice plugin when the selected slice and wavelength are updated'''
+ def __init__(self, slice, wavelength, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.slice = slice
+ self.wavelength = wavelength
+
+
class SliceToolStateMessage(Message):
'''Message generated by the select slice plot plugin when activated/deactivated'''
def __init__(self, change, *args, **kwargs):
diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py
index 1e75b4adcc..b6eccb0ef3 100644
--- a/jdaviz/core/marks.py
+++ b/jdaviz/core/marks.py
@@ -17,7 +17,8 @@
'PluginMark', 'LinesAutoUnit', 'PluginLine', 'PluginScatter',
'LineAnalysisContinuum', 'LineAnalysisContinuumCenter',
'LineAnalysisContinuumLeft', 'LineAnalysisContinuumRight',
- 'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark', 'FootprintOverlay']
+ 'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark', 'FootprintOverlay',
+ 'ApertureMark']
accent_color = "#c75d2c"
@@ -652,6 +653,11 @@ def overlay(self):
return self._overlay
+class ApertureMark(PluginLine):
+ def __init__(self, viewer, **kwargs):
+ super().__init__(viewer, **kwargs)
+
+
class HistogramMark(Lines):
def __init__(self, min_max_value, scales, **kwargs):
# Vertical line in LinearScale
diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py
index 61761de605..152cc879b4 100644
--- a/jdaviz/core/template_mixin.py
+++ b/jdaviz/core/template_mixin.py
@@ -28,9 +28,10 @@
from glue_jupyter.bqplot.image import BqplotImageView
from glue_jupyter.registries import viewer_registry
from glue_jupyter.widgets.linked_dropdown import get_choices as _get_glue_choices
+from regions import PixelRegion
from specutils import Spectrum1D
from specutils.manipulation import extract_region
-from traitlets import Any, Bool, HasTraits, List, Unicode, observe
+from traitlets import Any, Bool, Float, HasTraits, List, Unicode, observe
from ipywidgets import widget_serialization
from ipypopout import PopoutButton
@@ -41,13 +42,13 @@
from jdaviz.core.events import (AddDataMessage, RemoveDataMessage,
ViewerAddedMessage, ViewerRemovedMessage,
ViewerRenamedMessage, SnackbarMessage,
- AddDataToViewerMessage)
+ AddDataToViewerMessage, ChangeRefDataMessage)
from jdaviz.core.marks import (LineAnalysisContinuum,
LineAnalysisContinuumCenter,
LineAnalysisContinuumLeft,
LineAnalysisContinuumRight,
- ShadowLine)
-from jdaviz.core.region_translators import _get_region_from_spatial_subset
+ ShadowLine, ApertureMark)
+from jdaviz.core.region_translators import regions2roi, _get_region_from_spatial_subset
from jdaviz.core.user_api import UserApiWrapper, PluginUserApi
from jdaviz.style_registry import PopoutStyleWrapper
from jdaviz.utils import get_subset_type
@@ -59,7 +60,9 @@
'BasePluginComponent',
'SelectPluginComponent', 'UnitSelectPluginComponent', 'EditableSelectPluginComponent',
'PluginSubcomponent',
- 'SubsetSelect', 'SpatialSubsetSelectMixin', 'SpectralSubsetSelectMixin',
+ 'SubsetSelect',
+ 'SpatialSubsetSelectMixin', 'SpectralSubsetSelectMixin',
+ 'ApertureSubsetSelect', 'ApertureSubsetSelectMixin',
'DatasetSpectralSubsetValidMixin', 'SpectralContinuumMixin',
'ViewerSelect', 'ViewerSelectMixin',
'LayerSelect', 'LayerSelectMixin',
@@ -519,6 +522,12 @@ def _clear_cache(self, *attrs):
if attr in self.__dict__:
del self.__dict__[attr]
+ def add_traitlets(self, **traitlets):
+ for k, v in traitlets.items():
+ if v is None:
+ continue
+ self._plugin_traitlets[k] = v
+
def add_observe(self, traitlet_name, handler, first=False):
self._plugin.observe(handler, traitlet_name)
if first:
@@ -1573,9 +1582,7 @@ class SubsetSelect(SelectPluginComponent):
* :attr:`selected_obj`
* :attr:`selected_subset_state`
* :meth:`selected_min_max`
- """
- """
Traitlets (in the object, custom traitlets in the plugin):
* ``items`` (list of dicts with keys: label, color, type)
@@ -1613,7 +1620,6 @@ class SubsetSelect(SelectPluginComponent):
/>
"""
-
def __init__(self, plugin, items, selected, multiselect=None, selected_has_subregions=None,
dataset=None, viewers=None, default_text=None, manual_options=[], filters=[],
default_mode='default_text'):
@@ -1634,7 +1640,7 @@ def __init__(self, plugin, items, selected, multiselect=None, selected_has_subre
the name of the dataset traitlet defined in ``plugin``, to be used for accessing how
the subset is applied to the data (masks, etc), optional
viewers : list
- the reference names or ids of the viewer to extract the subregion. If not provided o
+ the reference names or ids of the viewer to extract the subregion. If not provided or
None, will loop through all references.
default_text : str or None
the text to show for no selection. If not provided or None, no entry will be provided
@@ -1683,7 +1689,8 @@ def _selected_changed(self, event):
self._update_has_subregions()
def _on_dataset_selected_changed(self, event):
- self._clear_cache('selected_subset_mask', 'selected_spatial_region')
+ self._clear_cache('selected_subset_mask',
+ 'selected_spatial_region')
def _subset_to_dict(self, subset):
# find layer artist in default spectrum-viewer
@@ -1825,6 +1832,8 @@ def _get_spatial_region(self, dataset, subset=None):
subset_state = self.selected_subset_state
else:
subset_state = self._get_subset_state(subset)
+ if subset_state is None:
+ return None
region = _get_region_from_spatial_subset(self.plugin, subset_state)
region.meta['label'] = subset
return region
@@ -1834,13 +1843,11 @@ def selected_spatial_region(self):
if not getattr(self, 'dataset', None): # pragma: no cover
raise ValueError("Retrieving subset mask requires associated dataset")
if self.is_multiselect and self.dataset.is_multiselect: # pragma: no cover
- # technically this could work if either has length of one, but would require extra
- # logic
raise NotImplementedError("cannot access selected_spatial_region for multiple subsets and multiple datasets") # noqa
types = self.selected_item.get('type')
if not isinstance(types, list):
types = [types]
- if np.any([type != 'spatial' for type in types]):
+ if np.any([type not in ('spatial', None) for type in types]):
raise TypeError("This action is only supported on spatial-type subsets")
if self.is_multiselect:
return [self._get_spatial_region(dataset=self.dataset.selected, subset=subset) for subset in self.selected] # noqa
@@ -1942,6 +1949,250 @@ def __init__(self, *args, **kwargs):
filters=['is_spatial'])
+class ApertureSubsetSelect(SubsetSelect):
+ """
+ Plugin select for aperture subsets, with support for single or multi-selection, as well as
+ live-preview rendered in the viewers.
+
+ Useful API methods/attributes:
+
+ * :meth:`~SelectPluginComponent.choices`
+ * ``selected``
+ * :meth:`~SelectPluginComponent.is_multiselect`
+ * :meth:`~SelectPluginComponent.select_default`
+ * :meth:`~SelectPluginComponent.select_all` (only if ``is_multiselect``)
+ * :meth:`~SelectPluginComponent.select_none` (only if ``is_multiselect``)
+ * :attr:`~SubsetSelect.selected_obj`
+ * :attr:`~SubsetSelect.selected_subset_state`
+ * :meth:`~SubsetSelect.selected_min_max`
+ * :meth:`marks`
+ * :meth:`image_viewers`
+
+ Traitlets (in the object, custom traitlets in the plugin):
+
+ * ``items`` (list of dicts with keys: label, color, type)
+ * ``selected`` (string)
+
+ Properties (in the object only):
+
+ * ``labels`` (list of labels corresponding to items)
+ * ``selected_item`` (dictionary in ``items`` coresponding to ``selected``, cached)
+ * ``selected_obj`` (subset object corresponding to ``selected``, cached)
+ * ``marks`` (list of marks added to image viewers in the app to preview the apertures)
+
+ Methods (in the object only):
+
+ * ``selected_min_max(cube)`` (quantity, only applicable for spectral subsets)
+
+ To use in a plugin:
+
+ * create (empty) traitlets in the plugin
+ * register with all the automatic logic in the plugin's init by passing the string names
+ of the respective traitlets.
+ * use component in plugin template (see below)
+ * refer to properties above based on the interally stored reference to the
+ instantiated object of this component
+ * observe the traitlets created and defined in the plugin, as necessary
+
+ Example template (label and hint are optional)::
+
+
+
+ """
+ def __init__(self, plugin, items, selected, scale_factor, multiselect=None,
+ dataset=None, viewers=None):
+ """
+ Parameters
+ ----------
+ plugin
+ the parent plugin object
+ items : str
+ the name of the items traitlet defined in ``plugin``
+ selected : str
+ the name of the selected traitlet defined in ``plugin``
+ scale_factor : str
+ the name of the traitlet defining the radius factor for the drawn aperture
+ multiselect : str
+ the name of the traitlet defining whether the dropdown should accept multiple selections
+ dataset : str
+ the name of the dataset traitlet defined in ``plugin``, to be used for accessing how
+ the subset is applied to the data (masks, etc), optional
+ viewers : list
+ the reference names or ids of the viewer to extract the subregion. If not provided or
+ None, will loop through all references.
+ """
+ # NOTE: is_not_composite is assumed in _get_mark_coords
+ super().__init__(plugin,
+ items=items,
+ selected=selected,
+ multiselect=multiselect,
+ filters=['is_spatial', 'is_not_composite', 'is_not_annulus'],
+ dataset=dataset,
+ viewers=viewers,
+ default_text=None)
+
+ self.add_traitlets(scale_factor=scale_factor)
+
+ self.add_observe('is_active', self._plugin_active_changed)
+ self.add_observe(selected, self._update_mark_coords)
+ self.add_observe(scale_factor, self._update_mark_coords)
+ # add marks to any new viewers
+ self.hub.subscribe(self, ViewerAddedMessage, handler=self._update_mark_coords)
+ # update coordinates when reference data is changed
+ # NOTE: when link type is changed, all subsets are required to be dropped
+ self.hub.subscribe(self, ChangeRefDataMessage, handler=self._update_mark_coords)
+
+ def _update_subset(self, *args, **kwargs):
+ # update coordinates when subset is modified (with subset tools plugin or drag event)
+ super()._update_subset(*args, **kwargs)
+ self._update_mark_coords()
+
+ def _on_dataset_selected_changed(self, event):
+ super()._on_dataset_selected_changed(event)
+ self._update_mark_coords()
+
+ def _plugin_active_changed(self, *args):
+ for mark in self.marks:
+ mark.visible = self.plugin.is_active
+
+ @property
+ def image_viewers(self):
+ return [viewer for viewer in self.app._viewer_store.values()
+ if isinstance(viewer, BqplotImageView)]
+
+ @property
+ def marks(self):
+ # NOTE: this will require additional logic if we want to support multiple independent
+ # ApertureSubsetSelect instances per-plugin
+ all_aperture_marks = []
+ for viewer in self.image_viewers:
+ # search for existing mark
+ matches = [mark for mark in viewer.figure.marks
+ if isinstance(mark, ApertureMark)]
+ if len(matches):
+ all_aperture_marks += matches
+ continue
+
+ x_coords, y_coords = self._get_mark_coords(viewer)
+
+ mark = ApertureMark(
+ viewer,
+ x=x_coords,
+ y=y_coords,
+ colors=['#c75109'],
+ fill_opacities=[0.0],
+ visible=self.plugin.is_active)
+ all_aperture_marks.append(mark)
+ viewer.figure.marks = viewer.figure.marks + [mark]
+ return all_aperture_marks
+
+ def _get_mark_coords(self, viewer):
+ if not len(self.selected) or not len(self.dataset.selected):
+ return [], []
+ if self.selected in self._manual_options:
+ return [], []
+
+ if getattr(self, 'multiselect', False):
+ # assume first dataset (for retrieving the region object)
+ # but iterate over all subsets
+ spatial_regions = [self._get_spatial_region(dataset=self.dataset.selected[0], subset=subset) # noqa
+ for subset in self.selected if subset != self._manual_options]
+ else:
+ # use cached version
+ spatial_regions = [self.selected_spatial_region]
+
+ x_coords, y_coords = np.array([]), np.array([])
+ for spatial_region in spatial_regions:
+ if spatial_region is None:
+ continue
+
+ if isinstance(spatial_region, PixelRegion):
+ pixel_region = spatial_region
+ else:
+ wcs = getattr(viewer.state.reference_data, 'coords', None)
+ if wcs is None:
+ return [], []
+ pixel_region = spatial_region.to_pixel(wcs)
+ roi = regions2roi(pixel_region)
+
+ # NOTE: this assumes that we'll apply the same radius factor to all subsets (all will
+ # be defined at the same slice for cones in cubes)
+ if hasattr(roi, 'radius'):
+ roi.radius *= self.scale_factor
+ elif hasattr(roi, 'radius_x'):
+ roi.radius_x *= self.scale_factor
+ roi.radius_y *= self.scale_factor
+ elif hasattr(roi, 'center'):
+ center = roi.center()
+ half_width = abs(roi.xmax - roi.xmin) * 0.5 * self.scale_factor
+ half_height = abs(roi.ymax - roi.ymin) * 0.5 * self.scale_factor
+ roi.xmin = center[0] - half_width
+ roi.xmax = center[0] + half_width
+ roi.ymin = center[1] - half_height
+ roi.ymax = center[1] + half_height
+ else: # pragma: no cover
+ raise NotImplementedError
+
+ x, y = roi.to_polygon()
+
+ # concatenate with nan between to avoid line connecting separate subsets
+ x_coords = np.concatenate((x_coords, np.array([np.nan]), x))
+ y_coords = np.concatenate((y_coords, np.array([np.nan]), y))
+
+ return x_coords, y_coords
+
+ def _update_mark_coords(self, *args):
+ for viewer in self.image_viewers:
+ x_coords, y_coords = self._get_mark_coords(viewer)
+ for mark in self.marks:
+ if mark.viewer != viewer:
+ continue
+ mark.x, mark.y = x_coords, y_coords
+
+
+class ApertureSubsetSelectMixin(VuetifyTemplate, HubListener):
+ """
+ Applies the ApertureSubsetSelect component as a mixin in the base plugin. This
+ automatically adds traitlets as well as new properties to the plugin with minimal
+ extra code. For multiple instances or custom traitlet names/defaults, use the
+ component instead.
+
+ To use in a plugin:
+
+ * add ``ApertureSubsetSelectMixin`` as a mixin to the class BEFORE ``DatasetSelectMixin``
+ * use the traitlets available from the plugin or properties/methods available from
+ ``plugin.aperture``.
+
+ Example template (label and hint are optional)::
+
+
+
+ """
+ aperture_items = List([]).tag(sync=True)
+ aperture_selected = Any('').tag(sync=True)
+ aperture_scale_factor = Float(1).tag(sync=True)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.aperture = ApertureSubsetSelect(self,
+ 'aperture_items',
+ 'aperture_selected',
+ 'aperture_scale_factor',
+ dataset='dataset' if hasattr(self, 'dataset') else None, # noqa
+ multiselect='multiselect' if hasattr(self, 'multiselect') else None) # noqa
+
+
class DatasetSpectralSubsetValidMixin(VuetifyTemplate, HubListener):
"""
Adds a traitlet tracking whether self.dataset and self.spectral_subset