Skip to content
New issue

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

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

Already on GitHub? # to your account

Expose spectrum-viewer bounds in UI #2604

Merged
merged 21 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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]

Cubeviz
^^^^^^^
- Calculated moments can now be output in velocity units. [#2584, #2588]
Expand Down
98 changes: 94 additions & 4 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from glue.config import colormaps, stretches
from glue.viewers.scatter.state import ScatterViewerState
from glue.viewers.profile.state import ProfileViewerState, ProfileLayerState
from glue.viewers.image.state import ImageSubsetLayerState
from glue.viewers.image.state import ImageSubsetLayerState, ImageViewerState
from glue.viewers.scatter.state import ScatterLayerState as BqplotScatterLayerState
from glue.viewers.image.composite_array import COLOR_CONVERTER
from glue_jupyter.bqplot.image.state import BqplotImageLayerState
Expand All @@ -26,7 +26,7 @@
skip_if_no_updates_since_last_active, with_spinner)
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.tools import ICON_DIR
from jdaviz.core.custom_traitlets import IntHandleEmpty
from jdaviz.core.custom_traitlets import IntHandleEmpty, FloatHandleEmpty

from scipy.interpolate import PchipInterpolator

Expand Down Expand Up @@ -177,6 +177,7 @@
viewer_multiselect = Bool(False).tag(sync=True)
viewer_items = List().tag(sync=True)
viewer_selected = Any().tag(sync=True) # Any needed for multiselect
viewer_limits = Dict().tag(sync=True)

layer_multiselect = Bool(False).tag(sync=True)
layer_items = List().tag(sync=True)
Expand Down Expand Up @@ -204,6 +205,27 @@
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)

viewer_x_max_value = FloatHandleEmpty().tag(sync=True)
viewer_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)

viewer_y_min_value = FloatHandleEmpty().tag(sync=True)
viewer_y_min_sync = Dict().tag(sync=True)

viewer_y_max_value = FloatHandleEmpty().tag(sync=True)
viewer_y_max_sync = Dict().tag(sync=True)

viewer_y_unit_value = Unicode(allow_none=True).tag(sync=True)
viewer_y_unit_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

# scatter/marker options
marker_visible_value = Bool().tag(sync=True)
marker_visible_sync = Dict().tag(sync=True)
Expand Down Expand Up @@ -330,6 +352,7 @@
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 @@ -368,6 +391,9 @@
def not_image(state):
return not is_image(state)

def not_image_viewer(state):
return not isinstance(state, ImageViewerState)

def not_image_or_spatial_subset(state):
return not is_image(state) and not is_spatial_subset(state)

Expand Down Expand Up @@ -406,6 +432,26 @@
self.uncertainty_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_uncertainty', # noqa
'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)

# Scatter/marker options:
# NOTE: marker_visible hides the entire layer (including the line)
self.marker_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'visible',
Expand Down Expand Up @@ -557,8 +603,6 @@
self.axes_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_axes',
'axes_visible_value', 'axes_visible_sync',
state_filter=not_profile)
# zoom limits
# display_units

self.show_viewer_labels = self.app.state.settings['viewer_labels']
self.app.state.add_callback('settings', self._on_app_settings_changed)
Expand Down Expand Up @@ -699,6 +743,52 @@
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')
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
# 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

Check warning on line 760 in jdaviz/configs/default/plugins/plot_options/plot_options.py

View check run for this annotation

Codecov / codecov/patch

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

Added line #L760 was not covered by tests
else:
self.show_viewer_bounds = False
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
Copy link
Member

Choose a reason for hiding this comment

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

this also limits the ability to zoom in to small wavelength ranges... I'm not sure that is worth it

Copy link
Member

Choose a reason for hiding this comment

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

maybe decimals can be based on the current range instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changing this to depend on the difference between min and max rather than just max makes sense to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That also prevents decreasing v_max to be less than v_min, which is nice.

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)

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
for viewer in viewers:
viewer.toolbar.tools['jdaviz:homezoom'].activate()

Check warning on line 790 in jdaviz/configs/default/plugins/plot_options/plot_options.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/default/plugins/plot_options/plot_options.py#L788-L790

Added lines #L788 - L790 were not covered by tests

@observe('stretch_function_sync', 'stretch_params_sync',
'stretch_vmin_sync', 'stretch_vmax_sync',
'image_color_mode_sync', 'image_color_sync', 'image_colormap_sync')
Expand Down
66 changes: 62 additions & 4 deletions jdaviz/configs/default/plugins/plot_options/plot_options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,64 @@
:hint="viewer_multiselect ? 'Select viewers to set options simultaneously' : 'Select the viewer to set options.'"
/>

<v-row v-if="show_viewer_bounds">
<v-expansion-panels popout>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<span style="padding: 6px">Viewer bounds</span>
</v-expansion-panel-header>
<v-expansion-panel-content class="plugin-expansion-panel-content">
<glue-state-sync-wrapper :sync="viewer_x_min_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('viewer_x_min')">
<glue-float-field
ref="viewer_x_min"
label="Viewer X Min"
:value.sync="viewer_x_min_value"
type="number"
:step="viewer_x_bound_step"
:suffix="viewer_x_unit_value"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="viewer_x_max_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('viewer_x_max')">
<glue-float-field
ref="viewer_x_max"
label="Viewer X Max"
:value.sync="viewer_x_max_value"
type="number"
:step="viewer_x_bound_step"
:suffix="viewer_x_unit_value"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="viewer_y_min_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('viewer_y_min')">
<glue-float-field
ref="viewer_y_min"
label="Viewer Y Min"
:value.sync="viewer_y_min_value"
type="number"
:step="viewer_y_bound_step"
:suffix="viewer_y_unit_value"
/>
</glue-state-sync-wrapper>
<glue-state-sync-wrapper :sync="viewer_y_max_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('viewer_y_max')">
<glue-float-field
ref="viewer_y_max"
label="Viewer Y Max"
:value.sync="viewer_y_max_value"
type="number"
:step="viewer_y_bound_step"
:suffix="viewer_y_unit_value"
/>
</glue-state-sync-wrapper>
<plugin-action-button
:results_isolated_to_plugin="false"
@click="reset_viewer_bounds"
>
Reset viewer bounds
</plugin-action-button>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-row>

<div v-if="image_color_mode_sync.in_subscribed_states">
<glue-state-sync-wrapper :sync="image_color_mode_sync" :multiselect="viewer_multiselect" @unmix-state="unmix_state('image_color_mode')">
<v-select
Expand Down Expand Up @@ -467,19 +525,19 @@
<div v-if="stretch_function_sync.in_subscribed_states && (!layer_multiselect || layer_selected.length <= 1)">
<div style="display: grid"> <!-- overlay container -->
<div style="grid-area: 1/1">
<glue-state-sync-wrapper
<glue-state-sync-wrapper
:sync="stretch_hist_sync"
:multiselect="layer_multiselect"
:multiselect="layer_multiselect"
@unmix-state="unmix_state(['stretch_function', 'stretch_params',
'stretch_vmin', 'stretch_vmax',
'stretch_vmin', 'stretch_vmax',
'image_color_mode', 'image_color', 'image_colormap'])"
>
<jupyter-widget :widget="stretch_histogram_widget"/>
</glue-state-sync-wrapper>
</div>
<div v-if="stretch_hist_spinner"
class="text-center"
style="grid-area: 1/1;
style="grid-area: 1/1;
z-index:2;
margin-left: -24px;
margin-right: -24px;
Expand Down
11 changes: 10 additions & 1 deletion jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3301,7 +3301,7 @@ def _on_viewer_layer_changed(self, msg=None):
'in_subscribed_states': in_subscribed_states,
'icons': icons,
'mixed': self.is_mixed(current_glue_values)}
if len(current_glue_values):
if len(current_glue_values) and current_glue_values[0] is not None:
# sync the initial value of the widget, avoiding recursion
self._on_glue_value_changed(current_glue_values[0])

Expand All @@ -3323,6 +3323,15 @@ def is_mixed(self, glue_values):
return True

return False

# Need this for temporary None value during startup
elif len(glue_values):
no_nones = [x for x in glue_values if x is not None]
if len(no_nones) == 0:
return False
if len(no_nones) != len(glue_values):
return True

return len(np.unique(glue_values, axis=0)) > 1

def _update_mixed_state(self):
Expand Down
Loading