Skip to content

Commit

Permalink
expose zoom-radius instead of zoom-level in state/plot options
Browse files Browse the repository at this point in the history
  • Loading branch information
kecnry committed Feb 1, 2024
1 parent 86de264 commit 5468fb1
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 52 deletions.
10 changes: 5 additions & 5 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions jdaviz/configs/default/plugins/plot_options/plot_options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@
:step="0.1"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="zoom_level_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_level')">
<glue-state-sync-wrapper :sync="zoom_radius_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_radius')">
<glue-float-field
ref="zoom_level"
label="Zoom-Level"
:value.sync="zoom_level_value"
ref="zoom_radius"
label="Zoom-radius"
:value.sync="zoom_radius_value"
type="number"
:step="0.1"
/>
Expand Down
43 changes: 40 additions & 3 deletions jdaviz/core/astrowidgets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
81 changes: 41 additions & 40 deletions jdaviz/core/freezable_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']


Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 5468fb1

Please sign in to comment.