Skip to content

Commit

Permalink
Framework for legend data menu (spacetelescope#3254)
Browse files Browse the repository at this point in the history
* API access to set/toggle layer visibility
* basic list of togglable layers in data menu UI
* remove plot options setting for toggling legend
* hide data menu behind developer flag
* wireframe for upcoming follow-ups
* transition when opening menu
* show subset icon in menu
* temporary icon labeling of sublayers
* add to existing changelog entry
* add test coverage
  • Loading branch information
kecnry authored Oct 31, 2024
1 parent a16a3a0 commit de00973
Show file tree
Hide file tree
Showing 12 changed files with 454 additions and 96 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
New Features
------------

* New design for viewer legend. [#3220]
* New design for viewer legend. [#3220, #3254]

Cubeviz
^^^^^^^
Expand Down
2 changes: 1 addition & 1 deletion jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def to_unit(self, data, cid, values, original_units, target_units):
'j-number-uncertainty': 'components/number_uncertainty.vue',
'j-plugin-popout': 'components/plugin_popout.vue',
'j-multiselect-toggle': 'components/multiselect_toggle.vue',
'j-subset-icon': 'components/subset_icon.vue',
'plugin-previews-temp-disabled': 'components/plugin_previews_temp_disabled.vue', # noqa
'plugin-table': 'components/plugin_table.vue',
'plugin-dataset-select': 'components/plugin_dataset_select.vue',
Expand Down Expand Up @@ -206,7 +207,6 @@ class ApplicationState(State):
'tray': True,
'tab_headers': True,
},
'viewer_labels': True,
'dense_toolbar': True,
'server_is_remote': False, # sets some defaults, should be set before loading the config
'context': {
Expand Down
7 changes: 5 additions & 2 deletions jdaviz/components/layer_viewer_icon_stylized.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<j-tooltip :tooltipcontent="tooltipContent(tooltip, label, visible, colormode, colors, linewidth, is_subset)">
<j-tooltip :tooltipcontent="tooltipContent(tooltip, label, visible, colormode, colors, linewidth, is_subset)" :disabled="disabled">
<v-btn
:rounded="is_subset"
@click="(e) => $emit('click', e)"
Expand All @@ -9,7 +9,10 @@
height="30px"
:disabled="disabled"
>
<span :style="'color: white; text-shadow: 0px 0px 3px black; '+borderStyle(linewidth)">
<v-icon v-if="String(icon).startsWith('mdi-')" style="color: white">
{{ icon }}
</v-icon>"
<span v-else :style="'color: white; text-shadow: 0px 0px 3px black; '+borderStyle(linewidth)">
{{ icon }}
</span>
</v-btn>
Expand Down
4 changes: 2 additions & 2 deletions jdaviz/components/plugin_switch.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<span v-if="use_eye_icon">
<v-btn icon @click.stop="$emit('update:value', !value)">
<v-btn icon @click.stop="$emit('update:value', !value); $emit('click', !value)">
<v-icon>mdi-eye{{ value ? '' : '-off' }}</v-icon>
</v-btn>
<span v-if="api_hints_enabled && api_hint" class="api-hint">
Expand All @@ -16,7 +16,7 @@
:class="api_hints_enabled && api_hint ? 'api-hint' : null"
:hint="hint"
v-model="value"
@change="$emit('update:value', $event)"
@change="$emit('update:value', $event); $emit('click', $event)"
persistent-hint>
</v-switch>
</template>
Expand Down
23 changes: 23 additions & 0 deletions jdaviz/components/subset_icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<j-tooltip v-if="subset_type == 'spatial'" span_style="display: inline-block" tooltipcontent="Spatial subset">
<v-icon dense>
mdi-chart-scatter-plot
</v-icon>
</j-tooltip>
<j-tooltip v-else-if="subset_type == 'spectral'" span_style="display: inline-block" tooltipcontent="Spectral subset">
<v-icon dense>
mdi-chart-bell-curve
</v-icon>
</j-tooltip>
<j-tooltip v-else-if="subset_type == 'temporal'" span_style="display: inline-block" tooltipcontent="Temporal subset">
<v-icon dense>
mdi-chart-line
</v-icon>
</j-tooltip>
</template>

<script>
module.exports = {
props: ['subset_type']
};
</script>
2 changes: 1 addition & 1 deletion jdaviz/components/tooltip.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ module.exports = {
return tooltips[this.$props.tipid];
},
getSpanStyle() {
return this.$props.span_style || "height: inherit; display: inherit";
return this.$props.span_style || "height: inherit; display: inherit; cursor: default";
},
getOpenDelay() {
return this.$props.delay || "0";
Expand Down
106 changes: 101 additions & 5 deletions jdaviz/configs/default/plugins/data_menu/data_menu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from traitlets import Dict, Unicode
from contextlib import contextmanager
from traitlets import Bool, Dict, Unicode, List, observe

from jdaviz.core.template_mixin import TemplateMixin, LayerSelectMixin
from jdaviz.core.user_api import UserApiWrapper
Expand All @@ -15,6 +16,9 @@ class DataMenu(TemplateMixin, LayerSelectMixin):
:ref:`public API <plugin-apis>`:
* ``layer`` (:class:`~jdaviz.core.template_mixin.LayerSelect`):
actively selected layer(s)
* :meth:`set_layer_visibility`
* :meth:`toggle_layer_visibility`
"""
template_file = __file__, "data_menu.vue"

Expand All @@ -28,27 +32,34 @@ class DataMenu(TemplateMixin, LayerSelectMixin):

cmap_samples = Dict(cmap_samples).tag(sync=True)

dm_layer_selected = List().tag(sync=True)

dev_data_menu = Bool(False).tag(sync=True)

def __init__(self, viewer, *args, **kwargs):
super().__init__(*args, **kwargs)
self._viewer = viewer
self._during_select_sync = False

# TODO: refactor how this is applied by default to go through filters directly
self.layer.remove_filter('filter_is_root')
self.layer.add_filter(is_not_wcs_only)
self.layer.multiselect = True
self.layer._default_mode = 'empty'

# first attach callback to catch any updates to viewer/layer icons and then
# set their initial state
self.hub.subscribe(self, IconsUpdatedMessage, self._on_app_icons_updated)
self.hub.subscribe(self, AddDataMessage, handler=lambda _: self.set_viewer_id())
self.hub.subscribe(self, AddDataMessage, handler=lambda _: self._set_viewer_id())
self.viewer_icons = dict(self.app.state.viewer_icons)
self.layer_icons = dict(self.app.state.layer_icons)

@property
def user_api(self):
expose = ['layer']
expose = ['layer', 'set_layer_visibility', 'toggle_layer_visibility']
return UserApiWrapper(self, expose=expose)

def set_viewer_id(self):
def _set_viewer_id(self):
# viewer_ids are not populated on the viewer at init, so we'll keep checking and set
# these the first time that they are available
if len(self.viewer_id) and len(self.viewer_reference):
Expand All @@ -65,4 +76,89 @@ def _on_app_icons_updated(self, msg):
self.viewer_icons = msg.icons
elif msg.icon_type == 'layer':
self.layer_icons = msg.icons
self.set_viewer_id()
self._set_viewer_id()

@contextmanager
def during_select_sync(self):
self._during_select_sync = True
try:
yield
except Exception: # pragma: no cover
self._during_select_sync = False
raise
self._during_select_sync = False

@observe('dm_layer_selected')
def _dm_layer_selected_changed(self, event={}):
if not hasattr(self, 'layer') or not self.layer.multiselect: # pragma: no cover
return
if self._during_select_sync:
return
if len(event.get('new')) == len(event.get('old')):
# not possible from UI interaction, but instead caused by a selected
# layer being removed (deleting a selected subset, etc). We want
# to update dm_layer_selected in order to preserve layer.selected
self._update_dm_layer_selected(event)
return
with self.during_select_sync():
# map index in dm_layer_selected (inverse order of layer_items)
# to set self.layer.selected
length = len(self.layer_items)
self.layer.selected = [self.layer_items[length-1-i]['label']
for i in self.dm_layer_selected]

@observe('layer_selected', 'layer_items')
def _update_dm_layer_selected(self, event={}):
if not hasattr(self, 'layer') or not self.layer.multiselect: # pragma: no cover
return
if self._during_select_sync:
return
with self.during_select_sync():
# map list of strings in self.layer.selected to indices in dm_layer_selected
layer_labels = [layer['label'] for layer in self.layer_items][::-1]
self.dm_layer_selected = [layer_labels.index(label) for label in self.layer.selected
if label in layer_labels]

def set_layer_visibility(self, layer_label, visible=True):
"""
Set the visibility of a layer in the viewer.
Parameters
----------
layer_label : str
The label of the layer to set the visibility of.
visible : bool
Whether the layer should be visible or not.
Returns
-------
dict
A dictionary of the current visible layers.
"""
for layer in self._viewer.layers:
if layer.layer.label == layer_label:
layer.visible = visible
elif hasattr(layer.layer, 'data') and layer.layer.data.label == layer_label:
layer.visible = layer.layer.label in self.visible_layers
return self.visible_layers

def toggle_layer_visibility(self, layer_label):
"""
Toggle the visibility of a layer in the viewer.
Parameters
----------
layer_label : str
The label of the layer to toggle the visibility of.
Returns
-------
bool
The new visibility state of the layer.
"""
visible = layer_label not in self.visible_layers
self.set_layer_visibility(layer_label, visible=visible)
return visible

def vue_set_layer_visibility(self, info, *args):
return self.set_layer_visibility(info.get('layer'), info.get('value')) # pragma: no cover
Loading

0 comments on commit de00973

Please sign in to comment.