-
Notifications
You must be signed in to change notification settings - Fork 76
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
Spatial-Spectral highlighting #1528
Changes from all commits
3d02783
c79a226
8ce6581
bfc8c2b
c023766
f6808b1
bab566a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
import numpy as np | ||
from glue.core import BaseData | ||
from glue.core.subset import RoiSubsetState, RangeSubsetState | ||
from glue_jupyter.bqplot.image import BqplotImageView | ||
|
||
from jdaviz.core.registries import viewer_registry | ||
from jdaviz.core.marks import SliceIndicatorMarks | ||
from jdaviz.core.marks import SliceIndicatorMarks, ShadowSpatialSpectral | ||
from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin | ||
from jdaviz.configs.cubeviz.helper import layer_is_cube_image_data | ||
from jdaviz.configs.imviz.helper import data_has_valid_wcs | ||
|
@@ -197,6 +198,63 @@ def __init__(self, *args, **kwargs): | |
# default_tool_priority=['jdaviz:selectslice'] | ||
super().__init__(*args, **kwargs) | ||
|
||
def _get_spatial_subset_layers(self): | ||
return [ls for ls in self.state.layers | ||
if isinstance(getattr(ls.layer, 'subset_state', None), RoiSubsetState)] | ||
|
||
def _get_spectral_subset_layers(self): | ||
return [ls for ls in self.state.layers | ||
if isinstance(getattr(ls.layer, 'subset_state', None), RangeSubsetState)] | ||
|
||
def _get_marks_for_layers(self, layers): | ||
layers_list = list(self.state.layers) | ||
# here we'll assume that all custom marks are subclasses of Lines/GL but don't directly | ||
# use Lines/LinesGL (so an isinstance check won't be sufficient here) | ||
layer_marks = self.native_marks | ||
# and now we'll assume that the marks are in the same order as the layers, this should | ||
# be the case as long as the order isn't manually resorted | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hopefully we remember this when/if we introduce layer reordering. |
||
return [layer_marks[layers_list.index(layer)] for layer in layers] | ||
|
||
def _on_subset_delete(self, msg): | ||
# delete any ShadowSpatialSpectral mark for which either of the spectral or spatial marks | ||
# no longer exists | ||
spectral_marks = self._get_marks_for_layers(self._get_spectral_subset_layers()) | ||
spatial_marks = self._get_marks_for_layers(self._get_spatial_subset_layers()) | ||
pllim marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.figure.marks = [m for m in self.figure.marks | ||
if not (isinstance(m, ShadowSpatialSpectral) and | ||
(m.spatial_spectrum_mark in spatial_marks or | ||
m.spectral_subset_mark in spectral_marks))] | ||
|
||
def _expected_subset_layer_default(self, layer_state): | ||
""" | ||
This gets called whenever the layer of an expected new subset is added, we want to set the | ||
default for the linewidth depending on whether it is spatial or spectral, and handle | ||
creating any necessary marks for spatial-spectral subset intersections. | ||
""" | ||
super()._expected_subset_layer_default(layer_state) | ||
pllim marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
this_mark = self._get_marks_for_layers([layer_state])[0] | ||
new_marks = [] | ||
|
||
if isinstance(layer_state.layer.subset_state, RoiSubsetState): | ||
layer_state.linewidth = 1 | ||
|
||
# need to add marks for every intersection between THIS spatial subset and ALL spectral | ||
spectral_marks = self._get_marks_for_layers(self._get_spectral_subset_layers()) | ||
for spectral_mark in spectral_marks: | ||
new_marks += [ShadowSpatialSpectral(this_mark, spectral_mark)] | ||
|
||
else: | ||
layer_state.linewidth = 3 | ||
|
||
# need to add marks for every intersection between THIS spectral subset and ALL spatial | ||
spatial_marks = self._get_marks_for_layers(self._get_spatial_subset_layers()) | ||
for spatial_mark in spatial_marks: | ||
new_marks += [ShadowSpatialSpectral(spatial_mark, this_mark)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the coverage say this is not covered? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, because the existing test in Edit: added the test, let's hope it now says this line is covered 🤞 |
||
|
||
# NOTE: += or append won't pick up on change | ||
self.figure.marks = self.figure.marks + new_marks | ||
|
||
@property | ||
def slice_indicator(self): | ||
for mark in self.figure.marks: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
from astropy import units as u | ||
from bqplot import LinearScale | ||
from bqplot.marks import Lines, Label, Scatter | ||
from copy import deepcopy | ||
from glue.core import HubListener | ||
from specutils import Spectrum1D | ||
|
||
|
@@ -326,19 +327,32 @@ class ShadowMixin: | |
|
||
Can manually override ``_on_shadowing_changed`` for more advanced logic cases. | ||
""" | ||
def _get_id(self, mark): | ||
return getattr(mark, '_model_id', None) | ||
|
||
def _setup_shadowing(self, shadowing, sync_traits=[], other_traits=[]): | ||
self._shadowing = shadowing | ||
self._sync_traits = sync_traits | ||
""" | ||
sync_traits: traits to set now, and mirror any changes to shadowing in the future | ||
other_trait: traits to set now, but not mirror in the future | ||
""" | ||
if not hasattr(self, '_shadowing'): | ||
self._shadowing = {} | ||
self._sync_traits = {} | ||
shadowing_id = self._get_id(shadowing) | ||
self._shadowing[shadowing_id] = shadowing | ||
self._sync_traits[shadowing_id] = sync_traits | ||
|
||
# sync initial values | ||
for attr in sync_traits + other_traits: | ||
self._on_shadowing_changed({'name': attr, 'new': getattr(shadowing, attr)}) | ||
self._on_shadowing_changed({'name': attr, | ||
'new': getattr(shadowing, attr), | ||
'owner': shadowing}) | ||
|
||
# subscribe to future changes | ||
shadowing.observe(self._on_shadowing_changed) | ||
|
||
def _on_shadowing_changed(self, change): | ||
if change['name'] in self._sync_traits: | ||
if change['name'] in self._sync_traits.get(self._get_id(change.get('owner')), []): | ||
setattr(self, change['name'], change['new']) | ||
return | ||
Comment on lines
+330
to
357
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this all extends the existing ShadowMixin to be able to shadow multiple marks simultaneously, the API remains the same, so should not affect other marks that make use of this, but we should thoroughly test those (line analysis continuum marks, slice indicator labels, etc) |
||
|
||
|
@@ -359,13 +373,48 @@ def __init__(self, shadowing, shadow_width=1, **kwargs): | |
['scales', 'x', 'y', 'visible', 'line_style', 'marker'], | ||
['stroke_width', 'marker_size']) | ||
|
||
|
||
class ShadowSpatialSpectral(Lines, HubListener, ShadowMixin): | ||
""" | ||
Shadow the mark of a spatial subset collapsed spectrum, with the mask from a spectral subset, | ||
and the styling from the spatial subset. | ||
""" | ||
def __init__(self, spatial_spectrum_mark, spectral_subset_mark): | ||
# spatial_spectrum_mark: Lines mark corresponding to the spatially-collapsed spectrum | ||
# from a spatial subset | ||
# spectral_subset_mark: Lines mark on the FULL cube corresponding to the glue-highlight | ||
# of the spectral subset | ||
super().__init__(scales=spatial_spectrum_mark.scales, marker=None) | ||
|
||
self._spatial_mark_id = self._get_id(spatial_spectrum_mark) | ||
self._setup_shadowing(spatial_spectrum_mark, | ||
['scales', 'y', 'visible', 'line_style'], | ||
['x']) | ||
|
||
self._spectral_mark_id = self._get_id(spectral_subset_mark) | ||
self._setup_shadowing(spectral_subset_mark, | ||
['stroke_width', 'x', 'y', 'visible', 'opacities', 'colors']) | ||
|
||
@property | ||
def spatial_spectrum_mark(self): | ||
return self._shadowing[self._spatial_mark_id] | ||
|
||
@property | ||
def spectral_subset_mark(self): | ||
return self._shadowing[self._spectral_mark_id] | ||
|
||
def _on_shadowing_changed(self, change): | ||
super()._on_shadowing_changed(change) | ||
if change['name'] in ['stroke_width', 'marker_size']: | ||
# apply the same, but increased by the shadow width | ||
setattr(self, change['name'], | ||
change['new'] + self._shadow_width if change['new'] else 0) | ||
return | ||
if hasattr(self, '_spectral_mark_id'): | ||
if change['name'] == 'y': | ||
# force a copy or else we'll overwrite the mask to the spatial mark! | ||
change['new'] = deepcopy(self.spatial_spectrum_mark.y) | ||
change['new'][np.isnan(self.spectral_subset_mark.y)] = np.nan | ||
|
||
elif change['name'] == 'visible': | ||
# only show if BOTH shadowing marks are set to visible | ||
change['new'] = self.spectral_subset_mark.visible and self.spatial_spectrum_mark.visible # noqa | ||
|
||
return super()._on_shadowing_changed(change) | ||
|
||
|
||
class ShadowLabelFixedY(Label, ShadowMixin): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we combine this into one and swap the class to be compared depending on a keyword?