Skip to content

Commit

Permalink
dynamic zoom step-size and units
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kecnry committed Feb 2, 2024
1 parent 5468fb1 commit 89d3bc0
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 33 deletions.
50 changes: 45 additions & 5 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions jdaviz/configs/default/plugins/plot_options/plot_options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,21 @@
<glue-state-sync-wrapper :sync="zoom_center_x_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_center_x')">
<glue-float-field
ref="zoom_center_x"
label="Center (x)"
label="X Center"
:value.sync="zoom_center_x_value"
type="number"
:step="0.1"
:step="zoom_step"
:suffix="display_units['image'] || 'pix'"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="zoom_center_y_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_center_y')">
<glue-float-field
ref="zoom_center_y"
label="Center (y)"
label="Y Center"
:value.sync="zoom_center_y_value"
type="number"
:step="0.1"
:step="zoom_step"
:suffix="display_units['image'] || 'pix'"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="zoom_radius_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('zoom_radius')">
Expand All @@ -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'"
/>
</glue-state-sync-wrapper>
<v-row justify="end">
Expand Down
55 changes: 32 additions & 23 deletions jdaviz/core/freezable_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

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

0 comments on commit 89d3bc0

Please sign in to comment.