diff --git a/CHANGES.rst b/CHANGES.rst
index cf107b003a..09f15959b1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,6 +32,9 @@ Cubeviz
- Live previews in spectral extraction plugin. [#2733]
+- Slice plugin is refactored to rely on the spectral value instead of the slice index. This removes
+ both the slider and slice-index input. [#2715]
+
Imviz
^^^^^
@@ -67,7 +70,8 @@ Cubeviz
be removed in a future release. [#2664]
- Slice plugin's ``wavelength``, ``wavelength_unit``, and ``show_wavelength`` are deprecated in favor
- of ``value``, ``value_unit``, and ``show_value``, respectively. [#2706]
+ of ``value``, ``value_unit``, and ``show_value``, respectively. ``slice`` is also deprecated
+ and should be replaced with accessing/setting ``value`` directly. [#2706, #2715]
Imviz
^^^^^
diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst
index 8b502c2938..ee5e37de1e 100644
--- a/docs/cubeviz/plugins.rst
+++ b/docs/cubeviz/plugins.rst
@@ -74,10 +74,10 @@ The slice plugin provides the ability to select the slice
of the cube currently visible in the image viewers, with the
corresponding wavelength highlighted in the spectrum viewer.
-The slider can be grabbed to scrub through the cube. To choose
-a specific slice, enter a slice number (integer) or an approximate
-wavelength (in which case the nearest slice will be selected and
-the wavelength entry will update to the exact value of that slice).
+To choose a specific slice, enter an approximate wavelength (in which case the nearest slice will
+be selected and the wavelength entry will "span" to the exact value of that slice). The snapping
+behavior can be disabled in the plugin settings to allow for smooth scrubbing, in which case the
+closest slice will still be displayed in the cube viewer.
The spectrum viewer also contains a tool to allow clicking and
dragging in the spectrum plot to choose the currently selected slice.
@@ -89,6 +89,7 @@ For your convenience, there are also player-style buttons with
the following functionality:
* Jump to first
+* Previous slice
* Play/Pause
* Next slice
* Jump to last
diff --git a/jdaviz/components/toolbar_nested.py b/jdaviz/components/toolbar_nested.py
index 47413eb08e..56f7843777 100644
--- a/jdaviz/components/toolbar_nested.py
+++ b/jdaviz/components/toolbar_nested.py
@@ -53,17 +53,17 @@ def __init__(self, viewer, tools_nested, default_tool_priority=[]):
primary=i == 0,
visible=True)
- # handle logic for tool visibilities (which will also handle setting the primary
- # to something other than the first entry, if necessary)
- self._update_tool_visibilities()
-
# default_tool_priority allows falling back on an existing tool
# if its the primary tool. If no items in default_tool_priority
# are currently "primary", then either no tool will be selected
# or will fallback on BasicJupyterToolbar's handling of
# viewer._default_mouse_mode_cls (which will not show that tool as active).
self.default_tool_priority = default_tool_priority
- self._handle_default_tool()
+
+ # handle logic for tool visibilities (which will also handle setting the primary
+ # to something other than the first entry, if necessary)
+ # NOTE: this will also call _handle_default_tool
+ self._update_tool_visibilities()
# toolbars in the main app viewers need to respond to the data-collection, etc,
# but those in plugins do not
@@ -132,6 +132,7 @@ def _update_tool_visibilities(self):
self.send_state("tools_data")
if needs_deactivate_active:
self.active_tool_id = None
+ self._handle_default_tool()
def _handle_default_tool(self):
# default to the first item in the default_tool_priority list that is currently
diff --git a/jdaviz/configs/cubeviz/helper.py b/jdaviz/configs/cubeviz/helper.py
index f5a5ad2e10..d0f5f2c89e 100644
--- a/jdaviz/configs/cubeviz/helper.py
+++ b/jdaviz/configs/cubeviz/helper.py
@@ -1,6 +1,7 @@
import numpy as np
from astropy.io import fits
from astropy.io import registry as io_registry
+from astropy.utils.decorators import deprecated
from glue.core import BaseData
from specutils import Spectrum1D
from specutils.io.registers import _astropy_has_priorities
@@ -14,6 +15,10 @@
__all__ = ['Cubeviz']
+_spectral_axis_names = ["Wave", "Wavelength", "Freq", "Frequency",
+ "Wavenumber", "Velocity", "Energy"]
+
+
class Cubeviz(ImageConfigHelper, LineListMixin):
"""Cubeviz Helper class"""
_default_configuration = 'cubeviz'
@@ -36,8 +41,7 @@ def _set_spectrum_x_axis(self, msg):
return
ref_data = viewer.state.reference_data
if ref_data and ref_data.ndim == 3:
- for att_name in ["Wave", "Wavelength", "Freq", "Frequency",
- "Wavenumber", "Velocity", "Energy"]:
+ for att_name in _spectral_axis_names:
if att_name in ref_data.component_ids():
if viewer.state.x_att != ref_data.id[att_name]:
viewer.state.x_att = ref_data.id[att_name]
@@ -76,6 +80,7 @@ def load_data(self, data, data_label=None, override_cube_limit=False, **kwargs):
super().load_data(data, parser_reference="cubeviz-data-parser", **kwargs)
+ @deprecated(since="3.9", alternative="select_wavelength")
def select_slice(self, slice):
"""
Select a slice by index.
@@ -89,8 +94,7 @@ def select_slice(self, slice):
raise TypeError("slice must be an integer")
if slice < 0:
raise ValueError("slice must be positive")
- msg = SliceSelectSliceMessage(slice=slice, sender=self)
- self.app.hub.broadcast(msg)
+ self.plugins['Slice'].slice = slice
def select_wavelength(self, wavelength):
"""
@@ -100,7 +104,7 @@ def select_wavelength(self, wavelength):
----------
wavelength : float
Wavelength to select in units of the x-axis of the spectrum. The nearest slice will
- be selected.
+ be selected if "snap to slice" is enabled in the slice plugin.
"""
if not isinstance(wavelength, (int, float)):
raise TypeError("wavelength must be a float or int")
diff --git a/jdaviz/configs/cubeviz/plugins/slice/slice.py b/jdaviz/configs/cubeviz/plugins/slice/slice.py
index 6e67584ac8..0f137f5289 100644
--- a/jdaviz/configs/cubeviz/plugins/slice/slice.py
+++ b/jdaviz/configs/cubeviz/plugins/slice/slice.py
@@ -3,21 +3,24 @@
import warnings
import numpy as np
-import astropy.units as u
+from astropy import units as u
from astropy.units import UnitsWarning
from astropy.utils.decorators import deprecated
-from glue_jupyter.bqplot.image import BqplotImageView
-from glue_jupyter.bqplot.profile import BqplotProfileView
-from traitlets import Any, Bool, Int, Unicode, observe
-from specutils.spectra.spectrum1d import Spectrum1D
+from traitlets import Bool, Int, Unicode, observe
+from jdaviz.configs.cubeviz.plugins.viewers import (WithSliceIndicator, WithSliceSelection,
+ CubevizImageView)
+from jdaviz.configs.cubeviz.helper import _spectral_axis_names
+from jdaviz.core.custom_traitlets import FloatHandleEmpty
from jdaviz.core.events import (AddDataMessage, SliceToolStateMessage,
SliceSelectSliceMessage, SliceValueUpdatedMessage,
+ NewViewerMessage, ViewerAddedMessage, ViewerRemovedMessage,
GlobalDisplayUnitChanged)
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin
from jdaviz.core.user_api import PluginUserApi
+
__all__ = ['Slice']
@@ -32,26 +35,30 @@ class Slice(PluginTemplateMixin):
* :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`
- * ``slice``
- Current slice number.
* ``value``
Value (wavelength or frequency) of the current slice. When setting this directly, it will
- update automatically to the value corresponding to the nearest slice.
+ update automatically to the value corresponding to the nearest slice, if ``snap_to_slice`` is
+ enabled.
* ``show_indicator``
Whether to show indicator in spectral viewer when slice tool is inactive.
* ``show_value``
Whether to show slice value in label to right of indicator.
"""
+ _cube_viewer_cls = CubevizImageView
+ _cube_viewer_default_label = 'flux-viewer'
+ cube_viewer_exists = Bool(True).tag(sync=True)
+
+ allow_disable_snapping = Bool(False).tag(sync=True) # noqa internal use to show and allow disabling snap-to-slice
+
template_file = __file__, "slice.vue"
- slice = Any(0).tag(sync=True)
- value = Any(-1).tag(sync=True)
- value_label = Unicode("Wavelength").tag(sync=True)
- value_unit = Any("").tag(sync=True)
+ value = FloatHandleEmpty().tag(sync=True)
+ value_label = Unicode("Value").tag(sync=True)
+ value_unit = Unicode("").tag(sync=True)
+ value_editing = Bool(False).tag(sync=True) # whether the value input is actively being edited
- min_slice = Int(0).tag(sync=True)
- max_slice = Int(100).tag(sync=True)
- wait = Int(200).tag(sync=True)
+ slider_throttle = Int(200).tag(sync=True) # milliseconds
+ snap_to_slice = Bool(True).tag(sync=True)
show_indicator = Bool(True).tag(sync=True)
show_value = Bool(True).tag(sync=True)
@@ -61,33 +68,80 @@ class Slice(PluginTemplateMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self._watched_viewers = []
- self._indicator_viewers = []
- self._x_all = None
+ self._indicator_initialized = False
self._player = None
- # initialize watching existing viewers WITH data (if initializing the plugin after data
- # already exists - otherwise the AddDataMessage will handle watching image viewers once
- # data is available)
- for id, viewer in self.app._viewer_store.items():
- if isinstance(viewer, BqplotProfileView) or len(viewer.data()):
- self._watch_viewer(viewer, True)
-
# Subscribe to requests from the helper to change the slice across all viewers
self.session.hub.subscribe(self, SliceSelectSliceMessage,
handler=self._on_select_slice_message)
- # Listen for add data events. **Note** this should only be used in
- # cases where there is a specific type of data expected and arbitrary
- # viewers are not expected to be created. That is, the expected data
- # in _all_ viewers should be uniform.
- self.session.hub.subscribe(self, AddDataMessage,
- handler=self._on_data_added)
+ # Listen for new viewers/data.
+ self.session.hub.subscribe(self, ViewerAddedMessage,
+ handler=self._on_viewer_added)
+ self.session.hub.subscribe(self, ViewerRemovedMessage,
+ handler=self._on_viewer_removed)
+ self.hub.subscribe(self, AddDataMessage,
+ handler=self._on_add_data)
+
+ # connect any pre-existing viewers
+ for viewer in self.app._viewer_store.values():
+ self._connect_viewer(viewer)
+
+ # initialize if cube viewer exists
+ self._check_if_cube_viewer_exists()
# update internal value (wavelength/frequency) when x display unit is changed
# so that the current slice number is preserved
self.session.hub.subscribe(self, GlobalDisplayUnitChanged,
handler=self._on_global_display_unit_changed)
+ self._initialize_location()
+
+ def _initialize_location(self, *args):
+ # initialize value_unit (this has to wait until data is loaded to an existing
+ # slice_indicator_viewer, so we'll keep trying until it is set - after that, changes
+ # will be handled by a change to global display units)
+ if not self.value_unit:
+ for viewer in self.slice_indicator_viewers:
+ # ignore while x_display_unit is unset or still degrees (before data transpose)
+ # if we ever need the slice axis to be degrees, this will need to be loosened
+ if getattr(viewer.state, 'x_display_unit', None) not in (None, 'deg'):
+ self.value_unit = viewer.state.x_display_unit
+ break
+
+ if self._indicator_initialized:
+ return
+
+ # set initial value (and snap to nearest point, if enabled)
+ # we'll loop over all slice indicator viewers and their layers
+ # and just use the first layer with data. Once initialized, this logic will be
+ # skipped going forward to not change any user selection (so will default to the
+ # middle of the first found layer)
+ for viewer in self.slice_indicator_viewers:
+ if str(viewer.state.x_att) not in self.valid_slice_att_names:
+ # avoid setting value to degs, before x_att is changed to wavelength, for example
+ continue
+ slice_values = viewer.slice_values
+ if len(slice_values):
+ self.value = slice_values[int(len(slice_values)/2)]
+ self._indicator_initialized = True
+ return
+
+ @property
+ def slice_axis(self):
+ # global display unit "axis" corresponding to the slice axis
+ return 'spectral'
+
+ @property
+ def valid_slice_att_names(self):
+ return _spectral_axis_names + ['Pixel Axis 2 [x]']
+
+ @property
+ def slice_selection_viewers(self):
+ return [v for v in self.app._viewer_store.values() if isinstance(v, WithSliceSelection)]
+
+ @property
+ def slice_indicator_viewers(self):
+ return [v for v in self.app._viewer_store.values() if isinstance(v, WithSliceIndicator)]
@property
@deprecated(since="3.9", alternative="value")
@@ -105,165 +159,211 @@ def show_wavelength(self):
return self.user_api.show_value
@property
- def user_api(self):
- return PluginUserApi(self, expose=('slice', 'wavelength', 'value',
- 'show_indicator', 'show_wavelength', 'show_value'))
+ @deprecated(since="3.9", alternative="value")
+ def slice(self):
+ # this assumes the first slice_selection_viewer (and layer)
+ return self.slice_selection_viewers[0].slice
+
+ @slice.setter
+ @deprecated(since="3.9", alternative="value")
+ def slice(self, slice):
+ # this assumes the first slice_selection_viewer (and layer)
+ value = self.slice_selection_viewers[0].slice_values[slice]
+ self.value = value
@property
- def slice_indicator(self):
- return self.spectrum_viewer.slice_indicator
-
- def _watch_viewer(self, viewer, watch=True):
- if isinstance(viewer, BqplotImageView):
- if watch and viewer not in self._watched_viewers:
- self._watched_viewers.append(viewer)
- viewer.state.add_callback('slices',
- self._viewer_slices_changed)
- elif not watch and viewer in self._watched_viewers:
- viewer.state.remove_callback('slices',
- self._viewer_slices_changed)
- self._watched_viewers.remove(viewer)
- elif isinstance(viewer, BqplotProfileView) and watch:
- if self._x_all is None and len(viewer.data()):
- # cache values (wavelengths/freqs) so that value <> slice conversion is efficient
- self._update_data(viewer.data()[0].spectral_axis)
-
- if viewer not in self._indicator_viewers:
- self._indicator_viewers.append(viewer)
- # if the units (or data) change, we need to update internally
- viewer.state.add_callback("reference_data",
- self._update_reference_data)
-
- def _on_data_added(self, msg):
- if isinstance(msg.viewer, BqplotImageView):
- if len(msg.data.shape) == 3:
- self.max_slice = msg.data.shape[-1] - 1 # Same as i_end in Export Plot plugin
- self._watch_viewer(msg.viewer, True)
- self._set_viewer_to_slice(msg.viewer, int(self.slice))
-
- elif isinstance(msg.viewer, BqplotProfileView):
- self._watch_viewer(msg.viewer, True)
-
- def _update_reference_data(self, reference_data):
- if reference_data is None:
- return # pragma: no cover
- self._update_data(reference_data.get_object(cls=Spectrum1D).spectral_axis)
-
- def _update_data(self, x_all):
- self._x_all = x_all.value
-
- if self.value == -1:
- if len(x_all):
- # initialize at middle of cube
- self.slice = int(len(x_all)/2)
- else:
- # leave in the pre-init state and don't update the value/slice
+ def user_api(self):
+ # NOTE: remove slice, wavelength, show_wavelength after deprecation period
+ expose = ['slice', 'wavelength', 'show_wavelength',
+ 'value',
+ 'show_indicator', 'show_value']
+ if self.allow_disable_snapping:
+ expose += ['snap_to_slice']
+ return PluginUserApi(self, expose=expose)
+
+ def _check_if_cube_viewer_exists(self, *args):
+ for viewer in self.app._viewer_store.values():
+ if isinstance(viewer, self._cube_viewer_cls):
+ self.cube_viewer_exists = True
return
-
- # Also update unit when data is updated
- self.value_unit = x_all.unit.to_string()
-
- # force value (wavelength/frequency) to update from the current slider slice
- self._on_slider_updated({'new': self.slice})
-
- # update data held inside slice indicator and force reverting to original active status
- self.slice_indicator._update_data(x_all)
-
- def _viewer_slices_changed(self, value):
- # the slices attribute on the viewer state was changed,
- # so we'll update the slider to match which will trigger
- # the slider observer (_on_slider_updated) and sync across
- # any other applicable viewers
- if len(value) == 3:
- self.slice = float(value[-1])
+ self.cube_viewer_exists = False
+
+ def vue_create_cube_viewer(self, *args):
+ self.app._on_new_viewer(NewViewerMessage(self._cube_viewer_cls, data=None, sender=self.app),
+ vid=self._cube_viewer_default_label,
+ name=self._cube_viewer_default_label)
+
+ dc = self.app.data_collection
+ for data in dc:
+ if data.ndim == 3:
+ # only load the first cube-like data
+ self.app.set_data_visibility(self._cube_viewer_default_label, data.label, True)
+ break
+
+ def _connect_viewer(self, viewer):
+ if isinstance(viewer, WithSliceIndicator):
+ # NOTE: on first call, this will initialize the indicator itself
+ viewer._set_slice_indicator_value(self.value)
+ # in the case where x_att is changed after the viewer is added or data is loaded, we
+ # may still need to initialize the location to a valid value (with a valid x_att)
+ viewer.state.add_callback('x_att', self._initialize_location)
+
+ def _on_viewer_added(self, msg):
+ viewer = self.app.get_viewer(msg.viewer_id)
+ self._connect_viewer(viewer)
+ self._check_if_cube_viewer_exists()
+
+ def _on_viewer_removed(self, msg):
+ self._check_if_cube_viewer_exists()
+
+ def _on_add_data(self, msg):
+ self._initialize_location()
+ if isinstance(msg.viewer, WithSliceSelection):
+ # instead of just setting viewer.slice_value, we'll make sure the "snapping" logic
+ # is updated (if enabled)
+ self._on_value_updated({'new': self.value})
def _on_select_slice_message(self, msg):
- # NOTE: by setting the slice index, the observer (_on_slider_updated)
- # will sync across all viewers and update the value (wavelength/frequency)
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=UnitsWarning)
- if msg.slice is not None:
- self.slice = msg.slice
- elif msg.value is not None:
- self.value = msg.value
-
- @property
- def slice_axis(self):
- return 'spectral'
+ self.value = msg.value
def _on_global_display_unit_changed(self, msg):
if msg.axis != self.slice_axis:
return
- prev_unit = self.value_unit
- # original unit during init can be blank or deg (before axis is set correctly)
- if self._x_all is None or prev_unit in ('deg', ''):
+ if not self.value_unit:
+ self.value_unit = str(msg.unit)
return
- self._update_data((self._x_all * u.Unit(prev_unit)).to(msg.unit, u.spectral()))
+ prev_unit = u.Unit(self.value_unit)
+ self.value_unit = str(msg.unit)
+ self.value = (self.value * prev_unit).to_value(msg.unit)
- @observe('value')
- def _on_value_updated(self, event):
- # convert to float (JS handles stripping any invalid characters)
- try:
- value = float(event.get('new'))
- except ValueError:
- # do not accept changes, we'll revert via the slider
- # since this @change event doesn't have access to
- # the old value, and self.value already updated
- # via the v-model
- self._on_slider_updated({'new': self.slice})
- return
-
- # NOTE: by setting the index, this should recursively update the
- # value (wavelength/frequency) to the nearest applicable value in _on_slider_updated
- self.slice = int(np.argmin(abs(value - self._x_all)))
+ @property
+ def valid_selection_values(self):
+ # all available slice values from cubes (unsorted)
+ viewers = self.slice_selection_viewers
+ if not len(viewers):
+ return np.array([])
+ return np.unique(np.concatenate([viewer.slice_values for viewer in viewers]))
- @observe('show_indicator', 'show_value')
- def _on_setting_changed(self, event):
- msg = SliceToolStateMessage({event['name']: event['new']}, sender=self)
- self.session.hub.broadcast(msg)
+ @property
+ def valid_selection_values_sorted(self):
+ # all available slice values from cubes (sorted)
+ return np.sort(self.valid_selection_values)
- def _set_viewer_to_slice(self, viewer, value):
- viewer.state.slices = (0, 0, value)
+ @property
+ def valid_indicator_values(self):
+ # all x-values in indicator viewers (unsorted)
+ viewers = self.slice_indicator_viewers
+ if not len(viewers):
+ return np.array([])
+ return np.unique(np.concatenate([viewer.slice_values for viewer in viewers]))
- @observe('slice')
- def _on_slider_updated(self, event):
- if self._x_all is None:
- return
+ @property
+ def valid_indicator_values_sorted(self):
+ return np.sort(self.valid_indicator_values)
- value = int(event.get('new', int(len(self._x_all)/2))) % (int(self.max_slice) + 1)
+ @property
+ def valid_values(self):
+ return self.valid_selection_values if self.cube_viewer_exists else self.valid_indicator_values # noqa
- self.value = self._x_all[value]
+ @property
+ def valid_values_sorted(self):
+ return self.valid_selection_values_sorted if self.cube_viewer_exists else self.valid_indicator_values_sorted # noqa
- for viewer in self._watched_viewers:
- self._set_viewer_to_slice(viewer, value)
- for viewer in self._indicator_viewers:
- if hasattr(viewer, 'slice_indicator'):
- viewer.slice_indicator.slice = value
+ @observe('value')
+ def _on_value_updated(self, event):
+ # convert to float (JS handles stripping any invalid characters)
+ if not isinstance(event.get('new'), float):
+ try:
+ self.value = float(event.get('new'))
+ except ValueError:
+ return
+ return
- self.hub.broadcast(SliceValueUpdatedMessage(slice=value,
- value=self.value,
+ if self.snap_to_slice and not self.value_editing:
+ valid_values = self.valid_selection_values
+ if len(valid_values):
+ closest_ind = np.argmin(abs(valid_values - self.value))
+ closest_value = valid_values[closest_ind]
+ if self.value != closest_value:
+ # cast to float in case closest_value is an integer (which would otherwise
+ # raise an error with setting to the float traitlet)
+ self.value = float(closest_value)
+ # will trigger another call to this method
+ return
+
+ for viewer in self.slice_indicator_viewers:
+ viewer._set_slice_indicator_value(self.value)
+ for viewer in self.slice_selection_viewers:
+ viewer.slice_value = self.value
+
+ self.hub.broadcast(SliceValueUpdatedMessage(value=self.value,
value_unit=self.value_unit,
sender=self))
+ @observe('snap_to_slice', 'value_editing')
+ def _on_snap_to_slice_changed(self, event):
+ if self.snap_to_slice and not self.value_editing:
+ self._on_value_updated({'new': self.value})
+
+ @observe('show_indicator', 'show_value')
+ def _on_setting_changed(self, event):
+ msg = SliceToolStateMessage({event['name']: event['new']}, viewer=None, sender=self)
+ self.session.hub.broadcast(msg)
+
def vue_goto_first(self, *args):
if self.is_playing:
return
- self._on_slider_updated({'new': self.min_slice})
+ self.value = np.nanmin(self.valid_values)
def vue_goto_last(self, *args):
if self.is_playing:
return
- self._on_slider_updated({'new': self.max_slice})
+ self.value = np.nanmax(self.valid_values)
+
+ def vue_play_prev(self, *args):
+ if self.is_playing:
+ return
+ valid_values = self.valid_values_sorted
+ if not len(valid_values):
+ return
+ current_ind = np.argmin(abs(valid_values - self.value))
+ if current_ind == 0:
+ # wrap
+ self.value = valid_values[len(valid_values) - 1]
+ else:
+ self.value = valid_values[current_ind - 1]
def vue_play_next(self, *args):
if self.is_playing:
return
- self._on_slider_updated({'new': self.slice + 1})
+ valid_values = self.valid_values_sorted
+ if not len(valid_values):
+ return
+ current_ind = np.argmin(abs(valid_values - self.value))
+ if len(valid_values) <= current_ind + 1:
+ # wrap
+ self.value = valid_values[0]
+ else:
+ self.value = valid_values[current_ind + 1]
def _player_worker(self):
ts = float(self.play_interval) * 1e-3 # ms to s
+ valid_values = self.valid_values_sorted
+ if not len(valid_values):
+ self.is_playing = False
+ return
while self.is_playing:
- self._on_slider_updated({'new': self.slice + 1})
+ # recompute current_ind in case user has moved slider
+ # could optimize this by only recomputing after a select slice message
+ # (will only make a difference if argmin becomes approaches play_interval)
+ current_ind = np.argmin(abs(valid_values - self.value))
+ if len(valid_values) <= current_ind + 1:
+ # wrap
+ self.value = valid_values[0]
+ else:
+ self.value = valid_values[current_ind + 1]
time.sleep(ts)
def vue_play_start_stop(self, *args):
@@ -275,7 +375,9 @@ def vue_play_start_stop(self, *args):
self.is_playing = False
return
- if self._x_all is None:
+ if len(self.slice_indicator_viewers) == 0 and len(self.slice_selection_viewers) == 0:
+ return
+ if not len(self.valid_values_sorted):
return
# Start
diff --git a/jdaviz/configs/cubeviz/plugins/slice/slice.vue b/jdaviz/configs/cubeviz/plugins/slice/slice.vue
index cc26975da8..309a4ba1c6 100644
--- a/jdaviz/configs/cubeviz/plugins/slice/slice.vue
+++ b/jdaviz/configs/cubeviz/plugins/slice/slice.vue
@@ -11,6 +11,14 @@
Indicator Settings
+
+
+
+
-
-
+
+
+ Show Cube Viewer
+
-
-
-
- value_editing = true"
+ @blur="(e) => value_editing = false"
class="mt-0 pt-0"
:label="value_label"
- :hint="value_label+' corresponding to slice'"
+ :hint="value_label+' corresponding to slice.'+(snap_to_slice && value_editing ? ' Indicator will snap to slice when clicking or tabbing away from input.' : '')"
:suffix="value_unit"
>
@@ -74,6 +69,14 @@
Jump to first
+
+
+
+ exposure_minus_1
+
+
+ Previous
+
@@ -88,7 +91,7 @@
exposure_plus_1
- Next slice
+ Next
@@ -108,7 +111,7 @@
created() {
this.throttledSetValue = _.throttle(
(v) => { this.slice = v; },
- this.wait);
+ this.slider_throttle);
},
}
diff --git a/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py b/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
index 8f133ac7e9..3c77484ade 100644
--- a/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
+++ b/jdaviz/configs/cubeviz/plugins/slice/tests/test_slice.py
@@ -1,7 +1,6 @@
import warnings
import pytest
-import numpy as np
from jdaviz.configs.cubeviz.plugins.slice.slice import Slice
@@ -10,50 +9,37 @@ def test_slice(cubeviz_helper, spectrum1d_cube):
app = cubeviz_helper.app
sl = Slice(app=app)
- # Make sure nothing crashes if plugin used without data
+ # No data yet
+ assert len(sl.slice_selection_viewers) == 2 # flux-viewer, uncert-viewer
+ assert len(sl.slice_indicator_viewers) == 1 # spectrum-viewer
+ assert len(sl.valid_indicator_values_sorted) == 0
+ assert len(sl.valid_selection_values_sorted) == 0
+
+ # Make sure nothing crashes if plugin used without data]
sl.vue_play_next()
- assert sl.slice == 0
sl.vue_play_start_stop()
assert not sl.is_playing
- assert not sl._player
- app.add_data(spectrum1d_cube, 'test')
- app.add_data_to_viewer("spectrum-viewer", "test")
- app.add_data_to_viewer("flux-viewer", "test")
- app.add_data_to_viewer("uncert-viewer", "test")
+ cubeviz_helper.load_data(spectrum1d_cube, data_label='test')
+ app.add_data_to_viewer("spectrum-viewer", "test[FLUX]")
+ app.add_data_to_viewer("flux-viewer", "test[FLUX]")
+ app.add_data_to_viewer("uncert-viewer", "test[FLUX]")
# sample cube only has 2 slices with wavelengths [4.62280007e-07 4.62360028e-07] m
- assert sl.slice == 1
+ assert len(sl.valid_indicator_values_sorted) == 2
+ slice_values = sl.valid_selection_values_sorted
+ assert len(slice_values) == 2
+
+ assert sl.value == slice_values[1]
+ assert cubeviz_helper.app.get_viewer("flux-viewer").slice == 1
assert cubeviz_helper.app.get_viewer("flux-viewer").state.slices[-1] == 1
assert cubeviz_helper.app.get_viewer("uncert-viewer").state.slices[-1] == 1
- cubeviz_helper.select_slice(0)
- assert sl.slice == 0
-
- with pytest.raises(
- TypeError,
- match="slice must be an integer"):
- cubeviz_helper.select_slice("blah")
+ cubeviz_helper.select_wavelength(slice_values[0])
+ assert cubeviz_helper.app.get_viewer("flux-viewer").slice == 0
+ assert sl.value == slice_values[0]
- with pytest.raises(
- ValueError,
- match="slice must be positive"):
- cubeviz_helper.select_slice(-5)
-
- cubeviz_helper.select_wavelength(4.62360028e-07)
- assert sl.slice == 1
-
- # from the widget this logic is duplicated (to avoid sending logic through messages)
- sl._on_value_updated({'new': '4.62e-07'})
- assert sl.slice == 0
- assert np.allclose(sl.value, 4.62280007e-07)
-
- # make sure that passing an invalid value from the UI would revert to the previous value
- # JS strips invalid characters, but doesn't ensure its float-compatible
- sl._on_value_updated({'new': '1.2.3'})
- assert sl.slice == 0
-
- assert len(sl._watched_viewers) == 2 # flux-viewer, uncert-viewer
- assert len(sl._indicator_viewers) == 1 # spectrum-viewer
+ cubeviz_helper.select_wavelength(slice_values[1])
+ assert sl.value == slice_values[1]
# test setting a static 2d image to the "watched" flux viewer to make sure it disconnects
mm = app.get_tray_item_from_name('cubeviz-moment-maps')
@@ -62,9 +48,6 @@ def test_slice(cubeviz_helper, spectrum1d_cube):
warnings.filterwarnings('ignore', message=r'.*No observer defined on WCS.*')
mm.vue_calculate_moment()
- assert len(sl._watched_viewers) == 2
- assert len(sl._indicator_viewers) == 1
-
# test in conjunction with as_steps
sv = app.get_viewer('spectrum-viewer')
orig_len = len(sv.native_marks[0].x)
@@ -73,18 +56,18 @@ def test_slice(cubeviz_helper, spectrum1d_cube):
new_len = len(sv.native_marks[0].x)
assert new_len == 2*orig_len
cubeviz_helper.select_wavelength(4.62360028e-07)
- assert sl.slice == 1
+ assert sl.value == slice_values[1]
# Test player buttons API
sl.vue_goto_first()
- assert sl.slice == 0
+ assert sl.value == slice_values[0]
sl.vue_goto_last()
- assert sl.slice == sl.max_slice
+ assert sl.value == slice_values[-1]
sl.vue_play_next() # Should automatically wrap to beginning
- assert sl.slice == 0
+ assert sl.value == slice_values[0]
sl.vue_play_start_stop() # Start
assert sl.is_playing
@@ -125,12 +108,16 @@ def test_init_slice(cubeviz_helper, spectrum1d_cube):
fv = cubeviz_helper.app.get_viewer('flux-viewer')
sl = cubeviz_helper.plugins['Slice']
- assert sl.slice == 1
+ slice_values = sl._obj.valid_selection_values_sorted
+
+ assert sl.value == slice_values[1]
+ assert fv.slice == 1
assert fv.state.slices == (0, 0, 1)
# make sure adding new data doesn't revert slice to 0
mm = cubeviz_helper.plugins['Moment Maps']
mm.calculate_moment(add_data=True)
- assert sl.slice == 1
+ assert sl.value == slice_values[1]
+ assert fv.slice == 1
assert fv.state.slices == (0, 0, 1)
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
index b6dadb5b59..1a6778fe41 100644
--- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
+++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
@@ -202,24 +202,25 @@ def test_aperture_markers(cubeviz_helper, spectrum1d_cube):
assert len(before_x) > 0
# sample cube only has 2 slices with wavelengths [4.62280007e-07 4.62360028e-07] m
- slice_plg.slice = 1
+ slice_values = [4.62280007e-07, 4.62360028e-07]
+ slice_plg.value = slice_values[1]
assert mark.x[1] == before_x[1]
- slice_plg.slice = 0
+ slice_plg.value = slice_values[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
+ slice_plg.value = slice_values[1]
assert mark.x[1] != before_x[1]
extract_plg._obj.vue_goto_reference_wavelength()
- assert slice_plg.slice == 0
+ assert_allclose(slice_plg.value, slice_values[0])
- slice_plg.slice = 1
+ slice_plg.value = slice_values[1]
extract_plg._obj.vue_adopt_slice_as_reference()
extract_plg._obj.vue_goto_reference_wavelength()
- assert slice_plg.slice == 1
+ assert_allclose(slice_plg.value, slice_values[1])
@pytest.mark.parametrize('subset', ['Subset 1', 'Subset 2'])
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py
index 891b069cab..d714d5b4f1 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py
@@ -39,7 +39,7 @@ def test_cubeviz_aperphot_cube_orig_flux(cubeviz_helper, image_cube_hdu_obj_micr
# Move slider and make sure it recomputes for a new slice automatically.
cube_slice_plg = cubeviz_helper.plugins["Slice"]._obj
- cube_slice_plg.slice = 0
+ cube_slice_plg.vue_goto_first()
plg.vue_do_aper_phot()
row = cubeviz_helper.get_aperture_photometry_results()[1]
@@ -181,4 +181,5 @@ def test_cubeviz_aperphot_cube_orig_flux_mjysr(cubeviz_helper, spectrum1d_cube_c
assert_allclose(row["pixarea_tot"], 2.350443053909789e-13 * u.sr)
assert_allclose(row["aperture_sum_mag"], 23.72476627732448 * u.mag)
assert_allclose(row["mean"], 5 * (u.MJy / u.sr))
+ # TODO: check if slice plugin has value_unit set correctly
assert_quantity_allclose(row["slice_wave"], 0.46236 * u.um)
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
index d106ad4ecb..0266dc52fa 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py
@@ -30,7 +30,8 @@ def test_plugin_user_apis(cubeviz_helper):
if plugin_name == 'Spectral Extraction' and attr == 'spatial_subset':
# deprecated, so would raise a deprecation warning
continue
- if plugin_name == 'Slice' and attr in ('wavelength', 'wavelength_unit', 'show_wavelength'): # noqa
+ if plugin_name == 'Slice' and attr in ('slice', 'wavelength',
+ 'wavelength_unit', 'show_wavelength'):
# deprecated, so would raise a deprecation warning
continue
assert hasattr(plugin, attr)
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_regions.py b/jdaviz/configs/cubeviz/plugins/tests/test_regions.py
index dbb5364e47..7949347d25 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_regions.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_regions.py
@@ -16,8 +16,8 @@
class TestLoadRegions(BaseRegionHandler):
@pytest.fixture(autouse=True)
def setup_class(self, cubeviz_helper, image_cube_hdu_obj_microns):
- cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label='has_microns')
self.cubeviz = cubeviz_helper
+ cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label='has_microns')
self.viewer = cubeviz_helper.default_viewer._obj # This is used in BaseRegionHandler
self.spectrum_viewer = cubeviz_helper.app.get_viewer(
cubeviz_helper._default_spectrum_viewer_reference_name
diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py
index 772f5ef2ab..6c9967ec00 100644
--- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py
+++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py
@@ -77,12 +77,14 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube):
# Set the active tool to spectrumperspaxel
flux_viewer.toolbar.active_tool = flux_viewer.toolbar.tools['jdaviz:spectrumperspaxel']
- x = 1
- y = 1
+
assert len(flux_viewer.native_marks) == 2
assert len(spectrum_viewer.data()) == 1
# Check coordinate info panel
+ sl = cubeviz_helper.plugins['Slice']
+ sl.value = sl._obj.valid_indicator_values_sorted[1]
+ assert flux_viewer.slice == 1
label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info']
label_mouseover._viewer_mouse_event(flux_viewer,
{'event': 'mousemove', 'domain': {'x': 1, 'y': 1}})
@@ -91,6 +93,8 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube):
'204.9997755344 27.0001999998 (deg)')
# Click on spaxel location
+ x = 1
+ y = 1
flux_viewer.toolbar.active_tool.on_mouse_event(
{'event': 'click', 'domain': {'x': x, 'y': y}, 'altKey': False})
assert len(flux_viewer.native_marks) == 3
diff --git a/jdaviz/configs/cubeviz/plugins/tools.py b/jdaviz/configs/cubeviz/plugins/tools.py
index 8f420d4149..6c986b163e 100644
--- a/jdaviz/configs/cubeviz/plugins/tools.py
+++ b/jdaviz/configs/cubeviz/plugins/tools.py
@@ -62,12 +62,12 @@ def __init__(self, viewer, **kwargs):
def activate(self):
self.viewer.add_event_callback(self.on_mouse_event,
events=['dragmove', 'click'])
- msg = SliceToolStateMessage({'active': True}, sender=self)
+ msg = SliceToolStateMessage({'active': True}, viewer=self.viewer, sender=self)
self.viewer.session.hub.broadcast(msg)
def deactivate(self):
self.viewer.remove_event_callback(self.on_mouse_event)
- msg = SliceToolStateMessage({'active': False}, sender=self)
+ msg = SliceToolStateMessage({'active': False}, viewer=self.viewer, sender=self)
self.viewer.session.hub.broadcast(msg)
def on_mouse_event(self, data):
diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py
index 75c9bdcc42..f2fb530238 100644
--- a/jdaviz/configs/cubeviz/plugins/viewers.py
+++ b/jdaviz/configs/cubeviz/plugins/viewers.py
@@ -1,3 +1,5 @@
+import numpy as np
+
from functools import cached_property
from glue.core import BaseData
@@ -14,10 +16,15 @@
from jdaviz.core.freezable_state import FreezableBqplotImageViewerState
from jdaviz.utils import get_subset_type
-__all__ = ['CubevizImageView', 'CubevizProfileView', 'WithSliceIndicator']
+__all__ = ['CubevizImageView', 'CubevizProfileView',
+ 'WithSliceIndicator', 'WithSliceSelection']
class WithSliceIndicator:
+ @property
+ def slice_component_label(self):
+ return str(self.state.x_att)
+
@cached_property
def slice_indicator(self):
# SliceIndicatorMarks does not yet exist
@@ -25,9 +32,90 @@ def slice_indicator(self):
self.figure.marks = self.figure.marks + slice_indicator.marks
return slice_indicator
+ @property
+ def slice_values(self):
+ def _get_component(layer):
+ try:
+ return layer.layer.get_component(self.slice_component_label).data
+ except (AttributeError, KeyError):
+ # layer either does not have get_component (because its a subset)
+ # or slice_component_label is not a component in this layer
+ # either way, return an empty array and skip this layer
+ return np.array([])
+ try:
+ return np.asarray(np.unique(np.concatenate([_get_component(layer) for layer in self.layers])), # noqa
+ dtype=float)
+ except ValueError:
+ return np.array([])
+
+ def _set_slice_indicator_value(self, value):
+ # this is a separate method so that viewers can override and map value if necessary
+ # NOTE: on first call, this will initialize the indicator itself
+ self.slice_indicator.value = value
+
+
+class WithSliceSelection:
+ @property
+ def slice_index(self):
+ # index in state.slices corresponding to the slice axis
+ return 2
+
+ @property
+ def slice_component_label(self):
+ slice_plg = self.jdaviz_helper.plugins.get('Slice', None)
+ if slice_plg is None: # pragma: no cover
+ raise ValueError("slice plugin must be activated to access slice_component_label")
+ return slice_plg._obj.slice_indicator_viewers[0].slice_component_label
+
+ @property
+ def slice_values(self):
+ # TODO: make a cached property and invalidate cache on add/remove data
+ # TODO: add support for multiple cubes (but then slice selection needs to be more complex)
+ # if slice_index is 0, then we want the equivalent of [:, 0, 0]
+ # if slice_index is 1, then we want the equivalent of [0, :, 0]
+ # if slice_index is 2, then we want the equivalent of [0, 0, :]
+ take_inds = [2, 1, 0]
+ take_inds.remove(self.slice_index)
+ for layer in self.layers:
+ try:
+ data_obj = layer.layer.data.get_component(self.slice_component_label).data
+ except (AttributeError, KeyError):
+ continue
+ else:
+ break
+ else:
+ return np.array([])
+ return np.asarray(data_obj.take(0, take_inds[0]).take(0, take_inds[1]), dtype=float)
+
+ @property
+ def slice(self):
+ return self.state.slices[self.slice_index]
+
+ @slice.setter
+ def slice(self, slice):
+ # NOTE: not intended for user-access - this should be controlled through the slice plugin
+ # in order to sync with all other viewers/slice indicators
+ slices = [0, 0, 0]
+ slices[self.slice_index] = slice
+ self.state.slices = tuple(slices)
+
+ @property
+ def slice_value(self):
+ return self.slice_values[self.slice]
+
+ @slice_value.setter
+ def slice_value(self, slice_value):
+ # NOTE: not intended for user-access - this should be controlled through the slice plugin
+ # in order to sync with all other viewers/slice indicators
+ # find the slice nearest slice_value
+ slice_values = self.slice_values
+ if not len(slice_values):
+ return
+ self.slice = np.argmin(abs(slice_values - slice_value))
+
@viewer_registry("cubeviz-image-viewer", label="Image 2D (Cubeviz)")
-class CubevizImageView(JdavizViewerMixin, BqplotImageView):
+class CubevizImageView(JdavizViewerMixin, WithSliceSelection, BqplotImageView):
# categories: zoom resets, (zoom, pan), subset, select tools, shortcuts
# NOTE: zoom and pan are merged here for space consideration and to avoid
# overflow to second row when opening the tray
diff --git a/jdaviz/configs/default/plugins/export_plot/export_plot.py b/jdaviz/configs/default/plugins/export_plot/export_plot.py
index 622a67f758..176ad84a3c 100644
--- a/jdaviz/configs/default/plugins/export_plot/export_plot.py
+++ b/jdaviz/configs/default/plugins/export_plot/export_plot.py
@@ -85,7 +85,7 @@ def _on_cubeviz_data_added(self, msg):
# NOTE: This needs revising if we allow loading more than one cube.
if isinstance(msg.viewer, BqplotImageView):
if len(msg.data.shape) == 3:
- self.i_end = msg.data.shape[-1] - 1 # Same as max_slice in Slice plugin
+ self.i_end = msg.data.shape[-1] - 1
def save_figure(self, filename=None, filetype=None):
"""
@@ -154,7 +154,7 @@ def _save_movie(self, i_start, i_end, fps, filename, rm_temp_files):
viewer = self.viewer.selected_obj
slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
- orig_slice = slice_plg.slice
+ orig_slice = viewer.slice
temp_png_files = []
i = i_start
video = None
@@ -167,7 +167,7 @@ def _save_movie(self, i_start, i_end, fps, filename, rm_temp_files):
if self.movie_interrupt:
break
- slice_plg._on_slider_updated({'new': i})
+ slice_plg.vue_play_next()
cur_pngfile = f"._cubeviz_movie_frame_{i}.png"
self.save_figure(filename=cur_pngfile, filetype="png")
temp_png_files.append(cur_pngfile)
@@ -297,8 +297,9 @@ def save_movie(self, i_start=None, i_end=None, fps=None, filename=None, filetype
slice_plg = self.app._jdaviz_helper.plugins["Slice"]._obj
if i_start < 0: # pragma: no cover
i_start = 0
- if i_end > slice_plg.max_slice: # pragma: no cover
- i_end = slice_plg.max_slice
+ max_slice = len(slice_plg.valid_values_sorted) - 1
+ if i_end > max_slice: # pragma: no cover
+ i_end = max_slice
if i_end <= i_start:
raise ValueError(f"No frames to write: i_start={i_start}, i_end={i_end}")
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 b9a9119a0b..62f8306346 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
@@ -142,7 +142,6 @@ def _on_slice_changed(self, msg):
return
self.cube_slice = f"{msg.value:.3e} {msg.value_unit}"
self._cube_wave = u.Quantity(msg.value, msg.value_unit)
- self._cube_idx = int(msg.slice)
@observe("dataset_selected")
def _on_dataset_selected_changed(self, event={}):
@@ -305,6 +304,11 @@ def _aperture_selected_changed(self, event={}):
else:
self._background_selected_changed()
+ @property
+ def _cubeviz_slice_ind(self):
+ fv = self.app.get_viewer(self.app._jdaviz_helper._default_flux_viewer_reference_name)
+ return fv.slice
+
def _calc_background_median(self, reg, data=None):
# Basically same way image stats are calculated in vue_do_aper_phot()
# except here we only care about one stat for the background.
@@ -320,7 +324,7 @@ def _calc_background_median(self, reg, data=None):
comp = data.get_component(data.main_components[0])
if self.config == "cubeviz" and data.ndim > 2:
- comp_data = comp.data[:, :, self._cube_idx].T # nx, ny --> ny, nx
+ comp_data = comp.data[:, :, self._cubeviz_slice_ind].T # nx, ny --> ny, nx
# Similar to coords_info logic.
if '_orig_spec' in getattr(data, 'meta', {}):
w = data.meta['_orig_spec'].wcs.celestial
@@ -449,7 +453,7 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None,
raise ValueError('Missing or invalid background value')
if self.config == "cubeviz" and data.ndim > 2:
- comp_data = comp.data[:, :, self._cube_idx].T # nx, ny --> ny, nx
+ comp_data = comp.data[:, :, self._cubeviz_slice_ind].T # nx, ny --> ny, nx
# Similar to coords_info logic.
if '_orig_spec' in getattr(data, 'meta', {}):
w = data.meta['_orig_spec'].wcs
@@ -470,7 +474,7 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None,
ycenter = reg.center.y
if data.coords is not None:
if self.config == "cubeviz" and data.ndim > 2:
- sky_center = w.pixel_to_world(self._cube_idx, ycenter, xcenter)[1]
+ sky_center = w.pixel_to_world(self._cubeviz_slice_ind, ycenter, xcenter)[1]
else: # "imviz"
sky_center = w.pixel_to_world(xcenter, ycenter)
else:
diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
index 745815a74d..d2bcdd45d3 100644
--- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
+++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
@@ -327,7 +327,7 @@ def _image_viewer_update(self, viewer, x, y):
if data_wcs:
try:
if wcs_ndim == 3:
- sky = data_wcs.pixel_to_world(viewer.state.slices[-1], y, x)[1].icrs
+ sky = data_wcs.pixel_to_world(viewer.slice, y, x)[1].icrs
else: # wcs_ndim == 2
sky = data_wcs.pixel_to_world(x, y).icrs
except Exception:
@@ -341,7 +341,7 @@ def _image_viewer_update(self, viewer, x, y):
slice_plugin = self.app._jdaviz_helper.plugins.get('Slice', None)
if slice_plugin is not None and len(image.shape) == 3:
# float to be compatible with default value of nan
- self._dict['slice'] = float(slice_plugin.slice)
+ self._dict['slice'] = float(viewer.slice)
self._dict['spectral_axis'] = slice_plugin.value
self._dict['spectral_axis:unit'] = slice_plugin._obj.value_unit
diff --git a/jdaviz/core/events.py b/jdaviz/core/events.py
index 88b0623ad1..a26a024344 100644
--- a/jdaviz/core/events.py
+++ b/jdaviz/core/events.py
@@ -306,39 +306,38 @@ def shared_image(self):
class SliceSelectSliceMessage(Message):
'''Message generated by the cubeviz helper and processed by the slice plugin to sync
slice selection across all viewers'''
- def __init__(self, slice=None, value=None, *args, **kwargs):
+ def __init__(self, value, *args, **kwargs):
super().__init__(*args, **kwargs)
- self._slice = slice
self._value = value
- @property
- def slice(self):
- return self._slice
-
@property
def value(self):
return self._value
class SliceValueUpdatedMessage(Message):
- '''Message generated by the slice plugin when the selected slice and wavelength are updated'''
- def __init__(self, slice, value, value_unit, *args, **kwargs):
+ '''Message generated by the slice plugin when the selected slice is updated'''
+ def __init__(self, value, value_unit, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.slice = slice
self.value = value
self.value_unit = value_unit
class SliceToolStateMessage(Message):
'''Message generated by the select slice plot plugin when activated/deactivated'''
- def __init__(self, change, *args, **kwargs):
+ def __init__(self, change, viewer, *args, **kwargs):
super().__init__(*args, **kwargs)
self._change = change
+ self._viewer = viewer
@property
def change(self):
return self._change
+ @property
+ def viewer(self):
+ return self._viewer
+
class LinkUpdatedMessage(Message):
'''Message generated when the WCS/pixel linking is changed'''
diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py
index c607d8b284..a2634adfd7 100644
--- a/jdaviz/core/marks.py
+++ b/jdaviz/core/marks.py
@@ -144,8 +144,9 @@ def __init__(self, viewer, x, **kwargs):
# the location of the marker will need to update automatically if the
# underlying data changes (through a unit conversion, for example)
- viewer.state.add_callback("reference_data",
- self._update_reference_data)
+ if hasattr(viewer.state, 'reference_data'):
+ viewer.state.add_callback("reference_data",
+ self._update_reference_data)
scales = viewer.scales
@@ -157,16 +158,18 @@ def __init__(self, viewer, x, **kwargs):
def _update_reference_data(self, reference_data):
if reference_data is None:
return
- self._update_data(reference_data.get_object(cls=Spectrum1D).spectral_axis)
+ self._update_unit(reference_data.get_object(cls=Spectrum1D).spectral_axis.unit)
- def _update_data(self, x_all):
+ def _update_unit(self, new_unit):
# the x-units may have changed. We want to convert the internal self.x
# from self.xunit to the new units (x_all.unit)
- new_unit = x_all.unit
+ if self.xunit is None:
+ self.xunit = new_unit
+ return
if new_unit == self.xunit:
return
old_quant = self.x[0]*self.xunit
- x = old_quant.to_value(x_all.unit, equivalencies=u.spectral())
+ x = old_quant.to_value(new_unit, equivalencies=u.spectral())
self.x = [x, x]
self.xunit = new_unit
@@ -249,8 +252,10 @@ def identify(self, identify):
def _process_identify_change(self, msg):
self.identify = msg.name_rest == self.table_index
- def _update_data(self, x_all):
- new_unit = x_all.unit
+ def _update_unit(self, new_unit):
+ if self.xunit is None:
+ self.xunit = new_unit
+ return
if new_unit == self.xunit:
return
@@ -264,35 +269,28 @@ def _update_data(self, x_all):
class SliceIndicatorMarks(BaseSpectrumVerticalLine, HubListener):
"""Subclass on bqplot Lines to handle slice/wavelength indicator.
"""
- def __init__(self, viewer, slice=0, **kwargs):
+ def __init__(self, viewer, value=0, **kwargs):
self._viewer = viewer
+ self._value = None
self._oob = False # out-of-bounds, either False, 'left', or 'right'
self._active = False
+ # TODO: new viewers need to respect plugin settings
self._show_if_inactive = True
self._show_value = True
- self.slice = slice
- data = viewer.data()[0]
- if hasattr(data, 'spectral_axis'):
- x_all = data.spectral_axis
- else:
- x_all = []
- # _update_data will set self._x_all, self._x_unit, self.x
- self._update_data(x_all)
-
- viewer.state.add_callback("x_min", lambda x_min: self._handle_oob(update_label=True))
- viewer.state.add_callback("x_max", lambda x_max: self._handle_oob(update_label=True))
+ viewer.state.add_callback("x_min", lambda x_min: self._value_handle_oob(update_label=True))
+ viewer.state.add_callback("x_max", lambda x_max: self._value_handle_oob(update_label=True))
viewer.session.hub.subscribe(self, SliceToolStateMessage,
handler=self._on_change_state)
super().__init__(viewer=viewer,
- x=self.x[0],
+ x=[value, value],
stroke_width=2,
marker='diamond',
fill='none', close_path=False,
labels=['slice'], labels_visibility='none', **kwargs)
- self._handle_oob()
+ self.value = value
# instead of using the Lines label which is limited, we'll use a Label object which
# will follow the x-coordinate of the slice indicator line, with a fixed y-value
@@ -302,47 +300,48 @@ def __init__(self, viewer, slice=0, **kwargs):
# default to the initial state of the tool since we can't control if this will
# happen before or after the initialization of the tool
- self._on_change_state({'active': True})
+ tool_active = self.viewer.toolbar.active_tool_id == 'jdaviz:selectslice'
+ self._on_change_state({'active': tool_active})
@property
def marks(self):
return [self, self.label]
- def _handle_oob(self, x_coord=None, update_label=False):
- if x_coord is None:
- x_coord = self._slice_to_x(self.slice)
+ def _value_handle_oob(self, x=None, update_label=False):
+ if x is None:
+ x = self.value
+ else:
+ self._value = x
x_min, x_max = self._viewer.state.x_min, self._viewer.state.x_max
if x_min is None or x_max is None:
- self.x = [x_coord, x_coord]
+ self.x = [x, x]
return
x_range = x_max - x_min
padding_fig = 0.01
padding = padding_fig * x_range
x_min += padding
x_max -= padding
- if x_coord < x_min:
+ # ensure y-scale has been set (we'll only be overriding x, but scatter viewers complain
+ # if y-scale is not set)
+ self.scales.setdefault('y', LinearScale(min=0, max=1))
+ if x < x_min:
self.x = [padding_fig, padding_fig]
self.scales = {**self.scales, 'x': LinearScale(min=0, max=1)}
self.line_style = 'dashed'
self._oob = 'left'
- elif x_coord > x_max:
+ elif x > x_max:
self.x = [1-padding_fig, 1-padding_fig]
self.scales = {**self.scales, 'x': LinearScale(min=0, max=1)}
self.line_style = 'dashed'
self._oob = 'right'
else:
- self.x = [x_coord, x_coord]
+ self.x = [x, x]
self.scales = {**self.scales, 'x': self._viewer.scales['x']}
self.line_style = 'solid'
self._oob = False
if update_label:
self._update_label()
- def _slice_to_x(self, slice=0):
- if not isinstance(slice, int):
- raise TypeError(f"slice must be of type int, not {type(slice)}")
- return self._x_all[slice]
-
def _update_colors_opacities(self):
# orange (accent) if active, import button blue otherwise (see css in main_styles.vue)
if not self._show_if_inactive and not self._active:
@@ -359,6 +358,8 @@ def _on_change_state(self, msg={}):
if isinstance(msg, dict):
changes = msg
else:
+ if msg.viewer is not None and msg.viewer != self.viewer:
+ return
changes = msg.change
for k, v in changes.items():
@@ -372,39 +373,31 @@ def _on_change_state(self, msg={}):
self._update_colors_opacities()
def _update_label(self):
+ def _formatted_value(value):
+ power = abs(np.log10(value))
+ if power >= 3:
+ # use scientific notation
+ return f'{value:0.4e}'
+ else:
+ return f'{value:0.4f}'
+
+ valuestr = _formatted_value(self.value)
+ xunit = str(self.xunit) if self.xunit is not None else ''
# U+00A0 is a blank space, U+25C0 a left arrow triangle, and U+25B6 a right arrow triangle
if self._oob == 'left':
- self.labels = [f'\u00A0 \u25c0 {self._slice_to_x(self.slice):0.4e} {self._x_unit} \u00A0'] # noqa
+ self.labels = [f'\u00A0 \u25c0 {valuestr} {xunit} \u00A0'] # noqa
elif self._oob == 'right':
- self.labels = [f'{self._slice_to_x(self.slice):0.4e} {self._x_unit} \u25b6 \u00A0']
+ self.labels = [f'{valuestr} {xunit} \u25b6 \u00A0']
else:
- self.labels = [f'\u00A0 {self._slice_to_x(self.slice):0.4e} {self._x_unit} \u00A0']
+ self.labels = [f'\u00A0 {valuestr} {xunit} \u00A0']
@property
- def slice(self):
- return self._slice
-
- @slice.setter
- def slice(self, slice):
- self._slice = slice
- # if this is within the init, the data may not have been set yet,
- # in which case we'll just set self._slice for the first time, but
- # do not need to update self.x or label (yet)
- if hasattr(self, '_x_all'):
- x_coord = self._slice_to_x(slice)
- self._handle_oob(x_coord)
- self._update_label()
+ def value(self):
+ return self._value
- def _update_data(self, x_all):
- # we want to preserve slice number, so we'll do a bit more than the
- # default unit-conversion in the base class
- self._x_all = x_all.value
- self._x_unit = str(x_all.unit)
- x_coord = self._slice_to_x(self.slice)
- self._handle_oob(x_coord)
- if self.labels_visibility == 'label':
- # update label with new value/unit
- self._update_label()
+ @value.setter
+ def value(self, value):
+ self._value_handle_oob(value, update_label=True)
class ShadowMixin:
diff --git a/notebooks/concepts/cubeviz_ndarray_gif.ipynb b/notebooks/concepts/cubeviz_ndarray_gif.ipynb
index 2ea0432750..b45f64c0c4 100644
--- a/notebooks/concepts/cubeviz_ndarray_gif.ipynb
+++ b/notebooks/concepts/cubeviz_ndarray_gif.ipynb
@@ -309,7 +309,7 @@
},
"outputs": [],
"source": [
- "slice_plg.slice = 0"
+ "slice_plg.value = 0"
]
},
{