diff --git a/CHANGES.rst b/CHANGES.rst index 61bc87d29e..9b3ab71d2b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ New Features - Stretch histogram shows a spinner when the histogram data is updating. [#2644] +- Spectrum viewer bounds can now be set through the Plot Options UI. [#2604] + Cubeviz ^^^^^^^ - Calculated moments can now be output in velocity units. [#2584, #2588] diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 75bf5c9c9c..7855565200 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -14,7 +14,7 @@ from glue.config import colormaps, stretches from glue.viewers.scatter.state import ScatterViewerState from glue.viewers.profile.state import ProfileViewerState, ProfileLayerState -from glue.viewers.image.state import ImageSubsetLayerState +from glue.viewers.image.state import ImageSubsetLayerState, ImageViewerState from glue.viewers.scatter.state import ScatterLayerState as BqplotScatterLayerState from glue.viewers.image.composite_array import COLOR_CONVERTER from glue_jupyter.bqplot.image.state import BqplotImageLayerState @@ -26,7 +26,7 @@ skip_if_no_updates_since_last_active, with_spinner) from jdaviz.core.user_api import PluginUserApi from jdaviz.core.tools import ICON_DIR -from jdaviz.core.custom_traitlets import IntHandleEmpty +from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty from scipy.interpolate import PchipInterpolator @@ -177,6 +177,7 @@ class PlotOptions(PluginTemplateMixin): viewer_multiselect = Bool(False).tag(sync=True) viewer_items = List().tag(sync=True) viewer_selected = Any().tag(sync=True) # Any needed for multiselect + viewer_limits = Dict().tag(sync=True) layer_multiselect = Bool(False).tag(sync=True) layer_items = List().tag(sync=True) @@ -204,6 +205,27 @@ class PlotOptions(PluginTemplateMixin): uncertainty_visible_value = Int().tag(sync=True) uncertainty_visible_sync = Dict().tag(sync=True) + viewer_x_min_value = FloatHandleEmpty().tag(sync=True) + viewer_x_min_sync = Dict().tag(sync=True) + + viewer_x_max_value = FloatHandleEmpty().tag(sync=True) + viewer_x_max_sync = Dict().tag(sync=True) + + viewer_x_unit_value = Unicode(allow_none=True).tag(sync=True) + viewer_x_unit_sync = Dict().tag(sync=True) + + viewer_y_min_value = FloatHandleEmpty().tag(sync=True) + viewer_y_min_sync = Dict().tag(sync=True) + + viewer_y_max_value = FloatHandleEmpty().tag(sync=True) + viewer_y_max_sync = Dict().tag(sync=True) + + viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True) + viewer_y_unit_sync = Dict().tag(sync=True) + + viewer_x_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value + viewer_y_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value + # scatter/marker options marker_visible_value = Bool().tag(sync=True) marker_visible_sync = Dict().tag(sync=True) @@ -330,6 +352,7 @@ class PlotOptions(PluginTemplateMixin): icon_checktoradial = Unicode(read_icon(os.path.join(ICON_DIR, 'checktoradial.svg'), 'svg+xml')).tag(sync=True) # noqa show_viewer_labels = Bool(True).tag(sync=True) + show_viewer_bounds = Bool(True).tag(sync=True) cmap_samples = Dict().tag(sync=True) swatches_palette = List().tag(sync=True) @@ -368,6 +391,9 @@ def is_image(state): def not_image(state): return not is_image(state) + def not_image_viewer(state): + return not isinstance(state, ImageViewerState) + def not_image_or_spatial_subset(state): return not is_image(state) and not is_spatial_subset(state) @@ -406,6 +432,26 @@ def state_attr_for_line_visible(state): self.uncertainty_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_uncertainty', # noqa 'uncertainty_visible_value', 'uncertainty_visible_sync') # noqa + # Viewer bounds + self.viewer_x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_min', + 'viewer_x_min_value', 'viewer_x_min_sync', + state_filter=not_image_viewer) + self.viewer_x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_max', + 'viewer_x_max_value', 'viewer_x_max_sync', + state_filter=not_image_viewer) + self.viewer_x_unit = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_display_unit', + 'viewer_x_unit_value', 'viewer_x_unit_sync', + state_filter=not_image_viewer) + self.viewer_y_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_min', + 'viewer_y_min_value', 'viewer_y_min_sync', + state_filter=not_image) + self.viewer_y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max', + 'viewer_y_max_value', 'viewer_y_max_sync', + state_filter=not_image) + self.viewer_y_unit = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_display_unit', + 'viewer_y_unit_value', 'viewer_y_unit_sync', + state_filter=not_image_viewer) + # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) self.marker_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'visible', @@ -557,8 +603,6 @@ def state_attr_for_line_visible(state): self.axes_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_axes', 'axes_visible_value', 'axes_visible_sync', state_filter=not_profile) - # zoom limits - # display_units self.show_viewer_labels = self.app.state.settings['viewer_labels'] self.app.state.add_callback('settings', self._on_app_settings_changed) @@ -699,6 +743,52 @@ def apply_RGB_presets(self): def vue_apply_RGB_presets(self, data): self.apply_RGB_presets() + @observe('viewer_selected', 'viewer_x_max_value', 'viewer_x_min_value', + 'viewer_y_max_value', 'viewer_y_min_value') + def _update_viewer_bound_steps(self, msg={}): + if not hasattr(self, 'viewer'): # pragma: no cover + # plugin hasn't been fully initialized yet + return + + if not self.viewer.selected: # pragma: no cover + # nothing selected yet + return + + if self.viewer_multiselect: + not_image = [not isinstance(v.state, ImageViewerState) for v in self.viewer.selected_obj] # noqa + if np.all(not_image): + self.show_viewer_bounds = True + else: + self.show_viewer_bounds = False + return + + viewer = self.viewer.selected_obj[0] if self.viewer_multiselect else self.viewer.selected_obj # noqa + if not isinstance(viewer.state, ImageViewerState): + self.show_viewer_bounds = True + # We round these values to show, e.g., 7.15 instead of 7.1499999 + if hasattr(viewer.state, "x_max") and viewer.state.x_max is not None: + bound_step = (viewer.state.x_max - viewer.state.x_min) / 100. + decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 + if decimals < 0: + decimals = 0 + self.viewer_x_bound_step = np.round(bound_step, decimals=decimals) + self.viewer_x_max_value = np.round(self.viewer_x_max_value, decimals=decimals) + self.viewer_x_min_value = np.round(self.viewer_x_min_value, decimals=decimals) + if hasattr(viewer.state, "y_max") and viewer.state.y_max is not None: + bound_step = (viewer.state.y_max - viewer.state.y_min) / 100. + decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 + if decimals < 0: + decimals = 0 + self.viewer_y_bound_step = np.round(bound_step, decimals=decimals) + self.viewer_y_max_value = np.round(self.viewer_y_max_value, decimals=decimals) + self.viewer_y_min_value = np.round(self.viewer_y_min_value, decimals=decimals) + + def vue_reset_viewer_bounds(self, _): + # This button is currently only exposed if only the spectrum viewer is selected + viewers = [self.viewer.selected_obj] if not self.viewer_multiselect else self.viewer.selected_obj # noqa + for viewer in viewers: + viewer.toolbar.tools['jdaviz:homezoom'].activate() + @observe('stretch_function_sync', 'stretch_params_sync', 'stretch_vmin_sync', 'stretch_vmax_sync', 'image_color_mode_sync', 'image_color_sync', 'image_colormap_sync') diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index a94d73d1b4..4c84405fdc 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -39,6 +39,64 @@ :hint="viewer_multiselect ? 'Select viewers to set options simultaneously' : 'Select the viewer to set options.'" /> + + + + + Viewer bounds + + + + + + + + + + + + + + + + Reset viewer bounds + + + + + +
- @@ -479,7 +537,7 @@
1 def _update_mixed_state(self):