From cdbc668cf85aa3375fd0fedcd3122a6dee2c0ed9 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 5 Jan 2024 15:31:10 -0500 Subject: [PATCH 01/23] move zoom-level and center logic to image viewer state --- jdaviz/configs/cubeviz/plugins/viewers.py | 6 +- jdaviz/configs/imviz/plugins/viewers.py | 2 + jdaviz/core/astrowidgets_api.py | 50 +--------------- jdaviz/core/freezable_state.py | 70 ++++++++++++++++++++++- 4 files changed, 79 insertions(+), 49 deletions(-) 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/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/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index bd4504e6e9..fcc97c4fc2 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -102,14 +102,7 @@ def center_on(self, point): else: # pragma: no cover pix = point - width = self.state.x_max - self.state.x_min - height = self.state.y_max - self.state.y_min - - with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): - self.state.x_min = pix[0] - (width * 0.5) - self.state.y_min = pix[1] - (height * 0.5) - self.state.x_max = self.state.x_min + width - self.state.y_max = self.state.y_min + height + self.state.zoom_center = pix def offset_by(self, dx, dy): """Move the center to a point that is given offset @@ -182,22 +175,7 @@ def zoom_level(self): if self.shape is None: # pragma: no cover raise ValueError('Viewer is still loading, try again later') - if hasattr(self, '_get_real_xy'): - image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) - # TODO: Do we want top layer instead? - # i_top = get_top_layer_index(self) - # image = self.layers[i_top].layer - real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) - real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) - else: - real_min = (self.state.x_min, self.state.y_min) - real_max = (self.state.x_max, self.state.y_max) - screenx = self.shape[1] - screeny = self.shape[0] - zoom_x = screenx / abs(real_max[0] - real_min[0]) - zoom_y = screeny / abs(real_max[1] - real_min[1]) - - return max(zoom_x, zoom_y) # Similar to Ginga get_scale() + return self.state.zoom_level # Loosely based on glue/viewers/image/state.py @zoom_level.setter @@ -215,29 +193,7 @@ def zoom_level(self, val): self.state.reset_limits() return - new_dx = self.shape[1] * 0.5 / val - if hasattr(self, '_get_real_xy'): - image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) - # TODO: Do we want top layer instead? - # i_top = get_top_layer_index(self) - # image = self.layers[i_top].layer - real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) - real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) - cur_xcen = (real_min[0] + real_max[0]) * 0.5 - new_x_min = self._get_real_xy(image, cur_xcen - new_dx - 0.5, real_min[1], reverse=True)[0] # noqa: E501 - new_x_max = self._get_real_xy(image, cur_xcen + new_dx - 0.5, real_max[1], reverse=True)[0] # noqa: E501 - else: - cur_xcen = (self.state.x_min + self.state.x_max) * 0.5 - new_x_min = cur_xcen - new_dx - 0.5 - new_x_max = cur_xcen + new_dx - 0.5 - - with delay_callback(self.state, 'x_min', 'x_max'): - self.state.x_min = new_x_min - self.state.x_max = new_x_max - - # We need to adjust the limits in here to avoid triggering all - # the update events then changing the limits again. - self.state._adjust_limits_aspect() + self.state.zoom_level = val # Discussion on why we need two different ways to set zoom at # https://github.com/astropy/astrowidgets/issues/144 diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index eb6bdf7b38..f608d1647d 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -1,4 +1,5 @@ -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 @@ -54,9 +55,76 @@ def _reset_x_limits(self, *event): class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): linked_by_wcs = False + zoom_level = CallbackProperty(1.0, docstring='Zoom-level') + zoom_center = CallbackProperty((0.0, 0.0), docstring='Coordinates 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_level', self._set_zoom_level) + self.add_callback('zoom_center', self._set_zoom_center) + for attr in ('x_min', 'x_max', 'y_min', 'y_max'): + self.add_callback(attr, self._set_axes_lim) + + @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_level(self, zoom_level): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + return + if zoom_level <= 0.0: + raise ValueError("zoom_level must be positive") + + cur_xcen = (self.x_min + self.x_max) * 0.5 + new_dx = self._viewer.shape[1] * 0.5 / zoom_level + new_x_min = cur_xcen - new_dx + new_x_max = cur_xcen + new_dx + + with self.during_zoom_sync(): + self.x_min = new_x_min - 0.5 + self.x_max = new_x_max - 0.5 + + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self._adjust_limits_aspect() + + def _set_zoom_center(self, zoom_center): + if self._during_zoom_sync: + return + + cur_xcen = (self.x_min + self.x_max) * 0.5 + cur_ycen = (self.y_min + self.y_max) * 0.5 + delta_x = zoom_center[0] - cur_xcen + delta_y = zoom_center[1] - cur_ycen + + with self.during_zoom_sync(): + self.x_min += delta_x + self.x_max += delta_x + self.y_min += delta_y + self.y_max += delta_y + + def _set_axes_lim(self, *args): + if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: + return + + screenx = self._viewer.shape[1] + screeny = self._viewer.shape[0] + zoom_x = screenx / (self.x_max - self.x_min) + zoom_y = screeny / (self.y_max - self.y_min) + center_x = 0.5 * (self.x_max + self.x_min) + center_y = 0.5 * (self.y_max + self.y_min) + + with self.during_zoom_sync(): + self.zoom_level = max(zoom_x, zoom_y) # Similar to Ginga get_scale() + self.zoom_center = (center_x, center_y) def reset_limits(self, *event): if self.reference_data is None: # Nothing to do From 1e1a63a92b58a7c0dfcab2403020ef092fddcb36 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 8 Jan 2024 10:08:55 -0500 Subject: [PATCH 02/23] expose zoom-level in plot options plugin --- .../plugins/plot_options/plot_options.py | 36 ++++++++++--------- .../plugins/plot_options/plot_options.vue | 21 +++++++---- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 4b8d6c6d6e..76a3b5cdab 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -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, FloatHandleEmpty +from jdaviz.core.custom_traitlets import IntHandleEmpty from scipy.interpolate import PchipInterpolator @@ -207,19 +207,19 @@ 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_value = Float().tag(sync=True) viewer_x_min_sync = Dict().tag(sync=True) - viewer_x_max_value = FloatHandleEmpty().tag(sync=True) + viewer_x_max_value = Float().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_value = Float().tag(sync=True) viewer_y_min_sync = Dict().tag(sync=True) - viewer_y_max_value = FloatHandleEmpty().tag(sync=True) + viewer_y_max_value = Float().tag(sync=True) viewer_y_max_sync = Dict().tag(sync=True) viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True) @@ -228,6 +228,12 @@ class PlotOptions(PluginTemplateMixin): 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 + viewer_zoom_center_value = List().tag(sync=True) # need to split into x/y? + viewer_zoom_center_sync = Dict().tag(sync=True) + + viewer_zoom_level_value = Float().tag(sync=True) + viewer_zoom_level_sync = Dict().tag(sync=True) + # scatter/marker options marker_visible_value = Bool().tag(sync=True) marker_visible_sync = Dict().tag(sync=True) @@ -357,7 +363,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) @@ -455,13 +460,19 @@ def state_attr_for_line_visible(state): 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) + state_filter=not_image_viewer) self.viewer_y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max', 'viewer_y_max_value', 'viewer_y_max_sync', - state_filter=not_image) + state_filter=not_image_viewer) 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.viewer_zoom_center = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center', + 'viewer_zoom_center_value', + 'viewer_zoom_center_sync') + self.viewer_zoom_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', + 'viewer_zoom_level_value', + 'viewer_zoom_level_sync') # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) @@ -768,17 +779,8 @@ def _update_viewer_bound_steps(self, msg={}): # 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. diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index 0c990ebb97..a1c6216f3d 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -39,8 +39,8 @@ :hint="viewer_multiselect ? 'Select viewers to set options simultaneously' : 'Select the viewer to set options.'" /> - - + + Viewer bounds @@ -49,7 +49,7 @@ + + + Date: Mon, 8 Jan 2024 11:44:44 -0500 Subject: [PATCH 03/23] split state into center_x/y and expose in plot options --- .../plugins/plot_options/plot_options.py | 16 ++++++++++----- .../plugins/plot_options/plot_options.vue | 20 ++++++++++++++++++- jdaviz/core/freezable_state.py | 15 ++++++++------ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 76a3b5cdab..d13ff1730c 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -228,8 +228,11 @@ class PlotOptions(PluginTemplateMixin): 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 - viewer_zoom_center_value = List().tag(sync=True) # need to split into x/y? - viewer_zoom_center_sync = Dict().tag(sync=True) + viewer_zoom_center_x_value = Float().tag(sync=True) + viewer_zoom_center_x_sync = Dict().tag(sync=True) + + viewer_zoom_center_y_value = Float().tag(sync=True) + viewer_zoom_center_y_sync = Dict().tag(sync=True) viewer_zoom_level_value = Float().tag(sync=True) viewer_zoom_level_sync = Dict().tag(sync=True) @@ -467,9 +470,12 @@ def state_attr_for_line_visible(state): 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.viewer_zoom_center = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center', - 'viewer_zoom_center_value', - 'viewer_zoom_center_sync') + self.viewer_zoom_center_x = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_x', # noqa + 'viewer_zoom_center_x_value', + 'viewer_zoom_center_x_sync') + self.viewer_zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y', # noqa + 'viewer_zoom_center_y_value', + 'viewer_zoom_center_y_sync') self.viewer_zoom_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', 'viewer_zoom_level_value', 'viewer_zoom_level_sync') diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index a1c6216f3d..3f3bd05d1c 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -86,10 +86,28 @@ :suffix="viewer_y_unit_value" /> + + + + + + Date: Mon, 8 Jan 2024 13:44:34 -0500 Subject: [PATCH 04/23] consolidate and expose to user API --- .../plugins/plot_options/plot_options.py | 139 +++++++++--------- .../plugins/plot_options/plot_options.vue | 58 ++++---- 2 files changed, 96 insertions(+), 101 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index d13ff1730c..af88844974 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -176,6 +176,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,35 +210,29 @@ class PlotOptions(PluginTemplateMixin): uncertainty_visible_value = Int().tag(sync=True) uncertainty_visible_sync = Dict().tag(sync=True) - viewer_x_min_value = Float().tag(sync=True) - viewer_x_min_sync = Dict().tag(sync=True) - - viewer_x_max_value = Float().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) + x_min_value = Float().tag(sync=True) + x_min_sync = Dict().tag(sync=True) - viewer_y_min_value = Float().tag(sync=True) - viewer_y_min_sync = Dict().tag(sync=True) + x_max_value = Float().tag(sync=True) + x_max_sync = Dict().tag(sync=True) - viewer_y_max_value = Float().tag(sync=True) - viewer_y_max_sync = Dict().tag(sync=True) + y_min_value = Float().tag(sync=True) + y_min_sync = Dict().tag(sync=True) - viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True) - viewer_y_unit_sync = Dict().tag(sync=True) + y_max_value = Float().tag(sync=True) + y_max_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 + 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_zoom_center_x_value = Float().tag(sync=True) - viewer_zoom_center_x_sync = Dict().tag(sync=True) + zoom_center_x_value = Float().tag(sync=True) + zoom_center_x_sync = Dict().tag(sync=True) - viewer_zoom_center_y_value = Float().tag(sync=True) - viewer_zoom_center_y_sync = Dict().tag(sync=True) + zoom_center_y_value = Float().tag(sync=True) + zoom_center_y_sync = Dict().tag(sync=True) - viewer_zoom_level_value = Float().tag(sync=True) - viewer_zoom_level_sync = Dict().tag(sync=True) + zoom_level_value = Float().tag(sync=True) + zoom_level_sync = Dict().tag(sync=True) # scatter/marker options marker_visible_value = Bool().tag(sync=True) @@ -452,33 +449,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_viewer) - self.viewer_y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max', - 'viewer_y_max_value', 'viewer_y_max_sync', - state_filter=not_image_viewer) - 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.viewer_zoom_center_x = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_x', # noqa - 'viewer_zoom_center_x_value', - 'viewer_zoom_center_x_sync') - self.viewer_zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y', # noqa - 'viewer_zoom_center_y_value', - 'viewer_zoom_center_y_sync') - self.viewer_zoom_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', - 'viewer_zoom_level_value', - 'viewer_zoom_level_sync') + 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_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', + 'zoom_level_value', 'zoom_level_sync') # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) @@ -638,6 +626,13 @@ 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) + # give UI access to sampled version of the available colormap choices def hex_for_cmap(cmap): N = 50 @@ -652,10 +647,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_level', + 'subset_color', 'subset_opacity', 'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax', 'stretch_hist_zoom_limits', 'stretch_hist_nbins', 'image_visible', 'image_color_mode', @@ -704,6 +701,12 @@ 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 vue_unmix_state(self, names): if isinstance(names, str): names = [names] @@ -774,8 +777,9 @@ 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 @@ -785,25 +789,16 @@ def _update_viewer_bound_steps(self, msg={}): # 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): - # 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) + for ax in ('x', 'y'): + ax_min = getattr(self, f'{ax}_min_value') + ax_max = getattr(self, f'{ax}_max_value') + bound_step = (ax_max - ax_min) / 100. # noqa + decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 + if decimals < 0: + decimals = 0 + setattr(self, f'{ax}_bound_step', np.round(bound_step, decimals=decimals)) + setattr(self, f'{ax}_min_value', np.round(ax_min, decimals=decimals)) + setattr(self, f'{ax}_max_value', np.round(ax_max, decimals=decimals)) 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 3f3bd05d1c..20e09542ae 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -46,69 +46,69 @@ Viewer bounds - + - + - + - + - + - + - + From 73e0fdd0083e492539a10eb06d3377bd3ec5e035 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 8 Jan 2024 13:59:21 -0500 Subject: [PATCH 05/23] add to existing changelog entry --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d23f107741..8dc46adfe3 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] From 22651379e35663d2a5070a5aec95047c4601fd9d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 9 Jan 2024 12:22:12 -0500 Subject: [PATCH 06/23] use original center logic --- jdaviz/core/freezable_state.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index ea4a21e233..7905e026b6 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -102,16 +102,14 @@ def _set_zoom_center(self, *args): if self._during_zoom_sync: return - cur_xcen = (self.x_min + self.x_max) * 0.5 - cur_ycen = (self.y_min + self.y_max) * 0.5 - delta_x = self.zoom_center_x - cur_xcen - delta_y = self.zoom_center_y - cur_ycen + width = self.x_max - self.x_min + height = self.y_max - self.y_min with self.during_zoom_sync(): - self.x_min += delta_x - self.x_max += delta_x - self.y_min += delta_y - self.y_max += delta_y + self.x_min = self.zoom_center_x - (width * 0.5) + self.y_min = self.zoom_center_y - (height * 0.5) + self.x_max = self.x_min + width + self.y_max = self.y_min + height def _set_axes_lim(self, *args): if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: From 283c996891d1c3cf402688fa649904d17f9bcd11 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 9 Jan 2024 12:27:45 -0500 Subject: [PATCH 07/23] set callbacks before initializing state --- jdaviz/core/freezable_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 7905e026b6..a13124480a 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -60,7 +60,6 @@ class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): 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_level', self._set_zoom_level) @@ -68,6 +67,7 @@ def __init__(self, *args, **kwargs): self.add_callback('zoom_center_y', self._set_zoom_center) for attr in ('x_min', 'x_max', 'y_min', 'y_max'): self.add_callback(attr, self._set_axes_lim) + super().__init__(*args, **kwargs) @contextmanager def during_zoom_sync(self): From 6f5117679f3fd460213a1d9a4d3b50cba5d1d0f5 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 10 Jan 2024 11:56:39 -0500 Subject: [PATCH 08/23] fix initializing zoom-level/center --- jdaviz/core/freezable_state.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index a13124480a..860da28f64 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -111,9 +111,17 @@ def _set_zoom_center(self, *args): self.x_max = self.x_min + width self.y_max = self.y_min + height + 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 screenx = self._viewer.shape[1] screeny = self._viewer.shape[0] From 6caa8d6d6f8361edbb988aa4d0e48a4b26f7ede6 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 10 Jan 2024 13:37:56 -0500 Subject: [PATCH 09/23] Revert "use original center logic" This reverts commit 16d0ee8ee31efdf764f23d4eee96aabda9439bd0. --- jdaviz/core/freezable_state.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 860da28f64..369d67ced9 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -102,14 +102,16 @@ def _set_zoom_center(self, *args): if self._during_zoom_sync: return - width = self.x_max - self.x_min - height = self.y_max - self.y_min + cur_xcen = (self.x_min + self.x_max) * 0.5 + cur_ycen = (self.y_min + self.y_max) * 0.5 + delta_x = self.zoom_center_x - cur_xcen + delta_y = self.zoom_center_y - cur_ycen with self.during_zoom_sync(): - self.x_min = self.zoom_center_x - (width * 0.5) - self.y_min = self.zoom_center_y - (height * 0.5) - self.x_max = self.x_min + width - self.y_max = self.y_min + height + self.x_min += delta_x + self.x_max += delta_x + self.y_min += delta_y + self.y_max += delta_y def _set_axes_aspect_ratio(self, axes_ratio): # when aspect-ratio is changed (changing viewer.shape), ensure zoom/center are synced From 94dd0321e1528723666d77131952747f06efc467 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 10 Jan 2024 13:43:02 -0500 Subject: [PATCH 10/23] fix astrowidgets center_on * required the new center logic along with fixing this bug that was missed when splitting zoom_center to zoom_center_x/y --- jdaviz/core/astrowidgets_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index fcc97c4fc2..452371f432 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -102,7 +102,9 @@ def center_on(self, point): else: # pragma: no cover pix = point - self.state.zoom_center = pix + with delay_callback(self.state, 'zoom_center_x', 'zoom_center_y'): + self.state.zoom_center_x = pix[0] + self.state.zoom_center_y = pix[1] def offset_by(self, dx, dy): """Move the center to a point that is given offset From 689e5a6bcd47c45896e506029e9982114568cf3e Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 10 Jan 2024 14:04:25 -0500 Subject: [PATCH 11/23] consolidate code logic --- jdaviz/core/freezable_state.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 369d67ced9..bb4a787587 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -74,6 +74,9 @@ def during_zoom_sync(self): self._during_zoom_sync = True try: yield + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self._adjust_limits_aspect() except Exception: self._during_zoom_sync = False raise @@ -94,10 +97,6 @@ def _set_zoom_level(self, zoom_level): self.x_min = new_x_min - 0.5 self.x_max = new_x_max - 0.5 - # We need to adjust the limits in here to avoid triggering all - # the update events then changing the limits again. - self._adjust_limits_aspect() - def _set_zoom_center(self, *args): if self._during_zoom_sync: return @@ -181,11 +180,8 @@ def reset_limits(self, *event): 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) - with delay_callback(self, 'x_min', 'x_max', 'y_min', 'y_max'): + with self.during_zoom_sync(): self.x_min = x_min self.x_max = x_max self.y_min = y_min self.y_max = 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() From 6c7e2fef1fb8c403640a71ec066a684c61124cd9 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 Jan 2024 09:45:39 -0500 Subject: [PATCH 12/23] do not use custom reset_limits for non-imviz image viewers --- jdaviz/core/freezable_state.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index bb4a787587..d0dd770a07 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -137,6 +137,10 @@ def _set_axes_lim(self, *args): self.zoom_center_y = center_y 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 @@ -176,9 +180,11 @@ 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) with self.during_zoom_sync(): self.x_min = x_min From 36c4003c5af57288d3c41abbec87d21ccdead146 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 Jan 2024 10:12:08 -0500 Subject: [PATCH 13/23] do not allow zoom_level <= 0 --- jdaviz/core/template_mixin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 61761de605..b3810864fe 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -3417,6 +3417,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']) From ea78dd53f492ff1e4325203f25574063c81a6902 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 Jan 2024 10:29:01 -0500 Subject: [PATCH 14/23] Revert "consolidate code logic" This reverts commit 06493961f21029be13d42022dda83991071765d4. --- jdaviz/core/freezable_state.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index d0dd770a07..8c50825ceb 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -74,9 +74,6 @@ def during_zoom_sync(self): self._during_zoom_sync = True try: yield - # We need to adjust the limits in here to avoid triggering all - # the update events then changing the limits again. - self._adjust_limits_aspect() except Exception: self._during_zoom_sync = False raise @@ -97,6 +94,10 @@ def _set_zoom_level(self, zoom_level): self.x_min = new_x_min - 0.5 self.x_max = new_x_max - 0.5 + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self._adjust_limits_aspect() + def _set_zoom_center(self, *args): if self._during_zoom_sync: return @@ -186,8 +187,11 @@ def reset_limits(self, *event): 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) - with self.during_zoom_sync(): + 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 + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self._adjust_limits_aspect() From 48fa6b4a2d1d78e3926c2465ada2d4d02c3eaa16 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 Jan 2024 12:25:09 -0500 Subject: [PATCH 15/23] enable for mosviz --- jdaviz/configs/mosviz/plugins/viewers.py | 1 + 1 file changed, 1 insertion(+) 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) From fc900b2358da1ad01b06e2a4094a266875d5fc41 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 24 Jan 2024 13:50:00 -0500 Subject: [PATCH 16/23] remove unused import introduced during rebase --- jdaviz/core/astrowidgets_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index 452371f432..66fe81f63a 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -9,7 +9,7 @@ from glue.config import colormaps from glue.core import Data -from jdaviz.configs.imviz.helper import get_top_layer_index, get_reference_image_data +from jdaviz.configs.imviz.helper import get_top_layer_index from jdaviz.core.events import SnackbarMessage, AstrowidgetMarkersChangedMessage from jdaviz.core.helpers import data_has_valid_wcs From 86de264d46ae968d34c7582aba68c0bc77c4e0c8 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 1 Feb 2024 14:25:35 -0500 Subject: [PATCH 17/23] small styling tweak to reset limits button in plot options --- .../default/plugins/plot_options/plot_options.vue | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index 20e09542ae..ff2e61ff73 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -113,12 +113,14 @@ :step="0.1" /> - - Reset viewer bounds - + + + Reset viewer bounds + + From 5468fb111f52f9c04683eb4596b7ef90192fbf66 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 24 Jan 2024 14:38:44 -0500 Subject: [PATCH 18/23] expose zoom-radius instead of zoom-level in state/plot options --- .../plugins/plot_options/plot_options.py | 10 +-- .../plugins/plot_options/plot_options.vue | 8 +- jdaviz/core/astrowidgets_api.py | 43 +++++++++- jdaviz/core/freezable_state.py | 81 ++++++++++--------- 4 files changed, 90 insertions(+), 52 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index af88844974..dd92409c0a 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -231,8 +231,8 @@ class PlotOptions(PluginTemplateMixin): zoom_center_y_value = Float().tag(sync=True) zoom_center_y_sync = Dict().tag(sync=True) - zoom_level_value = Float().tag(sync=True) - zoom_level_sync = Dict().tag(sync=True) + zoom_radius_value = Float().tag(sync=True) + zoom_radius_sync = Dict().tag(sync=True) # scatter/marker options marker_visible_value = Bool().tag(sync=True) @@ -465,8 +465,8 @@ def state_attr_for_line_visible(state): '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_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level', - 'zoom_level_value', 'zoom_level_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) @@ -651,7 +651,7 @@ def user_api(self): 'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity', 'line_as_steps', 'uncertainty_visible'] if self.config != "specviz": - expose += ['zoom_center_x', 'zoom_center_y', 'zoom_level', + 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', diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index ff2e61ff73..5c98c289a3 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -104,11 +104,11 @@ :step="0.1" /> - + diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index 66fe81f63a..09aa5220c5 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -9,7 +9,7 @@ from glue.config import colormaps from glue.core import Data -from jdaviz.configs.imviz.helper import get_top_layer_index +from jdaviz.configs.imviz.helper import get_top_layer_index, get_reference_image_data from jdaviz.core.events import SnackbarMessage, AstrowidgetMarkersChangedMessage from jdaviz.core.helpers import data_has_valid_wcs @@ -177,7 +177,22 @@ def zoom_level(self): if self.shape is None: # pragma: no cover raise ValueError('Viewer is still loading, try again later') - return self.state.zoom_level + if hasattr(self, '_get_real_xy'): + image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) + # TODO: Do we want top layer instead? + # i_top = get_top_layer_index(self) + # image = self.layers[i_top].layer + real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) + real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) + else: + real_min = (self.state.x_min, self.state.y_min) + real_max = (self.state.x_max, self.state.y_max) + screenx = self.shape[1] + screeny = self.shape[0] + zoom_x = screenx / abs(real_max[0] - real_min[0]) + zoom_y = screeny / abs(real_max[1] - real_min[1]) + + return max(zoom_x, zoom_y) # Similar to Ginga get_scale() # Loosely based on glue/viewers/image/state.py @zoom_level.setter @@ -195,7 +210,29 @@ def zoom_level(self, val): self.state.reset_limits() return - self.state.zoom_level = val + new_dx = self.shape[1] * 0.5 / val + if hasattr(self, '_get_real_xy'): + image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference) + # TODO: Do we want top layer instead? + # i_top = get_top_layer_index(self) + # image = self.layers[i_top].layer + real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min) + real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max) + cur_xcen = (real_min[0] + real_max[0]) * 0.5 + new_x_min = self._get_real_xy(image, cur_xcen - new_dx - 0.5, real_min[1], reverse=True)[0] # noqa: E501 + new_x_max = self._get_real_xy(image, cur_xcen + new_dx - 0.5, real_max[1], reverse=True)[0] # noqa: E501 + else: + cur_xcen = (self.state.x_min + self.state.x_max) * 0.5 + new_x_min = cur_xcen - new_dx - 0.5 + new_x_max = cur_xcen + new_dx - 0.5 + + with delay_callback(self.state, 'x_min', 'x_max'): + self.state.x_min = new_x_min + self.state.x_max = new_x_max + + # We need to adjust the limits in here to avoid triggering all + # the update events then changing the limits again. + self.state._adjust_limits_aspect() # Discussion on why we need two different ways to set zoom at # https://github.com/astropy/astrowidgets/issues/144 diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 8c50825ceb..977133efd4 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -6,6 +6,8 @@ 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'] @@ -55,16 +57,16 @@ def _reset_x_limits(self, *event): class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState): linked_by_wcs = False - zoom_level = CallbackProperty(1.0, docstring='Zoom-level') + 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): self.wcs_only_layers = [] # For Imviz rotation use. self._during_zoom_sync = False - self.add_callback('zoom_level', self._set_zoom_level) - self.add_callback('zoom_center_x', self._set_zoom_center) - self.add_callback('zoom_center_y', self._set_zoom_center) + 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) @@ -79,40 +81,34 @@ def during_zoom_sync(self): raise self._during_zoom_sync = False - def _set_zoom_level(self, zoom_level): + def _set_zoom_radius_center(self, *args): if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: return - if zoom_level <= 0.0: - raise ValueError("zoom_level must be positive") - - cur_xcen = (self.x_min + self.x_max) * 0.5 - new_dx = self._viewer.shape[1] * 0.5 / zoom_level - new_x_min = cur_xcen - new_dx - new_x_max = cur_xcen + new_dx + if self.zoom_radius <= 0.0: + raise ValueError("zoom_radius must be positive") + + # 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 + center_x, center_y = ref_wcs.world_to_pixel_values(self.zoom_center_x, self.zoom_center_y) # noqa + center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+self.zoom_radius, self.zoom_center_y) # noqa + radius = abs(center_xr - center_x) + else: + center_x, center_y = self.zoom_center_x, self.zoom_center_y + radius = 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(): - self.x_min = new_x_min - 0.5 - self.x_max = new_x_max - 0.5 + self.x_min = center_x - radius + self.x_max = center_x + radius + self.y_min = center_y - radius + self.y_max = center_y + radius - # We need to adjust the limits in here to avoid triggering all - # the update events then changing the limits again. self._adjust_limits_aspect() - def _set_zoom_center(self, *args): - if self._during_zoom_sync: - return - - cur_xcen = (self.x_min + self.x_max) * 0.5 - cur_ycen = (self.y_min + self.y_max) * 0.5 - delta_x = self.zoom_center_x - cur_xcen - delta_y = self.zoom_center_y - cur_ycen - - with self.during_zoom_sync(): - self.x_min += delta_x - self.x_max += delta_x - self.y_min += delta_y - self.y_max += delta_y - def _set_axes_aspect_ratio(self, axes_ratio): # when aspect-ratio is changed (changing viewer.shape), ensure zoom/center are synced # with zoom-limits @@ -125,17 +121,22 @@ def _set_axes_lim(self, *args): if None in (self.x_min, self.x_max, self.y_min, self.y_max): return - screenx = self._viewer.shape[1] - screeny = self._viewer.shape[0] - zoom_x = screenx / (self.x_max - self.x_min) - zoom_y = screeny / (self.y_max - self.y_min) - center_x = 0.5 * (self.x_max + self.x_min) - center_y = 0.5 * (self.y_max + self.y_min) + # 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 + x_min, y_min = ref_wcs.pixel_to_world_values(self.x_min, self.y_min) + x_max, y_max = ref_wcs.pixel_to_world_values(self.x_max, self.y_max) + 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_level = max(zoom_x, zoom_y) # Similar to Ginga get_scale() - self.zoom_center_x = center_x - self.zoom_center_y = center_y + 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 reset_limits(self, *event): # TODO: use consistent logic for all image viewers by removing this if-statement From 89d3bc03652484a7cf37094054b00de63ade7f0c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 2 Feb 2024 15:27:00 -0500 Subject: [PATCH 19/23] dynamic zoom step-size and units * NOTE: widget will not respect step until upstream PR is merged/pinned, but can be accessed (for testing purposes) with plot_options._obj.zoom_step --- .../plugins/plot_options/plot_options.py | 50 +++++++++++++++-- .../plugins/plot_options/plot_options.vue | 13 +++-- jdaviz/core/freezable_state.py | 55 +++++++++++-------- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index dd92409c0a..85d809a472 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -24,6 +24,7 @@ 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 @@ -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): """ @@ -234,6 +245,8 @@ class PlotOptions(PluginTemplateMixin): 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) marker_visible_sync = Dict().tag(sync=True) @@ -633,6 +646,9 @@ def state_attr_for_line_visible(state): 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 @@ -707,6 +723,14 @@ def _on_global_display_unit_changed(self, *args): 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] @@ -785,21 +809,37 @@ def _update_viewer_bound_steps(self, msg={}): # 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 for ax in ('x', 'y'): ax_min = getattr(self, f'{ax}_min_value') ax_max = getattr(self, f'{ax}_max_value') - bound_step = (ax_max - ax_min) / 100. # noqa + bound_step, decimals = _round_step((ax_max - ax_min) / 100.) decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 - if decimals < 0: - decimals = 0 - setattr(self, f'{ax}_bound_step', np.round(bound_step, decimals=decimals)) + 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 + + # in the case of multiple viewers, calculate based on the first + # alternatively, we could find the most extreme by looping over all selected viewers + viewer = self.viewer.selected_obj[0] if self.viewer_multiselect else self.viewer.selected_obj # noqa + 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 viewers = [self.viewer.selected_obj] if not self.viewer_multiselect else self.viewer.selected_obj # noqa diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.vue b/jdaviz/configs/default/plugins/plot_options/plot_options.vue index 5c98c289a3..aacddd0f99 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.vue +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.vue @@ -89,19 +89,21 @@ @@ -110,7 +112,8 @@ label="Zoom-radius" :value.sync="zoom_radius_value" type="number" - :step="0.1" + :step="zoom_step" + :suffix="display_units['image'] || 'pix'" /> diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 977133efd4..227d2728ac 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -102,10 +102,11 @@ def _set_zoom_radius_center(self, *args): # update limits with self.during_zoom_sync(): - self.x_min = center_x - radius - self.x_max = center_x + radius - self.y_min = center_y - radius - self.y_max = center_y + radius + 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() @@ -138,14 +139,7 @@ def _set_axes_lim(self, *args): self.zoom_center_x = 0.5 * (x_max + x_min) self.zoom_center_y = 0.5 * (y_max + y_min) - 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 - + 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 @@ -166,13 +160,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: @@ -188,11 +188,20 @@ def reset_limits(self, *event): 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() From dcaa2e4abb35145011b0d0ff774a63d620136078 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 2 Feb 2024 15:37:35 -0500 Subject: [PATCH 20/23] gracefully handle negative zoom radius --- jdaviz/core/freezable_state.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 227d2728ac..e4661de842 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -84,8 +84,6 @@ def during_zoom_sync(self): def _set_zoom_radius_center(self, *args): if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None: return - if self.zoom_radius <= 0.0: - raise ValueError("zoom_radius must be positive") # 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 @@ -93,11 +91,11 @@ def _set_zoom_radius_center(self, *args): image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) ref_wcs = image.coords center_x, center_y = ref_wcs.world_to_pixel_values(self.zoom_center_x, self.zoom_center_y) # noqa - center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+self.zoom_radius, self.zoom_center_y) # noqa + center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+abs(self.zoom_radius), self.zoom_center_y) # noqa radius = abs(center_xr - center_x) else: center_x, center_y = self.zoom_center_x, self.zoom_center_y - radius = self.zoom_radius + 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 From 903a12ac15690e852e12ce26b13b6c6ecb340f5d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Feb 2024 08:42:22 -0500 Subject: [PATCH 21/23] smarter viewer logic when setting zoom step --- .../configs/default/plugins/plot_options/plot_options.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 85d809a472..070b5b93ce 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -836,7 +836,13 @@ def _update_viewer_zoom_steps(self, msg={}): # in the case of multiple viewers, calculate based on the first # alternatively, we could find the most extreme by looping over all selected viewers - viewer = self.viewer.selected_obj[0] if self.viewer_multiselect else self.viewer.selected_obj # noqa + 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.) From ddf3c0d902766377389018a80807c06cc1c03690 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Feb 2024 08:43:28 -0500 Subject: [PATCH 22/23] revert center_on logic --- jdaviz/core/astrowidgets_api.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/jdaviz/core/astrowidgets_api.py b/jdaviz/core/astrowidgets_api.py index 09aa5220c5..bd4504e6e9 100644 --- a/jdaviz/core/astrowidgets_api.py +++ b/jdaviz/core/astrowidgets_api.py @@ -102,9 +102,14 @@ def center_on(self, point): else: # pragma: no cover pix = point - with delay_callback(self.state, 'zoom_center_x', 'zoom_center_y'): - self.state.zoom_center_x = pix[0] - self.state.zoom_center_y = pix[1] + width = self.state.x_max - self.state.x_min + height = self.state.y_max - self.state.y_min + + with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'): + self.state.x_min = pix[0] - (width * 0.5) + self.state.y_min = pix[1] - (height * 0.5) + self.state.x_max = self.state.x_min + width + self.state.y_max = self.state.y_min + height def offset_by(self, dx, dy): """Move the center to a point that is given offset From e380a96288e55547cc6e43d23f8a4412e9e67cc6 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Feb 2024 12:54:22 -0500 Subject: [PATCH 23/23] optimize world_to_pixel_values calls --- jdaviz/core/freezable_state.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index e4661de842..5f77e59222 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -90,8 +90,10 @@ def _set_zoom_radius_center(self, *args): if self.linked_by_wcs: image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) ref_wcs = image.coords - center_x, center_y = ref_wcs.world_to_pixel_values(self.zoom_center_x, self.zoom_center_y) # noqa - center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+abs(self.zoom_radius), self.zoom_center_y) # noqa + 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 @@ -125,8 +127,9 @@ def _set_axes_lim(self, *args): if self.linked_by_wcs: image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference) ref_wcs = image.coords - x_min, y_min = ref_wcs.pixel_to_world_values(self.x_min, self.y_min) - x_max, y_max = ref_wcs.pixel_to_world_values(self.x_max, self.y_max) + 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