Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

image viewer zoom-level/center #2649

Merged
merged 23 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cdbc668
move zoom-level and center logic to image viewer state
kecnry Jan 5, 2024
1e1a63a
expose zoom-level in plot options plugin
kecnry Jan 8, 2024
0227f10
split state into center_x/y and expose in plot options
kecnry Jan 8, 2024
8b28ce2
consolidate and expose to user API
kecnry Jan 8, 2024
73e0fdd
add to existing changelog entry
kecnry Jan 8, 2024
2265137
use original center logic
kecnry Jan 9, 2024
283c996
set callbacks before initializing state
kecnry Jan 9, 2024
6f51176
fix initializing zoom-level/center
kecnry Jan 10, 2024
6caa8d6
Revert "use original center logic"
kecnry Jan 10, 2024
94dd032
fix astrowidgets center_on
kecnry Jan 10, 2024
689e5a6
consolidate code logic
kecnry Jan 10, 2024
6c7e2fe
do not use custom reset_limits for non-imviz image viewers
kecnry Jan 11, 2024
36c4003
do not allow zoom_level <= 0
kecnry Jan 11, 2024
ea78dd5
Revert "consolidate code logic"
kecnry Jan 11, 2024
48fa6b4
enable for mosviz
kecnry Jan 11, 2024
fc900b2
remove unused import introduced during rebase
kecnry Jan 24, 2024
86de264
small styling tweak to reset limits button in plot options
kecnry Feb 1, 2024
5468fb1
expose zoom-radius instead of zoom-level in state/plot options
kecnry Jan 24, 2024
89d3bc0
dynamic zoom step-size and units
kecnry Feb 2, 2024
dcaa2e4
gracefully handle negative zoom radius
kecnry Feb 2, 2024
903a12a
smarter viewer logic when setting zoom step
kecnry Feb 5, 2024
ddf3c0d
revert center_on logic
kecnry Feb 5, 2024
e380a96
optimize world_to_pixel_values calls
kecnry Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
6 changes: 5 additions & 1 deletion jdaviz/configs/cubeviz/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -31,9 +32,12 @@ class CubevizImageView(JdavizViewerMixin, BqplotImageView):
]

default_class = None
_state_cls = FreezableBqplotImageViewerState
Copy link
Member Author

@kecnry kecnry Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is was the reason for the cubeviz test failure - this forces cubeviz to use Imviz's reset_limits which behaves differently, even after accounting for different pixel components. We may eventually want to update reset_limits and/or the tests so that the image viewers in cubeviz and Imviz behave consistently when resetting limits, but that is out of scope here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By Imviz's reset limits, do you mean this?

# Zoom on X and Y will auto-adjust.
if val == 'fit':
self.state.reset_limits()
return

I farmed it back out to glue-jupyter. Nothing fancy there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I meant FreezableBqplotImageViewerState.reset_limits() which overrides glue's BqplotImageViewerState.reset_limits()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you implemented this 2 years ago in #1897

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, and that so far has not been enabled in cubeviz.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then that particular line was changed from a different logic by me later in #1928

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is any point to make Imviz and Cubeviz consistent. Imviz has WCS linking to worry about, Cubeviz does not.


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)
Expand Down
181 changes: 115 additions & 66 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelect, LayerSelect,
PlotOptionsSyncState, Plot,
skip_if_no_updates_since_last_active, with_spinner)
from jdaviz.core.events import ChangeRefDataMessage
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.tools import ICON_DIR
from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty
from jdaviz.core.custom_traitlets import IntHandleEmpty

from scipy.interpolate import PchipInterpolator

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 @@ -176,6 +187,9 @@ class PlotOptions(PluginTemplateMixin):
template_file = __file__, "plot_options.vue"
uses_active_status = Bool(True).tag(sync=True)

# read-only display units
display_units = Dict().tag(sync=True)

viewer_multiselect = Bool(False).tag(sync=True)
viewer_items = List().tag(sync=True)
viewer_selected = Any().tag(sync=True) # Any needed for multiselect
Expand Down Expand Up @@ -207,26 +221,31 @@ class PlotOptions(PluginTemplateMixin):
uncertainty_visible_value = Int().tag(sync=True)
uncertainty_visible_sync = Dict().tag(sync=True)

viewer_x_min_value = FloatHandleEmpty().tag(sync=True)
viewer_x_min_sync = Dict().tag(sync=True)
x_min_value = Float().tag(sync=True)
x_min_sync = Dict().tag(sync=True)

viewer_x_max_value = FloatHandleEmpty().tag(sync=True)
viewer_x_max_sync = Dict().tag(sync=True)
x_max_value = Float().tag(sync=True)
x_max_sync = Dict().tag(sync=True)

viewer_x_unit_value = Unicode(allow_none=True).tag(sync=True)
viewer_x_unit_sync = Dict().tag(sync=True)
y_min_value = Float().tag(sync=True)
y_min_sync = Dict().tag(sync=True)

viewer_y_min_value = FloatHandleEmpty().tag(sync=True)
viewer_y_min_sync = Dict().tag(sync=True)
y_max_value = Float().tag(sync=True)
y_max_sync = Dict().tag(sync=True)

viewer_y_max_value = FloatHandleEmpty().tag(sync=True)
viewer_y_max_sync = Dict().tag(sync=True)
x_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value
y_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value

viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True)
viewer_y_unit_sync = Dict().tag(sync=True)
zoom_center_x_value = Float().tag(sync=True)
zoom_center_x_sync = Dict().tag(sync=True)

viewer_x_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value
viewer_y_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value
zoom_center_y_value = Float().tag(sync=True)
zoom_center_y_sync = Dict().tag(sync=True)

zoom_radius_value = Float().tag(sync=True)
zoom_radius_sync = Dict().tag(sync=True)

zoom_step = Float(1).tag(sync=True)

# scatter/marker options
marker_visible_value = Bool().tag(sync=True)
Expand Down Expand Up @@ -357,7 +376,6 @@ class PlotOptions(PluginTemplateMixin):
icon_checktoradial = Unicode(read_icon(os.path.join(ICON_DIR, 'checktoradial.svg'), 'svg+xml')).tag(sync=True) # noqa

show_viewer_labels = Bool(True).tag(sync=True)
show_viewer_bounds = Bool(True).tag(sync=True)

cmap_samples = Dict().tag(sync=True)
swatches_palette = List().tag(sync=True)
Expand Down Expand Up @@ -444,24 +462,24 @@ def state_attr_for_line_visible(state):
'uncertainty_visible_value', 'uncertainty_visible_sync') # noqa

# Viewer bounds
self.viewer_x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_min',
'viewer_x_min_value', 'viewer_x_min_sync',
state_filter=not_image_viewer)
self.viewer_x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_max',
'viewer_x_max_value', 'viewer_x_max_sync',
state_filter=not_image_viewer)
self.viewer_x_unit = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_display_unit',
'viewer_x_unit_value', 'viewer_x_unit_sync',
state_filter=not_image_viewer)
self.viewer_y_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_min',
'viewer_y_min_value', 'viewer_y_min_sync',
state_filter=not_image)
self.viewer_y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max',
'viewer_y_max_value', 'viewer_y_max_sync',
state_filter=not_image)
self.viewer_y_unit = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_display_unit',
'viewer_y_unit_value', 'viewer_y_unit_sync',
state_filter=not_image_viewer)
self.x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_min',
'x_min_value', 'x_min_sync',
state_filter=not_image_viewer)
self.x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_max',
'x_max_value', 'x_max_sync',
state_filter=not_image_viewer)
self.y_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_min',
'y_min_value', 'y_min_sync',
state_filter=not_image_viewer)
self.y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max',
'y_max_value', 'y_max_sync',
state_filter=not_image_viewer)
self.zoom_center_x = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_x',
'zoom_center_x_value', 'zoom_center_x_sync')
self.zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y',
'zoom_center_y_value', 'zoom_center_y_sync')
self.zoom_radius = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_radius',
'zoom_radius_value', 'zoom_radius_sync')

# Scatter/marker options:
# NOTE: marker_visible hides the entire layer (including the line)
Expand Down Expand Up @@ -621,6 +639,16 @@ def state_attr_for_line_visible(state):
self.show_viewer_labels = self.app.state.settings['viewer_labels']
self.app.state.add_callback('settings', self._on_app_settings_changed)

sv = self.spectrum_viewer
if sv is not None:
sv.state.add_callback('x_display_unit',
self._on_global_display_unit_changed)
sv.state.add_callback('y_display_unit',
self._on_global_display_unit_changed)

self.hub.subscribe(self, ChangeRefDataMessage,
handler=self._on_refdata_change)

# give UI access to sampled version of the available colormap choices
def hex_for_cmap(cmap):
N = 50
Expand All @@ -635,10 +663,12 @@ def user_api(self):
if self.config == "cubeviz":
expose += ['collapse_function', 'uncertainty_visible']
if self.config != "imviz":
expose += ['axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity',
expose += ['x_min', 'x_max', 'y_min', 'y_max',
'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity',
'line_as_steps', 'uncertainty_visible']
if self.config != "specviz":
expose += ['subset_color', 'subset_opacity',
expose += ['zoom_center_x', 'zoom_center_y', 'zoom_radius',
'subset_color', 'subset_opacity',
'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax',
'stretch_hist_zoom_limits', 'stretch_hist_nbins',
'image_visible', 'image_color_mode',
Expand Down Expand Up @@ -687,6 +717,20 @@ def select_all(self, viewers=True, layers=True):
self.layer_multiselect = True
self.layer.select_all()

def _on_global_display_unit_changed(self, *args):
sv = self.spectrum_viewer
self.display_units['spectral'] = sv.state.x_display_unit
self.display_units['flux'] = sv.state.y_display_unit
self.send_state('display_units')

def _on_refdata_change(self, *args):
if self.app._link_type.lower() == 'wcs':
self.display_units['image'] = 'deg'
else:
self.display_units['image'] = 'pix'
self.send_state('display_units')
self._update_viewer_zoom_steps()

def vue_unmix_state(self, names):
if isinstance(names, str):
names = [names]
Expand Down Expand Up @@ -757,45 +801,50 @@ def apply_RGB_presets(self):
def vue_apply_RGB_presets(self, data):
self.apply_RGB_presets()

@observe('viewer_selected', 'viewer_x_max_value', 'viewer_x_min_value',
'viewer_y_max_value', 'viewer_y_min_value')
@observe('viewer_selected',
'x_min_value', 'x_max_value',
'y_min_value', 'y_max_value')
def _update_viewer_bound_steps(self, msg={}):
if not hasattr(self, 'viewer'): # pragma: no cover
# plugin hasn't been fully initialized yet
return

if not self.viewer.selected: # pragma: no cover
if not self.viewer.selected or not self.x_min_sync['in_subscribed_states']:
# nothing selected yet
return

if self.viewer_multiselect:
not_image = [not isinstance(v.state, ImageViewerState) for v in self.viewer.selected_obj] # noqa
if np.all(not_image):
self.show_viewer_bounds = True
else:
self.show_viewer_bounds = False
return
for ax in ('x', 'y'):
ax_min = getattr(self, f'{ax}_min_value')
ax_max = getattr(self, f'{ax}_max_value')
bound_step, decimals = _round_step((ax_max - ax_min) / 100.)
decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6
setattr(self, f'{ax}_bound_step', bound_step)
setattr(self, f'{ax}_min_value', np.round(ax_min, decimals=decimals))
setattr(self, f'{ax}_max_value', np.round(ax_max, decimals=decimals))

@observe('viewer_selected',
'zoom_center_x_value', 'zoom_center_y_value',
'zoom_radius_value')
def _update_viewer_zoom_steps(self, msg={}):
if not hasattr(self, 'viewer'): # pragma: no cover
# plugin hasn't been fully initialized yet
return

if not self.viewer.selected or not self.zoom_radius_sync['in_subscribed_states']:
# nothing selected yet
return

viewer = self.viewer.selected_obj[0] if self.viewer_multiselect else self.viewer.selected_obj # noqa
if not isinstance(viewer.state, ImageViewerState):
self.show_viewer_bounds = True
# We round these values to show, e.g., 7.15 instead of 7.1499999
if hasattr(viewer.state, "x_max") and viewer.state.x_max is not None:
bound_step = (viewer.state.x_max - viewer.state.x_min) / 100.
decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6
if decimals < 0:
decimals = 0
self.viewer_x_bound_step = np.round(bound_step, decimals=decimals)
self.viewer_x_max_value = np.round(self.viewer_x_max_value, decimals=decimals)
self.viewer_x_min_value = np.round(self.viewer_x_min_value, decimals=decimals)
if hasattr(viewer.state, "y_max") and viewer.state.y_max is not None:
bound_step = (viewer.state.y_max - viewer.state.y_min) / 100.
decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6
if decimals < 0:
decimals = 0
self.viewer_y_bound_step = np.round(bound_step, decimals=decimals)
self.viewer_y_max_value = np.round(self.viewer_y_max_value, decimals=decimals)
self.viewer_y_min_value = np.round(self.viewer_y_min_value, decimals=decimals)
# in the case of multiple viewers, calculate based on the first
# alternatively, we could find the most extreme by looping over all selected viewers
viewers = self.viewer.selected_obj if self.viewer_multiselect else [self.viewer.selected_obj] # noqa
for viewer in viewers:
if hasattr(viewer.state, '_get_reset_limits'):
break
else:
# no image viewer
return
x_min, x_max, y_min, y_max = viewer.state._get_reset_limits(return_as_world=True)
self.zoom_step, _ = _round_step(max(x_max-x_min, y_max-y_min) / 100.)

def vue_reset_viewer_bounds(self, _):
# This button is currently only exposed if only the spectrum viewer is selected
Expand Down
Loading