Skip to content

Commit 2ceec46

Browse files
kecnryrosteen
authored andcommitted
viewer user-APIs exposing zoom/limit options (spacetelescope#2563)
* 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
1 parent 8c67179 commit 2ceec46

File tree

20 files changed

+160
-42
lines changed

20 files changed

+160
-42
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ API Changes
6464
- User APIs now raise a warning when attempting to set a non-existing attribute to avoid confusion
6565
caused by typos, etc. [#2577]
6666

67+
- Viewer API now exposed via ``viz.viewers`` dictionary, currently containing APIs to set axes
68+
limits as well as astrowidgets API commands for Imviz. [#2563]
69+
6770
Cubeviz
6871
^^^^^^^
6972

jdaviz/configs/cubeviz/plugins/tests/test_export_plots.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_export_movie_not_cubeviz(imviz_helper):
4848
@pytest.mark.skipif(not HAS_OPENCV, reason="opencv-python is not installed")
4949
def test_export_movie_cubeviz_exceptions(cubeviz_helper, spectrum1d_cube):
5050
cubeviz_helper.load_data(spectrum1d_cube, data_label="test")
51-
cubeviz_helper.default_viewer.shape = (100, 100)
51+
cubeviz_helper.default_viewer._obj.shape = (100, 100)
5252
cubeviz_helper.app.get_viewer("uncert-viewer").shape = (100, 100)
5353
plugin = cubeviz_helper.plugins["Export Plot"]
5454
assert plugin._obj.movie_msg == ""

jdaviz/configs/cubeviz/plugins/tests/test_regions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TestLoadRegions(BaseRegionHandler):
1818
def setup_class(self, cubeviz_helper, image_cube_hdu_obj_microns):
1919
cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label='has_microns')
2020
self.cubeviz = cubeviz_helper
21-
self.viewer = cubeviz_helper.default_viewer # This is used in BaseRegionHandler
21+
self.viewer = cubeviz_helper.default_viewer._obj # This is used in BaseRegionHandler
2222
self.spectrum_viewer = cubeviz_helper.app.get_viewer(
2323
cubeviz_helper._default_spectrum_viewer_reference_name
2424
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -935,8 +935,8 @@ def _histogram_nbins_changed(self, msg={}):
935935

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

941941
def _viewer_is_image_viewer(self):
942942
# Import here to prevent circular import (and not at the top of the method so the import

jdaviz/configs/default/plugins/viewers.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
from echo import delay_callback
23

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

89
from jdaviz.configs.imviz.helper import layer_is_image_data
910
from jdaviz.components.toolbar_nested import NestedJupyterToolbar
11+
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
1012
from jdaviz.core.registries import viewer_registry
13+
from jdaviz.core.user_api import ViewerUserApi
1114
from jdaviz.utils import ColorCycler, get_subset_type
1215

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

36+
@property
37+
def user_api(self):
38+
# default exposed user APIs. Can override this method in any particular viewer.
39+
if isinstance(self, BqplotImageView):
40+
if isinstance(self, AstrowidgetsImageViewerMixin):
41+
expose = ['save',
42+
'center_on', 'offset_by', 'zoom_level', 'zoom',
43+
'colormap_options', 'set_colormap',
44+
'stretch_options', 'stretch',
45+
'autocut_options', 'cuts',
46+
'marker', 'add_markers', 'remove_markers', 'reset_markers',
47+
'blink_once', 'reset_limits']
48+
else:
49+
# cubeviz image viewers don't inherit from AstrowidgetsImageViewerMixin yet,
50+
# but also shouldn't expose set_limits because of equal aspect ratio concerns
51+
expose = []
52+
elif isinstance(self, TableViewer):
53+
expose = []
54+
else:
55+
expose = ['set_limits', 'reset_limits']
56+
return ViewerUserApi(self, expose=expose)
57+
58+
def reset_limits(self):
59+
"""
60+
Reset viewer axes limits.
61+
"""
62+
self.state.reset_limits()
63+
64+
def set_limits(self, x_min=None, x_max=None, y_min=None, y_max=None):
65+
"""
66+
Set viewer axes limits.
67+
68+
Parameters
69+
----------
70+
x_min : float or None, optional
71+
lower-limit of x-axis (in current axes units)
72+
x_max: float or None, optional
73+
upper-limit of x-axis (in current axes units)
74+
y_min : float or None, optional
75+
lower-limit of y-axis (in current axes units)
76+
y_max: float or None, optional
77+
upper-limit of y-axis (in current axes units)
78+
"""
79+
for val in (x_min, x_max, y_min, y_max):
80+
if val is not None and not isinstance(val, (float, int)):
81+
raise TypeError('all arguments must be None, int, or float')
82+
83+
with delay_callback(self.state, 'x_min', 'x_max', 'y_min', 'y_max'):
84+
if x_min is not None:
85+
self.state.x_min = x_min
86+
if x_max is not None:
87+
self.state.x_max = x_max
88+
if y_min is not None:
89+
self.state.y_min = y_min
90+
if y_max is not None:
91+
self.state.y_max = y_max
92+
3393
@property
3494
def native_marks(self):
3595
"""

jdaviz/configs/imviz/plugins/line_profile_xy/line_profile_xy.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ def vue_draw_plot(self, msg={}):
127127
reset_lims=False)
128128
zoomed_data_x = comp.data[y_min:y_max, x]
129129
if zoomed_data_x.size > 0:
130-
self.plot_across_x.set_lims(x_min=y_min,
131-
x_max=y_max,
132-
y_min=zoomed_data_x.min() * 0.95,
133-
y_max=zoomed_data_x.max() * 1.05)
130+
self.plot_across_x.set_limits(x_min=y_min,
131+
x_max=y_max,
132+
y_min=zoomed_data_x.min() * 0.95,
133+
y_max=zoomed_data_x.max() * 1.05)
134134
self.plot_across_x.update_style('line', line_visible=True,
135135
markers_visible=False, color='gray', size=10)
136136
self.plot_across_x.viewer.axis_x.label = 'Y (pix)'
@@ -141,10 +141,10 @@ def vue_draw_plot(self, msg={}):
141141
reset_lims=False)
142142
zoomed_data_y = comp.data[y, x_min:x_max]
143143
if zoomed_data_y.size > 0:
144-
self.plot_across_y.set_lims(x_min=x_min,
145-
x_max=x_max,
146-
y_min=zoomed_data_y.min() * 0.95,
147-
y_max=zoomed_data_y.max() * 1.05)
144+
self.plot_across_y.set_limits(x_min=x_min,
145+
x_max=x_max,
146+
y_min=zoomed_data_y.min() * 0.95,
147+
y_max=zoomed_data_y.max() * 1.05)
148148
self.plot_across_y.update_style('line', line_visible=True,
149149
markers_visible=False, color='gray', size=10)
150150
self.plot_across_y.viewer.axis_x.label = 'X (pix)'

jdaviz/configs/imviz/tests/test_footprints.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313

1414
def _get_markers_from_viewer(viewer):
15+
if hasattr(viewer, '_obj'):
16+
viewer = viewer._obj
1517
return [m for m in viewer.figure.marks if isinstance(m, FootprintOverlay)]
1618

1719

jdaviz/configs/imviz/tests/test_linking.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ def test_pixel_linking(self):
2424

2525
@property
2626
def default_viewer_limits(self):
27-
return (self.imviz.default_viewer.state.x_min,
28-
self.imviz.default_viewer.state.x_max,
29-
self.imviz.default_viewer.state.y_min,
30-
self.imviz.default_viewer.state.y_max)
27+
return (self.imviz.default_viewer._obj.state.x_min,
28+
self.imviz.default_viewer._obj.state.x_max,
29+
self.imviz.default_viewer._obj.state.y_min,
30+
self.imviz.default_viewer._obj.state.y_max)
3131

3232

3333
class TestLink_WCS_NoWCS(BaseImviz_WCS_NoWCS, BaseLinkHandler):
@@ -104,7 +104,7 @@ def test_wcslink_affine_with_extras(self):
104104

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

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

372372
with pytest.raises(ValueError, match='No reference data for link look-up'):
373-
imviz_helper.default_viewer.get_link_type('foo')
373+
imviz_helper.default_viewer._obj.get_link_type('foo')

jdaviz/configs/imviz/tests/test_regions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def test_ds9_load_all(self, imviz_helper):
188188
with pytest.raises(ValueError, match="Cannot load regions without data"):
189189
imviz_helper.load_data(self.region_file)
190190

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

204204
def test_ds9_load_two_good(self, imviz_helper):
205-
self.viewer = imviz_helper.default_viewer
205+
self.viewer = imviz_helper.default_viewer._obj
206206
imviz_helper.load_data(self.arr, data_label='my_image')
207207
bad_regions = imviz_helper.load_regions_from_file(
208208
self.region_file, max_num_regions=2, return_bad_regions=True)
@@ -212,15 +212,15 @@ def test_ds9_load_two_good(self, imviz_helper):
212212
self.verify_region_loaded('MaskedSubset 1', count=0)
213213

214214
def test_ds9_load_one_bad(self, imviz_helper):
215-
self.viewer = imviz_helper.default_viewer
215+
self.viewer = imviz_helper.default_viewer._obj
216216
imviz_helper.load_data(self.arr, data_label='my_image')
217217
bad_regions = imviz_helper.load_regions(self.raw_regions[6], return_bad_regions=True)
218218
assert len(bad_regions) == 1
219219
assert imviz_helper.get_interactive_regions() == {}
220220
self.verify_region_loaded('MaskedSubset 1', count=0)
221221

222222
def test_ds9_load_one_good_one_bad(self, imviz_helper):
223-
self.viewer = imviz_helper.default_viewer
223+
self.viewer = imviz_helper.default_viewer._obj
224224
imviz_helper.load_data(self.arr, data_label='my_image')
225225
bad_regions = imviz_helper.load_regions(
226226
[self.raw_regions[3], self.raw_regions[6]], return_bad_regions=True)

jdaviz/configs/imviz/tests/test_simple_aper_phot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def setup_class(self, imviz_helper):
280280
imviz_helper.load_regions(regions)
281281

282282
self.imviz = imviz_helper
283-
self.viewer = imviz_helper.default_viewer
283+
self.viewer = imviz_helper.default_viewer._obj
284284
self.phot_plugin = imviz_helper.plugins["Aperture Photometry"]._obj
285285

286286
@pytest.mark.parametrize(('data_label', 'local_bkg'), [

jdaviz/configs/imviz/tests/test_tools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class TestPanZoomTools(BaseImviz_WCS_WCS):
99
def test_panzoom_tools(self):
10-
v = self.imviz.default_viewer
10+
v = self.imviz.default_viewer._obj
1111
v2 = self.imviz.create_image_viewer()
1212
self.imviz.app.add_data_to_viewer('imviz-1', 'has_wcs_1[SCI,1]')
1313

@@ -70,7 +70,7 @@ class TestSinglePixelRegion(BaseImviz_WCS_WCS):
7070
def test_singlepixelregion(self):
7171
self.imviz.link_data(link_type='wcs')
7272

73-
t = self.imviz.default_viewer.toolbar.tools['jdaviz:singlepixelregion']
73+
t = self.imviz.default_viewer._obj.toolbar.tools['jdaviz:singlepixelregion']
7474
t.activate()
7575

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

104104

105105
def test_blink(imviz_helper):
106-
viewer = imviz_helper.default_viewer
106+
viewer = imviz_helper.default_viewer._obj
107107

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

143143
def test_tool_visibility(imviz_helper):
144144
imviz_helper.load_data(np.ones((2, 2)))
145-
tb = imviz_helper.default_viewer.toolbar
145+
tb = imviz_helper.default_viewer._obj.toolbar
146146

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

jdaviz/configs/imviz/tests/utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def setup_class(self, imviz_helper):
4141

4242
self.wcs = WCS(hdu.header)
4343
self.imviz = imviz_helper
44-
self.viewer = imviz_helper.default_viewer
44+
self.viewer = imviz_helper.default_viewer._obj
4545

4646
# Since we are not really displaying, need this to test zoom.
4747
self.viewer.shape = (100, 100)
@@ -88,7 +88,7 @@ def setup_class(self, imviz_helper):
8888
self.wcs_1 = WCS(hdu1.header)
8989
self.wcs_2 = WCS(hdu2.header)
9090
self.imviz = imviz_helper
91-
self.viewer = imviz_helper.default_viewer
91+
self.viewer = imviz_helper.default_viewer._obj
9292

9393
# Since we are not really displaying, need this to test zoom.
9494
self.viewer.shape = (100, 100)
@@ -146,7 +146,7 @@ def setup_class(self, imviz_helper):
146146
self.wcs_1 = w_fits
147147
self.wcs_2 = w_gwcs
148148
self.imviz = imviz_helper
149-
self.viewer = imviz_helper.default_viewer
149+
self.viewer = imviz_helper.default_viewer._obj
150150

151151
# Since we are not really displaying, need this to test zoom.
152152
self.viewer.shape = (100, 100)
@@ -196,7 +196,7 @@ def setup_class(self, imviz_helper):
196196
self.wcs_1 = w_gwcs_1
197197
self.wcs_2 = w_gwcs_2
198198
self.imviz = imviz_helper
199-
self.viewer = imviz_helper.default_viewer
199+
self.viewer = imviz_helper.default_viewer._obj
200200

201201
# Since we are not really displaying, need this to test zoom.
202202
self.viewer.shape = (100, 100)

jdaviz/core/helpers.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ def plugins(self):
122122
return {item['label']: widget_serialization['from_json'](item['widget'], None).user_api
123123
for item in self.app.state.tray_items}
124124

125+
@property
126+
def viewers(self):
127+
"""
128+
Access API objects for any viewer.
129+
130+
Returns
131+
-------
132+
viewers : dict
133+
dict of viewer objects
134+
"""
135+
return {getattr(viewer, 'reference', k): viewer.user_api
136+
for k, viewer in self.app._viewer_store.items()}
137+
125138
@property
126139
def fitted_models(self):
127140
"""
@@ -611,7 +624,7 @@ def __init__(self, *args, **kwargs):
611624
def default_viewer(self):
612625
"""Default viewer instance. This is typically the first viewer
613626
(e.g., "imviz-0" or "cubeviz-0")."""
614-
return self._default_viewer
627+
return self._default_viewer.user_api
615628

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

730743
# Subset is global but reference data is viewer-dependent.
731744
if refdata_label is None:
732-
data = self.default_viewer.state.reference_data
745+
data = self.default_viewer._obj.state.reference_data
733746
else:
734747
data = self.app.data_collection[refdata_label]
735748

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

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

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

844857
# Subset is global, so we just use default viewer.
845-
for lyr in self.default_viewer.layers:
858+
for lyr in self.default_viewer._obj.layers:
846859
if (not hasattr(lyr, 'layer') or not isinstance(lyr.layer, Subset)
847860
or lyr.layer.ndim not in (2, 3)):
848861
continue
@@ -876,7 +889,7 @@ def _apply_interactive_region(self, toolname, from_pix, to_pix):
876889
This is for internal testing only.
877890
"""
878891
self.app.session.edit_subset_mode._mode = NewMode
879-
tool = self.default_viewer.toolbar.tools[toolname]
892+
tool = self.default_viewer._obj.toolbar.tools[toolname]
880893
tool.activate()
881894
tool.interact.brushing = True
882895
tool.interact.selected = [from_pix, to_pix]

jdaviz/core/template_mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3626,7 +3626,7 @@ def add_bins(self, label, sample=[0], bins=2, density=True, **kwargs):
36263626
colors=kwargs.pop('color', kwargs.pop('colors', 'gray')),
36273627
**kwargs)
36283628

3629-
def set_lims(self, x_min=None, x_max=None, y_min=None, y_max=None):
3629+
def set_limits(self, x_min=None, x_max=None, y_min=None, y_max=None):
36303630
with delay_callback(self.viewer.state, 'x_min', 'x_max', 'y_min', 'y_max'):
36313631
if x_min is not None:
36323632
self.viewer.state.x_min = x_min

0 commit comments

Comments
 (0)