Skip to content

Commit

Permalink
viewer user-APIs exposing zoom/limit options (spacetelescope#2563)
Browse files Browse the repository at this point in the history
* viewer user-APIs exposing zoom/limit options

* defer exposing astrowidgets API in cubeviz

* no public API (yet) for table viewers

* docstring and type-checks for set_lims

* set_lims > set_limits

* changelog and basic test coverage

* include astrowidgets API

* add blink_once and reset_limits to user API

* update internal calls to default_viewer

* to use public API where possible, and otherwise go through ._obj
  • Loading branch information
kecnry authored and rosteen committed Nov 29, 2023
1 parent 8c67179 commit 2ceec46
Show file tree
Hide file tree
Showing 20 changed files with 160 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ API Changes
- User APIs now raise a warning when attempting to set a non-existing attribute to avoid confusion
caused by typos, etc. [#2577]

- Viewer API now exposed via ``viz.viewers`` dictionary, currently containing APIs to set axes
limits as well as astrowidgets API commands for Imviz. [#2563]

Cubeviz
^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_export_movie_not_cubeviz(imviz_helper):
@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed")
def test_export_movie_cubeviz_exceptions(cubeviz_helper, spectrum1d_cube):
cubeviz_helper.load_data(spectrum1d_cube, data_label="test")
cubeviz_helper.default_viewer.shape = (100, 100)
cubeviz_helper.default_viewer._obj.shape = (100, 100)
cubeviz_helper.app.get_viewer("uncert-viewer").shape = (100, 100)
plugin = cubeviz_helper.plugins["Export Plot"]
assert plugin._obj.movie_msg == ""
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/configs/cubeviz/plugins/tests/test_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TestLoadRegions(BaseRegionHandler):
def setup_class(self, cubeviz_helper, image_cube_hdu_obj_microns):
cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label='has_microns')
self.cubeviz = cubeviz_helper
self.viewer = cubeviz_helper.default_viewer # This is used in BaseRegionHandler
self.viewer = cubeviz_helper.default_viewer._obj # This is used in BaseRegionHandler
self.spectrum_viewer = cubeviz_helper.app.get_viewer(
cubeviz_helper._default_spectrum_viewer_reference_name
)
Expand Down
4 changes: 2 additions & 2 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,8 +935,8 @@ def _histogram_nbins_changed(self, msg={}):

def set_histogram_limits(self, x_min=None, x_max=None, y_min=None, y_max=None):
# NOTE: leaving this out of user API until API is finalized with interactive setting
self.stretch_histogram.set_lims(x_min=x_min, x_max=x_max,
y_min=y_min, y_max=y_max)
self.stretch_histogram.set_limits(x_min=x_min, x_max=x_max,
y_min=y_min, y_max=y_max)

def _viewer_is_image_viewer(self):
# Import here to prevent circular import (and not at the top of the method so the import
Expand Down
60 changes: 60 additions & 0 deletions jdaviz/configs/default/plugins/viewers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
from echo import delay_callback

from glue.viewers.scatter.state import ScatterLayerState as BqplotScatterLayerState
from glue_jupyter.bqplot.profile import BqplotProfileView
Expand All @@ -7,7 +8,9 @@

from jdaviz.configs.imviz.helper import layer_is_image_data
from jdaviz.components.toolbar_nested import NestedJupyterToolbar
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
from jdaviz.core.registries import viewer_registry
from jdaviz.core.user_api import ViewerUserApi
from jdaviz.utils import ColorCycler, get_subset_type

__all__ = ['JdavizViewerMixin']
Expand All @@ -30,6 +33,63 @@ def __init__(self, *args, **kwargs):
# Allow each viewer to cycle through colors for each new addition to the viewer:
self.color_cycler = ColorCycler()

@property
def user_api(self):
# default exposed user APIs. Can override this method in any particular viewer.
if isinstance(self, BqplotImageView):
if isinstance(self, AstrowidgetsImageViewerMixin):
expose = ['save',
'center_on', 'offset_by', 'zoom_level', 'zoom',
'colormap_options', 'set_colormap',
'stretch_options', 'stretch',
'autocut_options', 'cuts',
'marker', 'add_markers', 'remove_markers', 'reset_markers',
'blink_once', 'reset_limits']
else:
# cubeviz image viewers don't inherit from AstrowidgetsImageViewerMixin yet,
# but also shouldn't expose set_limits because of equal aspect ratio concerns
expose = []
elif isinstance(self, TableViewer):
expose = []
else:
expose = ['set_limits', 'reset_limits']
return ViewerUserApi(self, expose=expose)

def reset_limits(self):
"""
Reset viewer axes limits.
"""
self.state.reset_limits()

def set_limits(self, x_min=None, x_max=None, y_min=None, y_max=None):
"""
Set viewer axes limits.
Parameters
----------
x_min : float or None, optional
lower-limit of x-axis (in current axes units)
x_max: float or None, optional
upper-limit of x-axis (in current axes units)
y_min : float or None, optional
lower-limit of y-axis (in current axes units)
y_max: float or None, optional
upper-limit of y-axis (in current axes units)
"""
for val in (x_min, x_max, y_min, y_max):
if val is not None and not isinstance(val, (float, int)):
raise TypeError('all arguments must be None, int, or float')

with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'):
if x_min is not None:
self.state.x_min = x_min
if x_max is not None:
self.state.x_max = x_max
if y_min is not None:
self.state.y_min = y_min
if y_max is not None:
self.state.y_max = y_max

@property
def native_marks(self):
"""
Expand Down
16 changes: 8 additions & 8 deletions jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ def vue_draw_plot(self, msg={}):
reset_lims=False)
zoomed_data_x = comp.data[y_min:y_max, x]
if zoomed_data_x.size > 0:
self.plot_across_x.set_lims(x_min=y_min,
x_max=y_max,
y_min=zoomed_data_x.min() * 0.95,
y_max=zoomed_data_x.max() * 1.05)
self.plot_across_x.set_limits(x_min=y_min,
x_max=y_max,
y_min=zoomed_data_x.min() * 0.95,
y_max=zoomed_data_x.max() * 1.05)
self.plot_across_x.update_style('line', line_visible=True,
markers_visible=False, color='gray', size=10)
self.plot_across_x.viewer.axis_x.label = 'Y (pix)'
Expand All @@ -141,10 +141,10 @@ def vue_draw_plot(self, msg={}):
reset_lims=False)
zoomed_data_y = comp.data[y, x_min:x_max]
if zoomed_data_y.size > 0:
self.plot_across_y.set_lims(x_min=x_min,
x_max=x_max,
y_min=zoomed_data_y.min() * 0.95,
y_max=zoomed_data_y.max() * 1.05)
self.plot_across_y.set_limits(x_min=x_min,
x_max=x_max,
y_min=zoomed_data_y.min() * 0.95,
y_max=zoomed_data_y.max() * 1.05)
self.plot_across_y.update_style('line', line_visible=True,
markers_visible=False, color='gray', size=10)
self.plot_across_y.viewer.axis_x.label = 'X (pix)'
Expand Down
2 changes: 2 additions & 0 deletions jdaviz/configs/imviz/tests/test_footprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@


def _get_markers_from_viewer(viewer):
if hasattr(viewer, '_obj'):
viewer = viewer._obj
return [m for m in viewer.figure.marks if isinstance(m, FootprintOverlay)]


Expand Down
12 changes: 6 additions & 6 deletions jdaviz/configs/imviz/tests/test_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ def test_pixel_linking(self):

@property
def default_viewer_limits(self):
return (self.imviz.default_viewer.state.x_min,
self.imviz.default_viewer.state.x_max,
self.imviz.default_viewer.state.y_min,
self.imviz.default_viewer.state.y_max)
return (self.imviz.default_viewer._obj.state.x_min,
self.imviz.default_viewer._obj.state.x_max,
self.imviz.default_viewer._obj.state.y_min,
self.imviz.default_viewer._obj.state.y_max)


class TestLink_WCS_NoWCS(BaseImviz_WCS_NoWCS, BaseLinkHandler):
Expand Down Expand Up @@ -104,7 +104,7 @@ def test_wcslink_affine_with_extras(self):

# linking should not change axes limits, but should when resetting
assert_allclose(self.default_viewer_limits, orig_pixel_limits)
self.imviz.default_viewer.state.reset_limits()
self.imviz.default_viewer.reset_limits()
assert_allclose(self.default_viewer_limits, (-1.5, 9.5, -1, 10))

# Customize display on second image (last loaded).
Expand Down Expand Up @@ -370,4 +370,4 @@ def test_imviz_no_data(imviz_helper):
assert len(links) == 0

with pytest.raises(ValueError, match='No reference data for link look-up'):
imviz_helper.default_viewer.get_link_type('foo')
imviz_helper.default_viewer._obj.get_link_type('foo')
8 changes: 4 additions & 4 deletions jdaviz/configs/imviz/tests/test_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def test_ds9_load_all(self, imviz_helper):
with pytest.raises(ValueError, match="Cannot load regions without data"):
imviz_helper.load_data(self.region_file)

self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj
imviz_helper.load_data(self.arr, data_label='my_image')
bad_regions = imviz_helper.load_regions_from_file(self.region_file, return_bad_regions=True)
assert len(bad_regions) == 1
Expand All @@ -202,7 +202,7 @@ def test_ds9_load_all(self, imviz_helper):
self.verify_region_loaded('MaskedSubset 1', count=1)

def test_ds9_load_two_good(self, imviz_helper):
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj
imviz_helper.load_data(self.arr, data_label='my_image')
bad_regions = imviz_helper.load_regions_from_file(
self.region_file, max_num_regions=2, return_bad_regions=True)
Expand All @@ -212,15 +212,15 @@ def test_ds9_load_two_good(self, imviz_helper):
self.verify_region_loaded('MaskedSubset 1', count=0)

def test_ds9_load_one_bad(self, imviz_helper):
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj
imviz_helper.load_data(self.arr, data_label='my_image')
bad_regions = imviz_helper.load_regions(self.raw_regions[6], return_bad_regions=True)
assert len(bad_regions) == 1
assert imviz_helper.get_interactive_regions() == {}
self.verify_region_loaded('MaskedSubset 1', count=0)

def test_ds9_load_one_good_one_bad(self, imviz_helper):
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj
imviz_helper.load_data(self.arr, data_label='my_image')
bad_regions = imviz_helper.load_regions(
[self.raw_regions[3], self.raw_regions[6]], return_bad_regions=True)
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/configs/imviz/tests/test_simple_aper_phot.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def setup_class(self, imviz_helper):
imviz_helper.load_regions(regions)

self.imviz = imviz_helper
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj
self.phot_plugin = imviz_helper.plugins["Aperture Photometry"]._obj

@pytest.mark.parametrize(('data_label', 'local_bkg'), [
Expand Down
8 changes: 4 additions & 4 deletions jdaviz/configs/imviz/tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class TestPanZoomTools(BaseImviz_WCS_WCS):
def test_panzoom_tools(self):
v = self.imviz.default_viewer
v = self.imviz.default_viewer._obj
v2 = self.imviz.create_image_viewer()
self.imviz.app.add_data_to_viewer('imviz-1', 'has_wcs_1[SCI,1]')

Expand Down Expand Up @@ -70,7 +70,7 @@ class TestSinglePixelRegion(BaseImviz_WCS_WCS):
def test_singlepixelregion(self):
self.imviz.link_data(link_type='wcs')

t = self.imviz.default_viewer.toolbar.tools['jdaviz:singlepixelregion']
t = self.imviz.default_viewer._obj.toolbar.tools['jdaviz:singlepixelregion']
t.activate()

# Create a region while viewing reference data.
Expand Down Expand Up @@ -103,7 +103,7 @@ def test_singlepixelregion(self):


def test_blink(imviz_helper):
viewer = imviz_helper.default_viewer
viewer = imviz_helper.default_viewer._obj

for i in range(3):
imviz_helper.load_data(np.zeros((2, 2)) + i, data_label=f'image_{i}')
Expand Down Expand Up @@ -142,7 +142,7 @@ def test_compass_open_while_load(imviz_helper):

def test_tool_visibility(imviz_helper):
imviz_helper.load_data(np.ones((2, 2)))
tb = imviz_helper.default_viewer.toolbar
tb = imviz_helper.default_viewer._obj.toolbar

assert not tb.tools_data['jdaviz:boxzoommatch']['visible']

Expand Down
8 changes: 4 additions & 4 deletions jdaviz/configs/imviz/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def setup_class(self, imviz_helper):

self.wcs = WCS(hdu.header)
self.imviz = imviz_helper
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj

# Since we are not really displaying, need this to test zoom.
self.viewer.shape = (100, 100)
Expand Down Expand Up @@ -88,7 +88,7 @@ def setup_class(self, imviz_helper):
self.wcs_1 = WCS(hdu1.header)
self.wcs_2 = WCS(hdu2.header)
self.imviz = imviz_helper
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj

# Since we are not really displaying, need this to test zoom.
self.viewer.shape = (100, 100)
Expand Down Expand Up @@ -146,7 +146,7 @@ def setup_class(self, imviz_helper):
self.wcs_1 = w_fits
self.wcs_2 = w_gwcs
self.imviz = imviz_helper
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj

# Since we are not really displaying, need this to test zoom.
self.viewer.shape = (100, 100)
Expand Down Expand Up @@ -196,7 +196,7 @@ def setup_class(self, imviz_helper):
self.wcs_1 = w_gwcs_1
self.wcs_2 = w_gwcs_2
self.imviz = imviz_helper
self.viewer = imviz_helper.default_viewer
self.viewer = imviz_helper.default_viewer._obj

# Since we are not really displaying, need this to test zoom.
self.viewer.shape = (100, 100)
Expand Down
23 changes: 18 additions & 5 deletions jdaviz/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ def plugins(self):
return {item['label']: widget_serialization['from_json'](item['widget'], None).user_api
for item in self.app.state.tray_items}

@property
def viewers(self):
"""
Access API objects for any viewer.
Returns
-------
viewers : dict
dict of viewer objects
"""
return {getattr(viewer, 'reference', k): viewer.user_api
for k, viewer in self.app._viewer_store.items()}

@property
def fitted_models(self):
"""
Expand Down Expand Up @@ -611,7 +624,7 @@ def __init__(self, *args, **kwargs):
def default_viewer(self):
"""Default viewer instance. This is typically the first viewer
(e.g., "imviz-0" or "cubeviz-0")."""
return self._default_viewer
return self._default_viewer.user_api

def load_regions_from_file(self, region_file, region_format='ds9', max_num_regions=20,
**kwargs):
Expand Down Expand Up @@ -729,7 +742,7 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None,

# Subset is global but reference data is viewer-dependent.
if refdata_label is None:
data = self.default_viewer.state.reference_data
data = self.default_viewer._obj.state.reference_data
else:
data = self.app.data_collection[refdata_label]

Expand Down Expand Up @@ -763,7 +776,7 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None,

# TODO: Do we want user to specify viewer? Does it matter?
self.app.session.edit_subset_mode._mode = NewMode
self.default_viewer.apply_roi(state)
self.default_viewer._obj.apply_roi(state)
self.app.session.edit_subset_mode.edit_subset = None # No overwrite next iteration # noqa

# Last resort: Masked Subset that is static (if data is not a cube)
Expand Down Expand Up @@ -842,7 +855,7 @@ def get_interactive_regions(self):
failed_regs = set()

# Subset is global, so we just use default viewer.
for lyr in self.default_viewer.layers:
for lyr in self.default_viewer._obj.layers:
if (not hasattr(lyr, 'layer') or not isinstance(lyr.layer, Subset)
or lyr.layer.ndim not in (2, 3)):
continue
Expand Down Expand Up @@ -876,7 +889,7 @@ def _apply_interactive_region(self, toolname, from_pix, to_pix):
This is for internal testing only.
"""
self.app.session.edit_subset_mode._mode = NewMode
tool = self.default_viewer.toolbar.tools[toolname]
tool = self.default_viewer._obj.toolbar.tools[toolname]
tool.activate()
tool.interact.brushing = True
tool.interact.selected = [from_pix, to_pix]
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3626,7 +3626,7 @@ def add_bins(self, label, sample=[0], bins=2, density=True, **kwargs):
colors=kwargs.pop('color', kwargs.pop('colors', 'gray')),
**kwargs)

def set_lims(self, x_min=None, x_max=None, y_min=None, y_max=None):
def set_limits(self, x_min=None, x_max=None, y_min=None, y_max=None):
with delay_callback(self.viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'):
if x_min is not None:
self.viewer.state.x_min = x_min
Expand Down
Loading

0 comments on commit 2ceec46

Please sign in to comment.