Skip to content

Commit 5468fb1

Browse files
committed
expose zoom-radius instead of zoom-level in state/plot options
1 parent 86de264 commit 5468fb1

File tree

4 files changed

+90
-52
lines changed

4 files changed

+90
-52
lines changed

jdaviz/configs/default/plugins/plot_options/plot_options.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ class PlotOptions(PluginTemplateMixin):
231231
zoom_center_y_value = Float().tag(sync=True)
232232
zoom_center_y_sync = Dict().tag(sync=True)
233233

234-
zoom_level_value = Float().tag(sync=True)
235-
zoom_level_sync = Dict().tag(sync=True)
234+
zoom_radius_value = Float().tag(sync=True)
235+
zoom_radius_sync = Dict().tag(sync=True)
236236

237237
# scatter/marker options
238238
marker_visible_value = Bool().tag(sync=True)
@@ -465,8 +465,8 @@ def state_attr_for_line_visible(state):
465465
'zoom_center_x_value', 'zoom_center_x_sync')
466466
self.zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y',
467467
'zoom_center_y_value', 'zoom_center_y_sync')
468-
self.zoom_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_level',
469-
'zoom_level_value', 'zoom_level_sync')
468+
self.zoom_radius = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_radius',
469+
'zoom_radius_value', 'zoom_radius_sync')
470470

471471
# Scatter/marker options:
472472
# NOTE: marker_visible hides the entire layer (including the line)
@@ -651,7 +651,7 @@ def user_api(self):
651651
'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity',
652652
'line_as_steps', 'uncertainty_visible']
653653
if self.config != "specviz":
654-
expose += ['zoom_center_x', 'zoom_center_y', 'zoom_level',
654+
expose += ['zoom_center_x', 'zoom_center_y', 'zoom_radius',
655655
'subset_color', 'subset_opacity',
656656
'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax',
657657
'stretch_hist_zoom_limits', 'stretch_hist_nbins',

jdaviz/configs/default/plugins/plot_options/plot_options.vue

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@
104104
:step="0.1"
105105
/>
106106
</glue-state-sync-wrapper>
107-
<glue-state-sync-wrapper :sync="zoom_level_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_level')">
107+
<glue-state-sync-wrapper :sync="zoom_radius_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_radius')">
108108
<glue-float-field
109-
ref="zoom_level"
110-
label="Zoom-Level"
111-
:value.sync="zoom_level_value"
109+
ref="zoom_radius"
110+
label="Zoom-radius"
111+
:value.sync="zoom_radius_value"
112112
type="number"
113113
:step="0.1"
114114
/>

jdaviz/core/astrowidgets_api.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from glue.config import colormaps
1010
from glue.core import Data
1111

12-
from jdaviz.configs.imviz.helper import get_top_layer_index
12+
from jdaviz.configs.imviz.helper import get_top_layer_index, get_reference_image_data
1313
from jdaviz.core.events import SnackbarMessage, AstrowidgetMarkersChangedMessage
1414
from jdaviz.core.helpers import data_has_valid_wcs
1515

@@ -177,7 +177,22 @@ def zoom_level(self):
177177
if self.shape is None: # pragma: no cover
178178
raise ValueError('Viewer is still loading, try again later')
179179

180-
return self.state.zoom_level
180+
if hasattr(self, '_get_real_xy'):
181+
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference)
182+
# TODO: Do we want top layer instead?
183+
# i_top = get_top_layer_index(self)
184+
# image = self.layers[i_top].layer
185+
real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min)
186+
real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max)
187+
else:
188+
real_min = (self.state.x_min, self.state.y_min)
189+
real_max = (self.state.x_max, self.state.y_max)
190+
screenx = self.shape[1]
191+
screeny = self.shape[0]
192+
zoom_x = screenx / abs(real_max[0] - real_min[0])
193+
zoom_y = screeny / abs(real_max[1] - real_min[1])
194+
195+
return max(zoom_x, zoom_y) # Similar to Ginga get_scale()
181196

182197
# Loosely based on glue/viewers/image/state.py
183198
@zoom_level.setter
@@ -195,7 +210,29 @@ def zoom_level(self, val):
195210
self.state.reset_limits()
196211
return
197212

198-
self.state.zoom_level = val
213+
new_dx = self.shape[1] * 0.5 / val
214+
if hasattr(self, '_get_real_xy'):
215+
image, i_ref = get_reference_image_data(self.jdaviz_app, self.reference)
216+
# TODO: Do we want top layer instead?
217+
# i_top = get_top_layer_index(self)
218+
# image = self.layers[i_top].layer
219+
real_min = self._get_real_xy(image, self.state.x_min, self.state.y_min)
220+
real_max = self._get_real_xy(image, self.state.x_max, self.state.y_max)
221+
cur_xcen = (real_min[0] + real_max[0]) * 0.5
222+
new_x_min = self._get_real_xy(image, cur_xcen - new_dx - 0.5, real_min[1], reverse=True)[0] # noqa: E501
223+
new_x_max = self._get_real_xy(image, cur_xcen + new_dx - 0.5, real_max[1], reverse=True)[0] # noqa: E501
224+
else:
225+
cur_xcen = (self.state.x_min + self.state.x_max) * 0.5
226+
new_x_min = cur_xcen - new_dx - 0.5
227+
new_x_max = cur_xcen + new_dx - 0.5
228+
229+
with delay_callback(self.state, 'x_min', 'x_max'):
230+
self.state.x_min = new_x_min
231+
self.state.x_max = new_x_max
232+
233+
# We need to adjust the limits in here to avoid triggering all
234+
# the update events then changing the limits again.
235+
self.state._adjust_limits_aspect()
199236

200237
# Discussion on why we need two different ways to set zoom at
201238
# https://github.com/astropy/astrowidgets/issues/144

jdaviz/core/freezable_state.py

+41-40
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from glue_jupyter.bqplot.image.state import BqplotImageViewerState
77
from glue.viewers.matplotlib.state import DeferredDrawCallbackProperty as DDCProperty
88

9+
from jdaviz.configs.imviz.helper import get_reference_image_data
10+
911
__all__ = ['FreezableState', 'FreezableProfileViewerState', 'FreezableBqplotImageViewerState']
1012

1113

@@ -55,16 +57,16 @@ def _reset_x_limits(self, *event):
5557
class FreezableBqplotImageViewerState(BqplotImageViewerState, FreezableState):
5658
linked_by_wcs = False
5759

58-
zoom_level = CallbackProperty(1.0, docstring='Zoom-level')
60+
zoom_radius = CallbackProperty(1.0, docstring="Zoom radius")
5961
zoom_center_x = CallbackProperty(0.0, docstring='x-coordinate of center of zoom box')
6062
zoom_center_y = CallbackProperty(0.0, docstring='y-coordinate of center of zoom box')
6163

6264
def __init__(self, *args, **kwargs):
6365
self.wcs_only_layers = [] # For Imviz rotation use.
6466
self._during_zoom_sync = False
65-
self.add_callback('zoom_level', self._set_zoom_level)
66-
self.add_callback('zoom_center_x', self._set_zoom_center)
67-
self.add_callback('zoom_center_y', self._set_zoom_center)
67+
self.add_callback('zoom_radius', self._set_zoom_radius_center)
68+
self.add_callback('zoom_center_x', self._set_zoom_radius_center)
69+
self.add_callback('zoom_center_y', self._set_zoom_radius_center)
6870
for attr in ('x_min', 'x_max', 'y_min', 'y_max'):
6971
self.add_callback(attr, self._set_axes_lim)
7072
super().__init__(*args, **kwargs)
@@ -79,40 +81,34 @@ def during_zoom_sync(self):
7981
raise
8082
self._during_zoom_sync = False
8183

82-
def _set_zoom_level(self, zoom_level):
84+
def _set_zoom_radius_center(self, *args):
8385
if self._during_zoom_sync or not hasattr(self, '_viewer') or self._viewer.shape is None:
8486
return
85-
if zoom_level <= 0.0:
86-
raise ValueError("zoom_level must be positive")
87-
88-
cur_xcen = (self.x_min + self.x_max) * 0.5
89-
new_dx = self._viewer.shape[1] * 0.5 / zoom_level
90-
new_x_min = cur_xcen - new_dx
91-
new_x_max = cur_xcen + new_dx
87+
if self.zoom_radius <= 0.0:
88+
raise ValueError("zoom_radius must be positive")
89+
90+
# When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units,
91+
# x/y_min/max are in pixels of the WCS-only layer
92+
if self.linked_by_wcs:
93+
image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference)
94+
ref_wcs = image.coords
95+
center_x, center_y = ref_wcs.world_to_pixel_values(self.zoom_center_x, self.zoom_center_y) # noqa
96+
center_xr, center_yr = ref_wcs.world_to_pixel_values(self.zoom_center_x+self.zoom_radius, self.zoom_center_y) # noqa
97+
radius = abs(center_xr - center_x)
98+
else:
99+
center_x, center_y = self.zoom_center_x, self.zoom_center_y
100+
radius = self.zoom_radius
101+
# now center_x/y and radius are in pixel units of the reference data, so can be used to
102+
# update limits
92103

93104
with self.during_zoom_sync():
94-
self.x_min = new_x_min - 0.5
95-
self.x_max = new_x_max - 0.5
105+
self.x_min = center_x - radius
106+
self.x_max = center_x + radius
107+
self.y_min = center_y - radius
108+
self.y_max = center_y + radius
96109

97-
# We need to adjust the limits in here to avoid triggering all
98-
# the update events then changing the limits again.
99110
self._adjust_limits_aspect()
100111

101-
def _set_zoom_center(self, *args):
102-
if self._during_zoom_sync:
103-
return
104-
105-
cur_xcen = (self.x_min + self.x_max) * 0.5
106-
cur_ycen = (self.y_min + self.y_max) * 0.5
107-
delta_x = self.zoom_center_x - cur_xcen
108-
delta_y = self.zoom_center_y - cur_ycen
109-
110-
with self.during_zoom_sync():
111-
self.x_min += delta_x
112-
self.x_max += delta_x
113-
self.y_min += delta_y
114-
self.y_max += delta_y
115-
116112
def _set_axes_aspect_ratio(self, axes_ratio):
117113
# when aspect-ratio is changed (changing viewer.shape), ensure zoom/center are synced
118114
# with zoom-limits
@@ -125,17 +121,22 @@ def _set_axes_lim(self, *args):
125121
if None in (self.x_min, self.x_max, self.y_min, self.y_max):
126122
return
127123

128-
screenx = self._viewer.shape[1]
129-
screeny = self._viewer.shape[0]
130-
zoom_x = screenx / (self.x_max - self.x_min)
131-
zoom_y = screeny / (self.y_max - self.y_min)
132-
center_x = 0.5 * (self.x_max + self.x_min)
133-
center_y = 0.5 * (self.y_max + self.y_min)
124+
# When WCS-linked (displayed on the sky): zoom_center_x/y and zoom_radius are in sky units,
125+
# x/y_min/max are in pixels of the WCS-only layer
126+
if self.linked_by_wcs:
127+
image, i_ref = get_reference_image_data(self._viewer.jdaviz_app, self._viewer.reference)
128+
ref_wcs = image.coords
129+
x_min, y_min = ref_wcs.pixel_to_world_values(self.x_min, self.y_min)
130+
x_max, y_max = ref_wcs.pixel_to_world_values(self.x_max, self.y_max)
131+
else:
132+
x_min, y_min = self.x_min, self.y_min
133+
x_max, y_max = self.x_max, self.y_max
134+
# now x_min/max, y_min/max are in axes units (degrees if WCS-linked, pixels otherwise)
134135

135136
with self.during_zoom_sync():
136-
self.zoom_level = max(zoom_x, zoom_y) # Similar to Ginga get_scale()
137-
self.zoom_center_x = center_x
138-
self.zoom_center_y = center_y
137+
self.zoom_radius = abs(0.5 * min(x_max - x_min, y_max - y_min))
138+
self.zoom_center_x = 0.5 * (x_max + x_min)
139+
self.zoom_center_y = 0.5 * (y_max + y_min)
139140

140141
def reset_limits(self, *event):
141142
# TODO: use consistent logic for all image viewers by removing this if-statement

0 commit comments

Comments
 (0)