Skip to content

Commit

Permalink
Plugin plots to viewers (spacetelescope#2498)
Browse files Browse the repository at this point in the history
* create separate app instance with glue-viewer
* nested toolbar in plugin viewers
* migrate implementation for scatter viewers (radial profile, line profiles) and histogram (stretch in plot options)
* downsampling no longer necessary
* lose logic to ensure vmin/max markers are within viewer range (might be confusing)
* home button on histogram viewer is buggy
* update tests
* temporary workaround to segfault on some machines
* bump glue-jupyter to ensure scatter lines support
  • Loading branch information
kecnry authored Oct 12, 2023
1 parent ffde818 commit 2f24a20
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 183 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
New Features
------------

- Plots in plugins now include basic zoom/pan tools for Plot Options,
Imviz Line Profiles, and Imviz's aperture photometry. [#2498]

Cubeviz
^^^^^^^

Expand Down
19 changes: 12 additions & 7 deletions jdaviz/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,12 @@ a:active {
text-decoration: none;
}
.jdaviz-nested-toolbar {
.invert, .invert-if-dark.theme--dark {
filter: invert(1) saturate(1) brightness(100);
color: white;
}
.jdaviz-nested-toolbar, .plugin-nested-toolbar {
/* height of nested toolbar to match viewer toolbar height */
height: 42px;
margin-right: 4px;
Expand All @@ -371,12 +376,7 @@ a:active {
filter: invert(1) saturate(1) brightness(100);
}
.invert, .invert-if-dark.theme--dark {
filter: invert(1) saturate(1) brightness(100);
color: white;
}
.jdaviz-nested-toolbar .v-btn {
.jdaviz-nested-toolbar .v-btn, .plugin-nested-toolbar .v-btn {
height: 42px !important;
border: none !important;
min-width: 42px !important;
Expand All @@ -394,6 +394,11 @@ a:active {
background-color: #c75109 !important;
}
.plugin-nested-toolbar .v-btn--active, .plugin-nested-toolbar .v-btn:focus {
/* semi-transparent active color (orange) */
background-color: #c7510996 !important;
}
.v-divider.theme--dark {
/* make the v-divider standout more */
border-color: hsla(0,0%,100%,.35) !important;
Expand Down
11 changes: 5 additions & 6 deletions jdaviz/components/plugin_plot.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<template>
<div class="plugin-plot-component">
<v-row style="margin: 0px 0px -8px 0px !important" justify="end">
<div style="line-height: 64px; width=32px" class="only-show-in-tray">
<div class="plugin-plot-component" style="margin-bottom: 40px">
<v-row dense style="margin: 0px 0px -8px 0px !important">
<jupyter-widget class='plugin-nested-toolbar' :widget="toolbar"></jupyter-widget>
<v-spacer></v-spacer>
<div style="line-height: 40px; width=32px" class="only-show-in-tray">
<j-plugin-popout :popout_button="popout_button"></j-plugin-popout>
</div>
</v-row>

<v-row style="margin: -16px 0px 8px 0px !important">
<jupyter-widget :widget="figure" style="height: 100%; width: 100%" />
</v-row>

</div>
</template>

<script>
</script>


<style scoped>
.only-show-in-tray {
display: none;
Expand All @@ -33,5 +33,4 @@
width: 100%;
height: 480px;
}
</style>
11 changes: 7 additions & 4 deletions jdaviz/components/toolbar_nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ def __init__(self, viewer, tools_nested, default_tool_priority=[]):
self.default_tool_priority = default_tool_priority
self._handle_default_tool()

for msg in (AddDataMessage, RemoveDataMessage, ViewerAddedMessage, ViewerRemovedMessage,
SpectralMarksChangedMessage):
self.viewer.hub.subscribe(self, msg,
handler=lambda _: self._update_tool_visibilities())
# toolbars in the main app viewers need to respond to the data-collection, etc,
# but those in plugins do not
if hasattr(self.viewer, 'hub'):
for msg in (AddDataMessage, RemoveDataMessage, ViewerAddedMessage, ViewerRemovedMessage,
SpectralMarksChangedMessage):
self.viewer.hub.subscribe(self, msg,
handler=lambda _: self._update_tool_visibilities())

def _is_visible(self, tool_id):
# tools can optionally implement self.is_visible(). If not NotImplementedError
Expand Down
6 changes: 3 additions & 3 deletions jdaviz/components/toolbar_nested.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<v-tooltip v-for="[id, {tooltip, img, menu_ind, has_suboptions, primary, visible}] of Object.entries(tools_data)" v-if="primary && visible" bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon :value="id" style="min-width: 40px !important" @contextmenu="(e) => show_submenu(e, has_suboptions, menu_ind)">
<img :src="img" width="20px" @click.ctrl.stop=""/>
<v-icon small v-if="has_suboptions" class="suboptions-carrot" @click.ctrl.stop="">mdi-menu-down</v-icon>
<img class="invert-if-dark" :src="img" width="20px" @click.ctrl.stop=""/>
<v-icon small v-if="has_suboptions" class="suboptions-carrot invert-if-dark" @click.ctrl.stop="">mdi-menu-down</v-icon>
</v-btn>
</template>
<span>{{ tooltip }}{{has_suboptions ? " [right-click for alt. tools]" : ""}}</span>
Expand Down Expand Up @@ -81,7 +81,7 @@
}
</script>

<style>
<style scoped>
.suboptions-carrot {
transform: rotate(-45deg);
bottom: 0px;
Expand Down
99 changes: 37 additions & 62 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from traitlets import Any, Dict, Float, Bool, Int, List, Unicode, observe

from glue.core.subset_group import GroupedSubset
import bqplot
from glue.config import stretches
from glue.viewers.scatter.state import ScatterViewerState
from glue.viewers.profile.state import ProfileViewerState, ProfileLayerState
Expand Down Expand Up @@ -403,8 +402,13 @@ def state_attr_for_line_visible(state):
'stretch_vmax_value', 'stretch_vmax_sync',
state_filter=is_image)

self.stretch_histogram = Plot(self)
self.stretch_histogram.add_bins('histogram', color='gray')
self.stretch_histogram = Plot(self, viewer_type='histogram')
# NOTE: this is a current workaround so the histogram viewer doesn't crash when replacing
# data. Note also that passing x=[0] fails on SOME machines, so we'll pass [0, 1] instead
self.stretch_histogram._add_data('ref', x=[0, 1])
self.stretch_histogram.layers['ref'].state.visible = False
self.stretch_histogram._add_data('histogram', x=[0, 1])

self.stretch_histogram.add_line('vmin', x=[0, 0], y=[0, 1], ynorm=True, color='#c75d2c')
self.stretch_histogram.add_line('vmax', x=[0, 0], y=[0, 1], ynorm=True, color='#c75d2c')
self.stretch_histogram.figure.axes[0].label = 'pixel value'
Expand Down Expand Up @@ -606,16 +610,6 @@ def _update_stretch_histogram(self, msg={}):
y_max = y_limits.max()

arr = comp.data[y_min:y_max, x_min:x_max]

size = arr.shape[0] * arr.shape[1]
if size > 400**2:
xstep = max(1, round(arr.shape[1] / 400))
ystep = max(1, round(arr.shape[0] / 400))
arr = arr[::ystep, ::xstep]
stretch_hist_downsampled = [size, arr.shape[0] * arr.shape[1]]
else:
stretch_hist_downsampled = size

sub_data = arr.ravel()

else:
Expand All @@ -635,49 +629,34 @@ def _update_stretch_histogram(self, msg={}):

sub_data = comp.data[inds].ravel()

# downsampling not currently implemented for 2d spectrum
stretch_hist_downsampled = len(sub_data)

else:
# include all data, regardless of zoom limits
arr = comp.data
size = arr.shape[0] * arr.shape[1]
if size > 400**2:
xstep = max(1, round(arr.shape[1] / 400))
ystep = max(1, round(arr.shape[0] / 400))
arr = arr[::ystep, ::xstep]
stretch_hist_downsampled = [size, arr.shape[0] * arr.shape[1]]
else:
stretch_hist_downsampled = size

sub_data = arr.ravel()

# filter out nans (or else bqplot will fail)
if np.any(np.isnan(sub_data)):
sub_data = sub_data[~np.isnan(sub_data)]

hist_mark = self.stretch_histogram.marks['histogram']
with hist_mark.hold_sync():
hist_mark.sample = sub_data
self.stretch_histogram._update_data('histogram', x=sub_data)

if len(sub_data) > 0:
interval = PercentileInterval(95)
if len(sub_data) > 0:
hist_lims = interval.get_limits(sub_data)
hist_mark.min, hist_mark.max = hist_lims
# set the stepsize for vmin/vmax to be approximately 1% of the range of the
# histogram (within the percentile interval), rounded to 1-2 significant digits
# to avoid random step sizes. This logic is somewhat arbitrary and can be safely
# modified or eventually exposed to the user if that would be useful.
stretch_vstep = (hist_lims[1] - hist_lims[0]) / 100.
self.stretch_vstep = np.round(stretch_vstep, decimals=-int(np.log10(stretch_vstep))+1) # noqa
hist_mark.bins = self.stretch_hist_nbins
# in case only the sample has changed but its length has not,
# we'll force the traitlet to trigger a change
hist_mark.send_state('sample')
if isinstance(stretch_hist_downsampled, list):
title = f"{stretch_hist_downsampled[1]} of {stretch_hist_downsampled[0]} pixels"
else:
title = f"{stretch_hist_downsampled} pixels"
self.stretch_histogram.figure.title = title
hist_lims = interval.get_limits(sub_data)
# set the stepsize for vmin/vmax to be approximately 1% of the range of the
# histogram (within the percentile interval), rounded to 1-2 significant digits
# to avoid random step sizes. This logic is somewhat arbitrary and can be safely
# modified or eventually exposed to the user if that would be useful.
stretch_vstep = (hist_lims[1] - hist_lims[0]) / 100.
self.stretch_vstep = np.round(stretch_vstep, decimals=-int(np.log10(stretch_vstep))+1) # noqa

self.stretch_histogram.viewer.state.hist_x_min = hist_lims[0]
self.stretch_histogram.viewer.state.hist_x_max = hist_lims[1]

self.stretch_histogram.figure.title = f"{len(sub_data)} pixels"

# update the n_bins since this may be a new layer
self._histogram_nbins_changed()

@observe('is_active', 'stretch_vmin_value', 'stretch_vmax_value', 'layer_selected',
'stretch_hist_nbins', 'image_contrast_value', 'image_bias_value',
Expand Down Expand Up @@ -739,13 +718,7 @@ def _update_stretch_curve(self, msg=None):
opacities=[0.5],
)

# reorder marks so histogram is on top:
figure_marks = self.stretch_histogram.figure.marks
for i, fig_mark in enumerate(figure_marks):
if isinstance(fig_mark, bqplot.Bins):
hist_mark = figure_marks.pop(i)
break
self.stretch_histogram.figure.marks = figure_marks + [hist_mark]
self.stretch_histogram._refresh_marks()

@observe('stretch_vmin_value')
def _stretch_vmin_changed(self, msg=None):
Expand All @@ -756,18 +729,20 @@ def _stretch_vmax_changed(self, msg=None):
self.stretch_histogram.marks['vmax'].x = [self.stretch_vmax_value, self.stretch_vmax_value]

@observe("stretch_hist_nbins")
def _histogram_nbins_changed(self, msg):
if self.stretch_histogram is None or msg['new'] == '' or msg['new'] < 1:
def _histogram_nbins_changed(self, msg={}):
if self.stretch_histogram is None:
return
self.stretch_histogram.marks['histogram'].bins = self.stretch_hist_nbins

def set_histogram_x_limits(self, x_min=None, x_max=None):
# NOTE: leaving this out of user API until API is finalized with interactive setting
self.stretch_histogram.set_xlims(x_min, x_max)
if self.stretch_hist_nbins == '' or self.stretch_hist_nbins < 1:
return
self.stretch_histogram.viewer.state.hist_n_bin = self.stretch_hist_nbins
# for some reason, this resets the internal marks, so we need to ensure the manual
# marks are still plotted
self.stretch_histogram._refresh_marks()

def set_histogram_y_limits(self, y_min, y_max):
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_ylims(y_min, y_max)
self.stretch_histogram.set_lims(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
Original file line number Diff line number Diff line change
Expand Up @@ -72,46 +72,47 @@ def test_stretch_histogram(cubeviz_helper, spectrum1d_cube_with_uncerts):

assert po.stretch_histogram is not None

hist_mark = po.stretch_histogram.marks['histogram']
flux_cube_sample = hist_mark.sample
hist_lyr = po.stretch_histogram.layers['histogram']
flux_cube_sample = hist_lyr.layer.data['x']

# changing viewer should change results
po.viewer.selected = 'uncert-viewer'
assert not allclose(hist_mark.sample, flux_cube_sample)
assert not allclose(hist_lyr.layer.data['x'], flux_cube_sample)

po.viewer.selected = 'flux-viewer'
assert_allclose(hist_mark.sample, flux_cube_sample)
assert_allclose(hist_lyr.layer.data['x'], flux_cube_sample)

# change viewer limits
fv = cubeviz_helper.app.get_viewer('flux-viewer')
fv.state.x_max = 0.5 * fv.state.x_max
# viewer limits should not be affected by default
assert_allclose(hist_mark.sample, flux_cube_sample)
# (re-retrieve layer - it should not have changed)
hist_lyr = po.stretch_histogram.layers['histogram']
assert_allclose(hist_lyr.layer.data['x'], flux_cube_sample)

# set to listen to viewer limits, the length of the samples will change
# (in this case the layer itself has been replaced)
po.stretch_hist_zoom_limits = True
assert len(hist_mark.sample) != len(flux_cube_sample)
hist_lyr = po.stretch_histogram.layers['histogram']
assert len(hist_lyr.layer.data['x']) != len(flux_cube_sample)

po.stretch_vmin.value = 0.5
po.stretch_vmax.value = 1

assert po.stretch_histogram.marks['vmin'].x[0] == po.stretch_vmin.value
assert po.stretch_histogram.marks['vmax'].x[0] == po.stretch_vmax.value

assert hist_mark.bins == 25
assert po.stretch_histogram.viewer.state.hist_n_bin == 25
po.stretch_hist_nbins = 20
assert hist_mark.bins == 20
assert po.stretch_histogram.viewer.state.hist_n_bin == 20

po.set_histogram_x_limits(x_min=0.25, x_max=2)
assert po.stretch_histogram.figure.axes[0].scale.min == 0.25
assert po.stretch_histogram.figure.axes[0].scale.max == 2
po.set_histogram_limits(x_min=0.25, x_max=2)
assert po.stretch_histogram.viewer.state.x_min == 0.25
assert po.stretch_histogram.viewer.state.x_max == 2

po.set_histogram_y_limits(y_min=1, y_max=2)
assert po.stretch_histogram.figure.axes[1].scale.min == 1
assert po.stretch_histogram.figure.axes[1].scale.max == 2

po.stretch_vmin.value = hist_mark.min - 1
po.stretch_vmax.value = hist_mark.max + 1
po.set_histogram_limits(y_min=1, y_max=2)
assert po.stretch_histogram.viewer.state.y_min == 1
assert po.stretch_histogram.viewer.state.y_max == 2

assert po.stretch_histogram.marks['vmin'].x[0] == po.stretch_vmin.value
assert po.stretch_histogram.marks['vmax'].x[0] == po.stretch_vmax.value
Expand Down
Loading

0 comments on commit 2f24a20

Please sign in to comment.