Skip to content

Commit

Permalink
data menu infrastructure (spacetelescope#3173)
Browse files Browse the repository at this point in the history
* data menu infrastructure

* use message instead of state-callback to update viewer/layer_icons

* avoid setting viewer_ids/reference repeatedly

* replace mention to old viewer_items['visible_layers']

* allow viewers to not have a data menu instance (Mosviz TableViewer)

* code cleanup
  • Loading branch information
kecnry authored and cshanahan1 committed Sep 6, 2024
1 parent 498659f commit ae33f67
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 60 deletions.
23 changes: 18 additions & 5 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
SnackbarMessage, RemoveDataMessage,
AddDataToViewerMessage, RemoveDataFromViewerMessage,
ViewerAddedMessage, ViewerRemovedMessage,
ViewerRenamedMessage, ChangeRefDataMessage)
ViewerRenamedMessage, ChangeRefDataMessage,
IconsUpdatedMessage)
from jdaviz.core.registries import (tool_registry, tray_registry, viewer_registry,
data_parser_registry)
from jdaviz.core.tools import ICON_DIR
Expand Down Expand Up @@ -391,6 +392,12 @@ def __init__(self, configuration=None, *args, **kwargs):
handler=self._on_layers_changed)
# SubsetDeleteMessage will also call _on_layers_changed via _on_subset_delete_message

# Emit messages when icons are updated
self.state.add_callback('viewer_icons',
lambda value: self.hub.broadcast(IconsUpdatedMessage('viewer', value, sender=self))) # noqa
self.state.add_callback('layer_icons',
lambda value: self.hub.broadcast(IconsUpdatedMessage('layer', value, sender=self))) # noqa

def _on_plugin_table_added(self, msg):
if msg.plugin._plugin_name is None:
# plugin was instantiated after the app was created, ignore
Expand Down Expand Up @@ -2484,7 +2491,14 @@ def _create_viewer_item(self, viewer, vid=None, name=None, reference=None,
# own attribute instead.
viewer._reference_id = vid # For reverse look-up

self.state.viewer_icons.setdefault(vid, len(self.state.viewer_icons)+1)
if vid not in self.state.viewer_icons:
self.state.viewer_icons = {**self.state.viewer_icons,
vid: len(self.state.viewer_icons) + 1}
# for some reason that state callback is not triggering this
self.hub.broadcast(IconsUpdatedMessage('viewer',
self.state.viewer_icons,
sender=self)
)

wcs_only_layers = getattr(viewer.state, 'wcs_only_layers', [])

Expand All @@ -2497,10 +2511,9 @@ def _create_viewer_item(self, viewer, vid=None, name=None, reference=None,
'name': name or vid,
'widget': "IPY_MODEL_" + viewer.figure_widget.model_id,
'toolbar': "IPY_MODEL_" + viewer.toolbar.model_id if viewer.toolbar else '', # noqa
'layer_options': "IPY_MODEL_" + viewer.layer_options.model_id,
'viewer_options': "IPY_MODEL_" + viewer.viewer_options.model_id,
'data_menu': 'IPY_MODEL_' + viewer._data_menu.model_id if hasattr(viewer, '_data_menu') else '', # noqa
# TODO: remove unused entries after old data menu deprecation period
'selected_data_items': {}, # noqa data_id: visibility state (visible, hidden, mixed), READ-ONLY
'visible_layers': {}, # label: {color}, READ-ONLY
'wcs_only_layers': wcs_only_layers,
'reference_data_label': reference_data_label,
'canvas_angle': 0, # canvas rotation clockwise rotation angle in deg
Expand Down
1 change: 1 addition & 0 deletions jdaviz/configs/default/plugins/data_menu/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .data_menu import * # noqa
52 changes: 52 additions & 0 deletions jdaviz/configs/default/plugins/data_menu/data_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from traitlets import Dict, Unicode

from jdaviz.core.template_mixin import TemplateMixin
from jdaviz.core.user_api import UserApiWrapper
from jdaviz.core.events import IconsUpdatedMessage

__all__ = ['DataMenu']


class DataMenu(TemplateMixin):
"""Viewer Data Menu"""
template_file = __file__, "data_menu.vue"

viewer_id = Unicode().tag(sync=True)
viewer_reference = Unicode().tag(sync=True)

layer_icons = Dict().tag(sync=True) # read-only, see app.state.layer_icons
viewer_icons = Dict().tag(sync=True) # read-only, see app.state.viewer_icons

visible_layers = Dict().tag(sync=True) # read-only, set by viewer

def __init__(self, viewer, *args, **kwargs):
super().__init__(*args, **kwargs)
self._viewer = viewer
# 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.viewer_icons = dict(self.app.state.viewer_icons)
self.layer_icons = dict(self.app.state.layer_icons)

@property
def user_api(self):
expose = []
return UserApiWrapper(self, expose=expose)

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):
return
try:
self.viewer_id = getattr(self._viewer, '_reference_id', '')
self.viewer_reference = self._viewer.reference
except AttributeError:
return

def _on_app_icons_updated(self, msg):
if msg.icon_type == 'viewer':
self.viewer_icons = msg.icons
elif msg.icon_type == 'layer':
self.layer_icons = msg.icons
self.set_viewer_id()
43 changes: 43 additions & 0 deletions jdaviz/configs/default/plugins/data_menu/data_menu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<div>
<div v-if="Object.keys(viewer_icons).length > 1" class="viewer-label invert-if-dark">
<j-tooltip span_style="white-space: nowrap">
<j-layer-viewer-icon span_style="float: right;" :icon="viewer_icons[viewer_id]"></j-layer-viewer-icon>
</j-tooltip>
<span class="invert-if-dark" style="margin-left: 24px; margin-right: 32px; line-height: 24px">{{viewer_reference || viewer_id}}</span>
</div>

<div v-for="(layer_info, layer_name) in visible_layers" class="viewer-label invert-if-dark">
<j-tooltip span_style="white-space: nowrap">
<j-layer-viewer-icon span_style="float: right;" :icon="layer_icons[layer_name]" :linewidth="layer_info.linewidth" :linestyle="'solid'" :color="layer_info.color"></j-layer-viewer-icon>
</j-tooltip>
<span class="invert-if-dark" style="margin-left: 24px; margin-right: 32px; line-height: 24px">
<v-icon v-if="layer_info.prefix_icon" dense>
{{layer_info.prefix_icon}}
</v-icon>
{{layer_name}}
</span>
</div>
</div>
</template>

<style scoped>
.viewer-label {
display: block;
float: right;
background-color: #c3c3c3c3;
width: 24px;
overflow: hidden;
white-space: nowrap;
/*cursor: pointer;*/
}
.viewer-label:last-child {
padding-bottom: 2px;
}
.viewer-label:hover {
background-color: #e5e5e5;
width: auto;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
}
</style>
15 changes: 13 additions & 2 deletions jdaviz/configs/default/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from specutils import Spectrum1D

from jdaviz.components.toolbar_nested import NestedJupyterToolbar
from jdaviz.configs.default.plugins.data_menu import DataMenu
from jdaviz.core.astrowidgets_api import AstrowidgetsImageViewerMixin
from jdaviz.core.events import SnackbarMessage
from jdaviz.core.freezable_state import FreezableProfileViewerState
Expand Down Expand Up @@ -61,10 +62,15 @@ def __init__(self, *args, **kwargs):
# Allow each viewer to cycle through colors for each new addition to the viewer:
self.color_cycler = ColorCycler()

self._data_menu = DataMenu(viewer=self, app=self.jdaviz_app)

@property
def user_api(self):
# default exposed user APIs. Can override this method in any particular viewer.
if not isinstance(self, TableViewer):
# TODO: eventually remove data_labels, data_labels_loaded,
# and data_labels_visible once deprecation period passes
# TODO: add data_menu once API is finalized and ready to be made public
expose = ['data_labels', 'data_labels_loaded', 'data_labels_visible']
else:
expose = []
Expand All @@ -88,6 +94,11 @@ def user_api(self):
return ViewerUserApi(self, expose=expose)

@property
def data_menu(self):
return self._data_menu.user_api

@property
# TODO: deprecate in favor of viewer.data_menu.layers_loaded
def data_labels_loaded(self):
"""
List of data labels loaded in this viewer.
Expand All @@ -103,6 +114,7 @@ def data_labels_loaded(self):
if self.jdaviz_app._get_data_item_by_id(data_id) is not None]

@property
# TODO: deprecate in favor of viewer.data_menu.layers_visible
def data_labels_visible(self):
"""
List of data labels visible in this viewer.
Expand Down Expand Up @@ -255,8 +267,7 @@ def _get_layer_info(layer):
'linewidth': _get_layer_linewidth(layer),
'prefix_icon': prefix_icon}

viewer_item = self.jdaviz_app._viewer_item_by_id(self.reference_id)
viewer_item['visible_layers'] = visible_layers
self._data_menu.visible_layers = visible_layers

def _on_layers_update(self, layers=None):
if self.__class__.__name__ == 'MosvizTableViewer':
Expand Down
60 changes: 12 additions & 48 deletions jdaviz/container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,9 @@
</div>

<v-card tile flat style="flex: 1; margin-top: -2px; overflow: hidden;">
<div v-if="app_settings.viewer_labels" class='viewer-label-container'>
<div v-if="Object.keys(viewer_icons).length > 1" class="viewer-label invert-if-dark">
<j-tooltip span_style="white-space: nowrap">
<j-layer-viewer-icon span_style="float: right;" :icon="viewer_icons[viewer.id]" :linked_by_wcs="viewer.linked_by_wcs"></j-layer-viewer-icon>
</j-tooltip>
<span class="invert-if-dark" style="margin-left: 24px; margin-right: 32px; line-height: 24px">{{viewer.reference || viewer.id}}</span>
</div>

<div v-for="(layer_info, layer_name) in viewer.visible_layers" class="viewer-label invert-if-dark">
<j-tooltip span_style="white-space: nowrap">
<j-layer-viewer-icon span_style="float: right;" :icon="layer_icons[layer_name]" :linewidth="layer_info.linewidth" :linestyle="'solid'" :color="layer_info.color" :linked_by_wcs="viewer.linked_by_wcs"></j-layer-viewer-icon>
</j-tooltip>
<span class="invert-if-dark" style="margin-left: 24px; margin-right: 32px; line-height: 24px">
<v-icon v-if="layer_info.prefix_icon" dense>
{{layer_info.prefix_icon}}
</v-icon>
{{layer_name}}
</span>
</div>
<div class="viewer-label-container">
<jupyter-widget :widget="viewer.data_menu" v-if="app_settings.viewer_labels"></jupyter-widget>
</div>

<jupyter-widget
:widget="viewer.widget"
:ref="'viewer-widget-'+viewer.id"
Expand All @@ -93,34 +75,16 @@
</template>

<style>
.viewer-label-container {
position: absolute;
right: 0;
z-index: 1;
width: 24px;
}
.viewer-label {
display: block;
float: right;
background-color: #c3c3c3c3;
width: 24px;
overflow: hidden;
white-space: nowrap;
/*cursor: pointer;*/
}
.viewer-label:last-child {
padding-bottom: 2px;
}
.viewer-label:hover {
background-color: #e5e5e5;
width: auto;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
}
.imviz div.v-card.v-card--flat.v-sheet.v-sheet--tile {
/* black background beyond edges of canvas for canvas rotation */
background-color: black
}
.viewer-label-container {
position: absolute;
right: 0;
z-index: 1;
width: 24px;
}
.imviz div.v-card.v-card--flat.v-sheet.v-sheet--tile {
/* black background beyond edges of canvas for canvas rotation */
background-color: black
}
</style>

<script>
Expand Down
13 changes: 12 additions & 1 deletion jdaviz/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
'CanvasRotationChangedMessage',
'GlobalDisplayUnitChanged', 'ChangeRefDataMessage',
'PluginTableAddedMessage', 'PluginTableModifiedMessage',
'PluginPlotAddedMessage', 'PluginPlotModifiedMessage']
'PluginPlotAddedMessage', 'PluginPlotModifiedMessage',
'IconsUpdatedMessage']


class NewViewerMessage(Message):
Expand Down Expand Up @@ -464,3 +465,13 @@ class PluginPlotModifiedMessage(PluginPlotAddedMessage):
'''Message generated when the items in a plugin plot are changed'''
def __init__(self, sender):
super().__init__(sender)


class IconsUpdatedMessage(Message):
'''Message generated when the viewer or layer icons are updated'''
def __init__(self, icon_type, icons, **kwargs):
# icon_type = 'layer' or 'viewer'
super().__init__(**kwargs)
self.icon_type = icon_type
# icons might be a CallbackDict, cast to ensure its a dictionary
self.icons = dict(icons)
7 changes: 3 additions & 4 deletions jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def show(self, loc="inline", title=None): # pragma: no cover
Parameters
----------
loc : str
The display location determines where to present the viz app.
The display location determines where to present the plugin UI.
Supported locations:
"inline": Display the plugin inline in a notebook.
Expand Down Expand Up @@ -3887,14 +3887,13 @@ def add_results_from_plugin(self, data_item, replace=None, label=None):
for viewer_select_item in self.add_to_viewer_items[1:]:
# index 0 is for "None"
viewer_ref = viewer_select_item['reference']
viewer_item = self.app._viewer_item_by_reference(viewer_ref)
viewer = self.app.get_viewer(viewer_ref)
for layer in viewer.layers:
if layer.layer.label != label:
continue
else:
add_to_viewer_refs.append(viewer_ref)
add_to_viewer_vis.append(label in viewer_item['visible_layers'])
add_to_viewer_vis.append(label in viewer._data_menu.visible_layers)
preserve_these = {}
for att in layer.state.as_dict():
# Can't set cmap_att, size_att, etc
Expand Down Expand Up @@ -4421,7 +4420,7 @@ def show(self, loc="inline", title=None): # pragma: no cover
Parameters
----------
loc : str
The display location determines where to present the viz app.
The display location determines where to present the component UI.
Supported locations:
"inline": Display the component inline in a notebook.
Expand Down

0 comments on commit ae33f67

Please sign in to comment.