From aa8200f7f628a677ff03a95e4a767f54759ffd03 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Feb 2024 17:43:20 -0500 Subject: [PATCH] image viewer zoom-level/center (#2649) --- CHANGES.rst | 2 +- jdaviz/configs/cubeviz/plugins/viewers.py | 6 +- .../plugins/plot_options/plot_options.py | 181 +++++++++++------- .../plugins/plot_options/plot_options.vue | 96 ++++++---- jdaviz/configs/imviz/plugins/viewers.py | 2 + jdaviz/configs/mosviz/plugins/viewers.py | 1 + jdaviz/core/freezable_state.py | 130 +++++++++++-- jdaviz/core/template_mixin.py | 5 + 8 files changed, 306 insertions(+), 117 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9f893da0f0..57dbfc4359 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,7 +7,7 @@ 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] +- Spectrum and image viewer bounds can now be set through the Plot Options UI. [#2604, #2649] - Opacity for spatial subsets is now adjustable from within Plot Options. [#2663] diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py index e7119e8732..33a2b99766 100644 --- a/jdaviz/configs/cubeviz/plugins/viewers.py +++ b/jdaviz/configs/cubeviz/plugins/viewers.py @@ -9,8 +9,9 @@ from jdaviz.configs.cubeviz.helper import layer_is_cube_image_data from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView -from jdaviz.utils import get_subset_type from jdaviz.core.events import AddDataMessage, RemoveDataMessage +from jdaviz.core.freezable_state import FreezableBqplotImageViewerState +from jdaviz.utils import get_subset_type __all__ = ['CubevizImageView', 'CubevizProfileView'] @@ -31,9 +32,12 @@ class CubevizImageView(JdavizViewerMixin, BqplotImageView): ] default_class = None + _state_cls = FreezableBqplotImageViewerState def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # provide reference from state back to viewer to use for zoom syncing + self.state._viewer = self self._subscribe_to_layers_update() self.state.add_callback('reference_data', self._initial_x_axis) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 3dceddb5c4..ca5600580c 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -24,9 +24,10 @@ from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelect, LayerSelect, PlotOptionsSyncState, Plot, skip_if_no_updates_since_last_active, with_spinner) +from jdaviz.core.events import ChangeRefDataMessage from jdaviz.core.user_api import PluginUserApi from jdaviz.core.tools import ICON_DIR -from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty +from jdaviz.core.custom_traitlets import IntHandleEmpty from scipy.interpolate import PchipInterpolator @@ -93,6 +94,16 @@ def update_knots(self, x, y): stretches.add("spline", SplineStretch, display="Spline") +def _round_step(step): + # round the step for a float input + if step <= 0: + return 1e-6, 6 + decimals = -int(np.log10(abs(step))) + 1 if step != 0 else 6 + if decimals < 0: + decimals = 0 + return np.round(step, decimals), decimals + + @tray_registry('g-plot-options', label="Plot Options") class PlotOptions(PluginTemplateMixin): """ @@ -176,6 +187,9 @@ class PlotOptions(PluginTemplateMixin): template_file = __file__, "plot_options.vue" uses_active_status = Bool(True).tag(sync=True) + # read-only display units + display_units = Dict().tag(sync=True) + viewer_multiselect = Bool(False).tag(sync=True) viewer_items = List().tag(sync=True) viewer_selected = Any().tag(sync=True) # Any needed for multiselect @@ -207,26 +221,31 @@ 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) + x_min_value = Float().tag(sync=True) + x_min_sync = Dict().tag(sync=True) - viewer_x_max_value = FloatHandleEmpty().tag(sync=True) - viewer_x_max_sync = Dict().tag(sync=True) + x_max_value = Float().tag(sync=True) + 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) + y_min_value = Float().tag(sync=True) + y_min_sync = Dict().tag(sync=True) - viewer_y_min_value = FloatHandleEmpty().tag(sync=True) - viewer_y_min_sync = Dict().tag(sync=True) + y_max_value = Float().tag(sync=True) + y_max_sync = Dict().tag(sync=True) - viewer_y_max_value = FloatHandleEmpty().tag(sync=True) - viewer_y_max_sync = Dict().tag(sync=True) + x_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value + y_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value - viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True) - viewer_y_unit_sync = Dict().tag(sync=True) + zoom_center_x_value = Float().tag(sync=True) + zoom_center_x_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 + zoom_center_y_value = Float().tag(sync=True) + zoom_center_y_sync = Dict().tag(sync=True) + + zoom_radius_value = Float().tag(sync=True) + zoom_radius_sync = Dict().tag(sync=True) + + zoom_step = Float(1).tag(sync=True) # scatter/marker options marker_visible_value = Bool().tag(sync=True) @@ -357,7 +376,6 @@ 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) @@ -444,24 +462,24 @@ def state_attr_for_line_visible(state): '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) + self.x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_min', + 'x_min_value', 'x_min_sync', + state_filter=not_image_viewer) + self.x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_max', + 'x_max_value', 'x_max_sync', + state_filter=not_image_viewer) + self.y_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_min', + 'y_min_value', 'y_min_sync', + state_filter=not_image_viewer) + self.y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max', + 'y_max_value', 'y_max_sync', + state_filter=not_image_viewer) + self.zoom_center_x = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_x', + 'zoom_center_x_value', 'zoom_center_x_sync') + self.zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y', + 'zoom_center_y_value', 'zoom_center_y_sync') + self.zoom_radius = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_radius', + 'zoom_radius_value', 'zoom_radius_sync') # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) @@ -621,6 +639,16 @@ def state_attr_for_line_visible(state): self.show_viewer_labels = self.app.state.settings['viewer_labels'] self.app.state.add_callback('settings', self._on_app_settings_changed) + sv = self.spectrum_viewer + if sv is not None: + sv.state.add_callback('x_display_unit', + self._on_global_display_unit_changed) + sv.state.add_callback('y_display_unit', + self._on_global_display_unit_changed) + + self.hub.subscribe(self, ChangeRefDataMessage, + handler=self._on_refdata_change) + # give UI access to sampled version of the available colormap choices def hex_for_cmap(cmap): N = 50 @@ -635,10 +663,12 @@ def user_api(self): if self.config == "cubeviz": expose += ['collapse_function', 'uncertainty_visible'] if self.config != "imviz": - expose += ['axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity', + expose += ['x_min', 'x_max', 'y_min', 'y_max', + 'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity', 'line_as_steps', 'uncertainty_visible'] if self.config != "specviz": - expose += ['subset_color', 'subset_opacity', + expose += ['zoom_center_x', 'zoom_center_y', 'zoom_radius', + 'subset_color', 'subset_opacity', 'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax', 'stretch_hist_zoom_limits', 'stretch_hist_nbins', 'image_visible', 'image_color_mode', @@ -687,6 +717,20 @@ def select_all(self, viewers=True, layers=True): self.layer_multiselect = True self.layer.select_all() + def _on_global_display_unit_changed(self, *args): + sv = self.spectrum_viewer + self.display_units['spectral'] = sv.state.x_display_unit + self.display_units['flux'] = sv.state.y_display_unit + self.send_state('display_units') + + def _on_refdata_change(self, *args): + if self.app._link_type.lower() == 'wcs': + self.display_units['image'] = 'deg' + else: + self.display_units['image'] = 'pix' + self.send_state('display_units') + self._update_viewer_zoom_steps() + def vue_unmix_state(self, names): if isinstance(names, str): names = [names] @@ -757,45 +801,50 @@ 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') + @observe('viewer_selected', + 'x_min_value', 'x_max_value', + 'y_min_value', 'y_max_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 + if not self.viewer.selected or not self.x_min_sync['in_subscribed_states']: # 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 + for ax in ('x', 'y'): + ax_min = getattr(self, f'{ax}_min_value') + ax_max = getattr(self, f'{ax}_max_value') + bound_step, decimals = _round_step((ax_max - ax_min) / 100.) + decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 + setattr(self, f'{ax}_bound_step', bound_step) + setattr(self, f'{ax}_min_value', np.round(ax_min, decimals=decimals)) + setattr(self, f'{ax}_max_value', np.round(ax_max, decimals=decimals)) + + @observe('viewer_selected', + 'zoom_center_x_value', 'zoom_center_y_value', + 'zoom_radius_value') + def _update_viewer_zoom_steps(self, msg={}): + if not hasattr(self, 'viewer'): # pragma: no cover + # plugin hasn't been fully initialized yet + return + + if not self.viewer.selected or not self.zoom_radius_sync['in_subscribed_states']: + # nothing selected yet + 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) + # in the case of multiple viewers, calculate based on the first + # alternatively, we could find the most extreme by looping over all selected viewers + viewers = self.viewer.selected_obj if self.viewer_multiselect else [self.viewer.selected_obj] # noqa + for viewer in viewers: + if hasattr(viewer.state, '_get_reset_limits'): + break + else: + # no image viewer + return + x_min, x_max, y_min, y_max = viewer.state._get_reset_limits(return_as_world=True) + self.zoom_step, _ = _round_step(max(x_max-x_min, y_max-y_min) / 100.) def vue_reset_viewer_bounds(self, _): # This button is currently only exposed if only the spectrum viewer is selected diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index 0c990ebb97..aacddd0f99 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -39,59 +39,91 @@ :hint="viewer_multiselect ? 'Select viewers to set options simultaneously' : 'Select the viewer to set options.'" /> - - + + Viewer bounds - + - + - + - + - - Reset viewer bounds - + + + + + + + + + + + + Reset viewer bounds + + diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py index 1433cf40a5..4a19d2d9cc 100644 --- a/jdaviz/configs/imviz/plugins/viewers.py +++ b/jdaviz/configs/imviz/plugins/viewers.py @@ -36,6 +36,8 @@ class ImvizImageView(JdavizViewerMixin, BqplotImageView, AstrowidgetsImageViewer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # provide reference from state back to viewer to use for zoom syncing + self.state._viewer = self self.init_astrowidgets_api() self._subscribe_to_layers_update() diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py index 69e370d8dc..91fee2ee5b 100644 --- a/jdaviz/configs/mosviz/plugins/viewers.py +++ b/jdaviz/configs/mosviz/plugins/viewers.py @@ -33,6 +33,7 @@ class MosvizImageView(JdavizViewerMixin, BqplotImageView, AstrowidgetsImageViewe ] default_class = None + _state_cls = FreezableBqplotImageViewerState def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index eb6bdf7b38..5f77e59222 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -1,10 +1,13 @@ -from echo import delay_callback +from contextlib import contextmanager +from echo import delay_callback, CallbackProperty import numpy as np from glue.viewers.profile.state import ProfileViewerState from glue_jupyter.bqplot.image.state import BqplotImageViewerState from glue.viewers.matplotlib.state import DeferredDrawCallbackProperty as DDCProperty +from jdaviz.configs.imviz.helper import get_reference_image_data + __all__ = ['FreezableState', 'FreezableProfileViewerState', 'FreezableBqplotImageViewerState'] @@ -54,14 +57,90 @@ def _reset_x_limits(self, *event): class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): linked_by_wcs = False + zoom_radius = CallbackProperty(1.0, docstring="Zoom radius") + zoom_center_x = CallbackProperty(0.0, docstring='x-coordinate of center of zoom box') + zoom_center_y = CallbackProperty(0.0, docstring='y-coordinate of center of zoom box') + def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) self.wcs_only_layers = [] # For Imviz rotation use. + self._during_zoom_sync = False + self.add_callback('zoom_radius', self._set_zoom_radius_center) + self.add_callback('zoom_center_x', self._set_zoom_radius_center) + self.add_callback('zoom_center_y', self._set_zoom_radius_center) + for attr in ('x_min', 'x_max', 'y_min', 'y_max'): + self.add_callback(attr, self._set_axes_lim) + super().__init__(*args, **kwargs) - def reset_limits(self, *event): - if self.reference_data is None: # Nothing to do + @contextmanager + def during_zoom_sync(self): + self._during_zoom_sync = True + try: + yield + except Exception: + self._during_zoom_sync = False + raise + self._during_zoom_sync = False + + def _set_zoom_radius_center(self, *args): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + return + + # When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units, + # x/y_min/max are in pixels of the WCS-only layer + if self.linked_by_wcs: + image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) + ref_wcs = image.coords + cr = ref_wcs.world_to_pixel_values((self.zoom_center_x, self.zoom_center_x+abs(self.zoom_radius)), # noqa + (self.zoom_center_y, self.zoom_center_y)) + center_x, center_xr = cr[0] + center_y, _ = cr[1] + radius = abs(center_xr - center_x) + else: + center_x, center_y = self.zoom_center_x, self.zoom_center_y + radius = abs(self.zoom_radius) + # now center_x/y and radius are in pixel units of the reference data, so can be used to + # update limits + + with self.during_zoom_sync(): + x_min = center_x - radius + x_max = center_x + radius + y_min = center_y - radius + y_max = center_y + radius + self.x_min, self.x_max, self.y_min, self.y_max = x_min, x_max, y_min, y_max + + self._adjust_limits_aspect() + + def _set_axes_aspect_ratio(self, axes_ratio): + # when aspect-ratio is changed (changing viewer.shape), ensure zoom/center are synced + # with zoom-limits + super()._set_axes_aspect_ratio(axes_ratio) + self._set_axes_lim() + + def _set_axes_lim(self, *args): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + return + if None in (self.x_min, self.x_max, self.y_min, self.y_max): return + # When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units, + # x/y_min/max are in pixels of the WCS-only layer + if self.linked_by_wcs: + image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) + ref_wcs = image.coords + lims = ref_wcs.pixel_to_world_values((self.x_min, self.x_max), (self.y_min, self.y_max)) + x_min, x_max = lims[0] + y_min, y_max = lims[1] + else: + x_min, y_min = self.x_min, self.y_min + x_max, y_max = self.x_max, self.y_max + # now x_min/max, y_min/max are in axes units (degrees if WCS-linked, pixels otherwise) + + with self.during_zoom_sync(): + self.zoom_radius = abs(0.5 * min(x_max - x_min, y_max - y_min)) + self.zoom_center_x = 0.5 * (x_max + x_min) + self.zoom_center_y = 0.5 * (y_max + y_min) + + def _get_reset_limits(self, return_as_world=False): wcs_success = False if self.linked_by_wcs and self.reference_data.coords is not None: x_min, x_max = np.inf, -np.inf @@ -82,13 +161,19 @@ def reset_limits(self, *event): world_top_right = data.coords.pixel_to_world(layer.layer.data[pixel_ids[1]].max(), layer.layer.data[pixel_ids[0]].max()) - pixel_bottom_left = self.reference_data.coords.world_to_pixel(world_bottom_left) - pixel_top_right = self.reference_data.coords.world_to_pixel(world_top_right) - - x_min = min(x_min, pixel_bottom_left[0] - 0.5) - x_max = max(x_max, pixel_top_right[0] + 0.5) - y_min = min(y_min, pixel_bottom_left[1] - 0.5) - y_max = max(y_max, pixel_top_right[1] + 0.5) + if return_as_world: + x_min = min(x_min, world_bottom_left.ra.value) + x_max = max(x_max, world_top_right.ra.value) + y_min = min(y_min, world_bottom_left.dec.value) + y_max = max(y_max, world_top_right.dec.value) + else: + pixel_bottom_left = self.reference_data.coords.world_to_pixel(world_bottom_left) + pixel_top_right = self.reference_data.coords.world_to_pixel(world_top_right) + + x_min = min(x_min, pixel_bottom_left[0] - 0.5) + x_max = max(x_max, pixel_top_right[0] + 0.5) + y_min = min(y_min, pixel_bottom_left[1] - 0.5) + y_max = max(y_max, pixel_top_right[1] + 0.5) wcs_success = True if not wcs_success: @@ -98,15 +183,26 @@ def reset_limits(self, *event): if not layer.visible or layer.layer.data.ndim == 1: continue pixel_ids = layer.layer.pixel_component_ids + pixel_id_x = [comp for comp in pixel_ids if comp.label.endswith('[x]')][0] + pixel_id_y = [comp for comp in pixel_ids if comp.label.endswith('[y]')][0] - x_max = max(x_max, layer.layer.data[pixel_ids[1]].max() + 0.5) - y_max = max(y_max, layer.layer.data[pixel_ids[0]].max() + 0.5) + x_max = max(x_max, layer.layer.data[pixel_id_x].max() + 0.5) + y_max = max(y_max, layer.layer.data[pixel_id_y].max() + 0.5) + + return x_min, x_max, y_min, y_max + + def reset_limits(self, *event): + # TODO: use consistent logic for all image viewers by removing this if-statement + # if/when WCS linking is supported (i.e. in cubeviz) + if getattr(self, '_viewer', None) is not None and self._viewer.jdaviz_app.config != 'imviz': + return super().reset_limits(*event) + if self.reference_data is None: # Nothing to do + return + + x_min, x_max, y_min, y_max = self._get_reset_limits() with delay_callback(self, 'x_min', 'x_max', 'y_min', 'y_max'): - self.x_min = x_min - self.x_max = x_max - self.y_min = y_min - self.y_max = y_max + self.x_min, self.x_max, self.y_min, self.y_max = x_min, x_max, y_min, y_max # We need to adjust the limits in here to avoid triggering all # the update events then changing the limits again. self._adjust_limits_aspect() diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 545115c96f..41e9f99196 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -3721,6 +3721,11 @@ def _on_value_changed(self, msg): helper = getattr(glue_state, f'{glue_name}_helper') value = [choice for choice in helper.choices if str(choice) == msg['new']][0] setattr(glue_state, glue_name, value) + elif glue_name in ('zoom_level') and msg['new'] <= 0: + # ignore if negative number (otherwise would fail) + self.value = msg['old'] + self._processing_change_to_glue = False + return else: setattr(glue_state, glue_name, msg['new'])