diff --git a/CHANGES.rst b/CHANGES.rst
index fe5aa7216f..131aafbd33 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,7 +4,7 @@
New Features
------------
-* New design for viewer legend. [#3220, #3254]
+* New design for viewer legend. [#3220, #3254, #3263]
Cubeviz
^^^^^^^
diff --git a/jdaviz/app.py b/jdaviz/app.py
index 5576d84202..bcc0c405b2 100644
--- a/jdaviz/app.py
+++ b/jdaviz/app.py
@@ -151,7 +151,8 @@ def to_unit(self, data, cid, values, original_units, target_units):
'plugin-slider': 'components/plugin_slider.vue',
'plugin-color-picker': 'components/plugin_color_picker.vue',
'plugin-input-header': 'components/plugin_input_header.vue',
- 'glue-state-sync-wrapper': 'components/glue_state_sync_wrapper.vue'}
+ 'glue-state-sync-wrapper': 'components/glue_state_sync_wrapper.vue',
+ 'data-menu-add-data': 'components/data_menu_add_data.vue'}
_verbosity_levels = ('debug', 'info', 'warning', 'error')
diff --git a/jdaviz/components/data_menu_add_data.vue b/jdaviz/components/data_menu_add_data.vue
new file mode 100644
index 0000000000..05887060a7
--- /dev/null
+++ b/jdaviz/components/data_menu_add_data.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+ mdi-plus
+
+
+
+
+ Load Data
+
+
+
+ {$emit('add-data', data.label)}"
+ >
+ {{ data.label }}
+
+
+
+
+ Create Subset
+
+
+
+ {$emit('create-subset', tool.name)}"
+ >
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jdaviz/components/toolbar_nested.py b/jdaviz/components/toolbar_nested.py
index 56f7843777..7f15932602 100644
--- a/jdaviz/components/toolbar_nested.py
+++ b/jdaviz/components/toolbar_nested.py
@@ -156,6 +156,39 @@ def _on_change_v_model(self, event):
if event['old'] is not None:
self.active_tool_id = event['old']
+ def _select_tool(self, tool_id, menu_ind):
+ for search_tool_id, info in self.tools_data.items():
+ if info['menu_ind'] == menu_ind and info['primary']:
+ prev_id = search_tool_id
+ prev_info = info
+ break
+ else:
+ raise ValueError("could not find previous selection")
+
+ if isinstance(self.tools[tool_id], CheckableTool):
+ # only switch to primary if its actually checkable, otherwise
+ # just activate once
+ self.tools_data = {
+ **self.tools_data,
+ prev_id: {
+ **prev_info,
+ 'primary': False
+ },
+ tool_id: {
+ **self.tools_data[tool_id],
+ 'primary': True
+ }
+ }
+
+ # and finally, set to be the active tool (this triggers _on_change_v_model which in turn
+ # triggers BasicJupyterToolbar._on_change_active_tool)
+ self.active_tool_id = tool_id
+
+ def select_tool(self, tool_id):
+ # find the previous primary tool in the same menu_ind
+ menu_ind = self.tools_data[tool_id]['menu_ind']
+ self._select_tool(tool_id, menu_ind)
+
def add_tool(self, tool, menu_ind, has_suboptions=True, primary=False, visible=True):
# NOTE: this method is essentially copied from glue-jupyter's BasicJupyterToolbar,
# but we need extra values in the tools_data dictionary. We could call super(),
@@ -183,29 +216,4 @@ def vue_select_primary(self, args):
Activate the primary tool from a given menu index
"""
menu_ind, tool_id = args
- for search_tool_id, info in self.tools_data.items():
- if info['menu_ind'] == menu_ind and info['primary']:
- prev_id = search_tool_id
- prev_info = info
- break
- else:
- raise ValueError("could not find previous selection")
-
- if isinstance(self.tools[tool_id], CheckableTool):
- # only switch to primary if its actually checkable, otherwise
- # just activate once
- self.tools_data = {
- **self.tools_data,
- prev_id: {
- **prev_info,
- 'primary': False
- },
- tool_id: {
- **self.tools_data[tool_id],
- 'primary': True
- }
- }
-
- # and finally, set to be the active tool (this triggers _on_change_v_model which in turn
- # triggers BasicJupyterToolbar._on_change_active_tool)
- self.active_tool_id = tool_id
+ self._select_tool(tool_id, menu_ind)
diff --git a/jdaviz/configs/cubeviz/plugins/viewers.py b/jdaviz/configs/cubeviz/plugins/viewers.py
index 0bc60c5903..96238b96ad 100644
--- a/jdaviz/configs/cubeviz/plugins/viewers.py
+++ b/jdaviz/configs/cubeviz/plugins/viewers.py
@@ -39,6 +39,8 @@ def __init__(self, *args, **kwargs):
# Hide axes by default
self.state.show_axes = False
+ self.data_menu._obj.dataset.add_filter('is_cube_or_image')
+
@property
def _default_spectrum_viewer_reference_name(self):
return self.jdaviz_helper._default_spectrum_viewer_reference_name
diff --git a/jdaviz/configs/default/plugins/data_menu/data_menu.py b/jdaviz/configs/default/plugins/data_menu/data_menu.py
index 1d3f0fe8a0..5c4acb930b 100644
--- a/jdaviz/configs/default/plugins/data_menu/data_menu.py
+++ b/jdaviz/configs/default/plugins/data_menu/data_menu.py
@@ -1,7 +1,7 @@
from contextlib import contextmanager
from traitlets import Bool, Dict, Unicode, List, observe
-from jdaviz.core.template_mixin import TemplateMixin, LayerSelectMixin
+from jdaviz.core.template_mixin import (TemplateMixin, LayerSelectMixin, DatasetSelectMixin)
from jdaviz.core.user_api import UserApiWrapper
from jdaviz.core.events import IconsUpdatedMessage, AddDataMessage
from jdaviz.utils import cmap_samples, is_not_wcs_only
@@ -9,7 +9,17 @@
__all__ = ['DataMenu']
-class DataMenu(TemplateMixin, LayerSelectMixin):
+SUBSET_TOOL_IDS = {'circle': 'bqplot:truecircle',
+ 'rectangle': 'bqplot:rectangle',
+ 'ellipse': 'bqplot:ellipse',
+ 'annulus': 'bqplot:circannulus',
+ 'xrange': 'bqplot:xrange',
+ 'yrange': 'bqplot:yrange'}
+
+SUBSET_NAMES = {v: k for k, v in SUBSET_TOOL_IDS.items()}
+
+
+class DataMenu(TemplateMixin, LayerSelectMixin, DatasetSelectMixin):
"""Viewer Data Menu
Only the following attributes and methods are available through the
@@ -19,6 +29,8 @@ class DataMenu(TemplateMixin, LayerSelectMixin):
actively selected layer(s)
* :meth:`set_layer_visibility`
* :meth:`toggle_layer_visibility`
+ * :meth:`create_subset`
+ * :meth:`add_data`
"""
template_file = __file__, "data_menu.vue"
@@ -31,6 +43,7 @@ class DataMenu(TemplateMixin, LayerSelectMixin):
visible_layers = Dict().tag(sync=True) # read-only, set by viewer
cmap_samples = Dict(cmap_samples).tag(sync=True)
+ subset_tools = List().tag(sync=True)
dm_layer_selected = List().tag(sync=True)
@@ -47,6 +60,13 @@ def __init__(self, viewer, *args, **kwargs):
self.layer.multiselect = True
self.layer._default_mode = 'empty'
+ # we'll use a modified version of the dataset mixin to have a filtered
+ # list of data entries in the app that are not in the current viewer.
+ # changing the selection has no consequence.
+ def data_not_in_viewer(data):
+ return data.label not in self.layer.choices
+ self.dataset.filters = ['is_not_wcs_only', 'not_child_layer', data_not_in_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)
@@ -54,11 +74,24 @@ def __init__(self, viewer, *args, **kwargs):
self.viewer_icons = dict(self.app.state.viewer_icons)
self.layer_icons = dict(self.app.state.layer_icons)
+ # this currently assumes that toolbar.tools_data is set at init and does not change
+ # if we ever support dynamic tool registration, this will need to be updated
+ self.subset_tools = [{'id': k, 'img': v['img'], 'name': SUBSET_NAMES.get(k, k)}
+ for k, v in self._viewer.toolbar.tools_data.items()
+ if k in SUBSET_TOOL_IDS.values()]
+
@property
def user_api(self):
- expose = ['layer', 'set_layer_visibility', 'toggle_layer_visibility']
+ expose = ['layer', 'set_layer_visibility', 'toggle_layer_visibility',
+ 'create_subset', 'add_data']
return UserApiWrapper(self, expose=expose)
+ @observe('layer_items')
+ def _update_data_not_in_viewer(self, msg):
+ # changing the layers in the viewer needs to trigger an update to dataset_items
+ # through the set filters
+ self.dataset._on_data_changed()
+
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
@@ -162,3 +195,39 @@ def toggle_layer_visibility(self, layer_label):
def vue_set_layer_visibility(self, info, *args):
return self.set_layer_visibility(info.get('layer'), info.get('value')) # pragma: no cover
+
+ def add_data(self, data_label):
+ """
+ Add a dataset to the viewer.
+
+ Parameters
+ ----------
+ data_label : str
+ The label of the dataset to add to the viewer.
+ """
+ if data_label not in self.dataset.choices:
+ raise ValueError(f"Data label '{data_label}' not able to be loaded into '{self.viewer_id}'. Must be one of: {self.dataset.choices}") # noqa
+ return self.app.add_data_to_viewer(self.viewer_id, data_label)
+
+ def vue_add_data_to_viewer(self, info, *args):
+ self.add_data(info.get('data_label')) # pragma: no cover
+
+ def create_subset(self, subset_type):
+ """
+ Create a new subset in the viewer. This sets the app-wide subset selection to 'Create New'
+ and selects the appropriate tool in this viewer's toolbar.
+
+ Parameters
+ ----------
+ subset_type : str
+ The type of subset to create. Must be one of 'circle', 'rectangle', 'ellipse',
+ 'annulus', 'xrange', or 'yrange'.
+ """
+ # clear previous selection, finalize subsets, temporarily sets default tool
+ self._viewer.toolbar.active_tool_id = None
+ # set toolbar to the selection, will also set app-wide subset selection to "Create New"
+ # NOTE: supports passing either the user-friendly name or the actual ID
+ self._viewer.toolbar.select_tool(SUBSET_TOOL_IDS.get(subset_type, subset_type))
+
+ def vue_create_subset(self, info, *args):
+ self.create_subset(info.get('subset_type')) # pragma: no cover
diff --git a/jdaviz/configs/default/plugins/data_menu/data_menu.vue b/jdaviz/configs/default/plugins/data_menu/data_menu.vue
index be50687122..8de84bc32a 100644
--- a/jdaviz/configs/default/plugins/data_menu/data_menu.vue
+++ b/jdaviz/configs/default/plugins/data_menu/data_menu.vue
@@ -63,7 +63,7 @@
icon
disabled
>
- mdi-format-vertical-align-center
+ mdi-format-vertical-align-center
@@ -75,14 +75,13 @@
-
-
- mdi-plus
-
-
+ {add_data_to_viewer({data_label: data_label})}"
+ @create-subset="(subset_type) => {create_subset({subset_type: subset_type}); data_menu_open = false}"
+ >
+
- mdi-delete
+ mdi-delete
- mdi-label
+ mdi-label
Edit Subset
@@ -271,6 +271,7 @@
.dm-footer > .v-list-item__icon, .dm-footer > .v-list-item__content, .dm-footer > .v-list-item__action {
filter: invert(1);
}
+
.dm-header.v-btn--disabled .v-icon {
color: green !important;
}
diff --git a/jdaviz/configs/default/tests/test_data_menu.py b/jdaviz/configs/default/tests/test_data_menu.py
index 143a7fbffb..af3105b42a 100644
--- a/jdaviz/configs/default/tests/test_data_menu.py
+++ b/jdaviz/configs/default/tests/test_data_menu.py
@@ -66,3 +66,28 @@ def test_data_menu_selection(specviz_helper, spectrum1d):
assert len(dm._obj.layer_items) == 1
assert dm._obj.dm_layer_selected == [0]
assert dm.layer.selected == ['test']
+
+
+def test_data_menu_add_data(imviz_helper):
+ for i in range(3):
+ imviz_helper.load_data(np.zeros((2, 2)) + i, data_label=f'image_{i}', show_in_viewer=False)
+
+ dm = imviz_helper.viewers['imviz-0']._obj.data_menu
+ assert len(dm._obj.layer_items) == 0
+ assert len(dm.layer.choices) == 0
+ assert len(dm._obj.dataset.choices) == 3
+
+ dm.add_data('image_0')
+ assert dm.layer.choices == ['image_0']
+ assert len(dm._obj.dataset.choices) == 2
+
+
+def test_data_menu_create_subset(imviz_helper):
+ imviz_helper.load_data(np.zeros((2, 2)), data_label='image', show_in_viewer=True)
+
+ dm = imviz_helper.viewers['imviz-0']._obj.data_menu
+ assert imviz_helper.app.session.edit_subset_mode.edit_subset == []
+
+ dm.create_subset('circle')
+ assert imviz_helper.app.session.edit_subset_mode.edit_subset == []
+ assert imviz_helper.viewers['imviz-0']._obj.toolbar.active_tool_id == 'bqplot:truecircle'
diff --git a/jdaviz/configs/imviz/plugins/viewers.py b/jdaviz/configs/imviz/plugins/viewers.py
index e7175e41b1..0486582e81 100644
--- a/jdaviz/configs/imviz/plugins/viewers.py
+++ b/jdaviz/configs/imviz/plugins/viewers.py
@@ -63,6 +63,8 @@ def __init__(self, *args, **kwargs):
# intensive.
self.state.image_external_padding = 0.5
+ self.data_menu._obj.dataset.add_filter('is_image')
+
def on_mouse_or_key_event(self, data):
active_image_layer = self.active_image_layer
if active_image_layer is None:
diff --git a/jdaviz/configs/mosviz/helper.py b/jdaviz/configs/mosviz/helper.py
index 514cdef410..2ded2d0624 100644
--- a/jdaviz/configs/mosviz/helper.py
+++ b/jdaviz/configs/mosviz/helper.py
@@ -160,6 +160,10 @@ def _row_click_message_handler(self, msg):
self._handle_image_zoom(msg)
# expose the row to vue for each of the viewers
self.app.state.settings = {**self.app.state.settings, 'mosviz_row': msg.selected_index}
+ # update data filters in each viewer's data_menu
+ for viewer in self.viewers.values():
+ if data_menu := getattr(viewer._obj, '_data_menu', None):
+ data_menu.dataset._on_data_changed()
def _handle_image_zoom(self, msg):
mos_data = self.app.data_collection['MOS Table']
diff --git a/jdaviz/configs/mosviz/plugins/viewers.py b/jdaviz/configs/mosviz/plugins/viewers.py
index 91fee2ee5b..a84d772998 100644
--- a/jdaviz/configs/mosviz/plugins/viewers.py
+++ b/jdaviz/configs/mosviz/plugins/viewers.py
@@ -43,6 +43,9 @@ def __init__(self, *args, **kwargs):
self.state.show_axes = False # Axes are wrong anyway
self.figure.fig_margin = {'left': 0, 'bottom': 0, 'top': 0, 'right': 0}
+ self.data_menu._obj.dataset.add_filter('is_image_not_spectrum')
+ self.data_menu._obj.dataset.add_filter('same_mosviz_row')
+
def data(self, cls=None):
return [layer_state.layer.get_object(cls=cls or self.default_class)
for layer_state in self.state.layers
@@ -95,6 +98,9 @@ def __init__(self, *args, **kwargs):
for k in ('x_min', 'x_max'):
self.state.add_callback(k, self._handle_x_axis_orientation)
+ self.data_menu._obj.dataset.add_filter('is_2d_spectrum_or_trace')
+ self.data_menu._obj.dataset.add_filter('same_mosviz_row')
+
@cached_property
def reference_spectral_axis(self):
return self.state.reference_data.get_object().spectral_axis.value
@@ -243,6 +249,13 @@ class MosvizProfileView(SpecvizProfileView):
['jdaviz:sidebar_plot', 'jdaviz:sidebar_export']
]
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # NOTE: is_spectrum filter already applied by SpecvizProfileView
+ self.data_menu.layer.add_filter('not_trace')
+ self.data_menu._obj.dataset.add_filter('same_mosviz_row')
+
def set_plot_axes(self):
super().set_plot_axes()
self.figure.axes[1].num_ticks = 5
diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py
index bde414f458..baf6372f06 100644
--- a/jdaviz/configs/specviz/plugins/viewers.py
+++ b/jdaviz/configs/specviz/plugins/viewers.py
@@ -32,6 +32,10 @@ class SpecvizProfileView(JdavizProfileView):
_state_cls = FreezableProfileViewerState
_default_profile_subset_type = 'spectral'
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.data_menu._obj.dataset.add_filter('is_spectrum')
+
@property
def redshift(self):
return self.jdaviz_helper._redshift
diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py
index c97c3a8866..6e24465e06 100644
--- a/jdaviz/core/template_mixin.py
+++ b/jdaviz/core/template_mixin.py
@@ -1527,6 +1527,12 @@ def not_spatial_subset_in_profile_viewer(lyr):
# so we want to exclude spatial subsets
return get_subset_type(lyr) != 'spatial'
+ def is_trace(lyr):
+ return 'Trace' in getattr(getattr(lyr, 'data', None), 'meta', [])
+
+ def not_trace(lyr):
+ return not is_trace(lyr)
+
return super()._is_valid_item(lyr, locals())
def _layer_to_dict(self, layer_label):
@@ -3525,7 +3531,7 @@ def layer_in_flux_viewer(data):
return data.label in [lyr.layer.label for lyr in self.flux_viewer.layers] # noqa E741
def is_trace(data):
- return hasattr(data, 'meta') and 'Trace' in data.meta
+ return 'Trace' in getattr(data, 'meta', [])
def not_trace(data):
return not is_trace(data)
@@ -3533,9 +3539,26 @@ def not_trace(data):
def is_image(data):
return len(data.shape) == 2
+ def is_image_not_spectrum(data):
+ return (is_image(data)
+ and not getattr(data.coords, 'is_spectral', True))
+
def is_cube(data):
return len(data.shape) == 3
+ def is_cube_or_image(data):
+ return len(data.shape) >= 2
+
+ def is_spectrum(data):
+ return (len(data.shape) == 1
+ and data.coords is not None
+ and getattr(data.coords, 'is_spectral', True))
+
+ def is_2d_spectrum_or_trace(data):
+ return (data.ndim == 2
+ and data.coords is not None
+ and getattr(data.coords, 'is_spectral', True)) or 'Trace' in data.meta
+
def is_flux_cube(data):
if hasattr(self.app._jdaviz_helper, '_loaded_uncert_cube'):
uncert_label = getattr(self.app._jdaviz_helper._loaded_uncert_cube, 'label', None)
@@ -3550,6 +3573,19 @@ def not_child_layer(data):
# ignore layers that are children in associations:
return self.app._get_assoc_data_parent(data.label) is None
+ def same_mosviz_row(data):
+ # NOTE: requires calling _on_data_changed on a change to row
+ # currently handled by mosviz helper _row_click_message_handler
+ meta = getattr(data, 'meta', None)
+ if meta is None:
+ return True
+ data_row = meta.get('mosviz_row', None)
+ app_row = self.app.state.settings.get('mosviz_row', None)
+
+ if data_row is None or app_row is None:
+ return True
+ return data_row == app_row
+
layer_is_not_dq = layer_is_not_dq_global
return super()._is_valid_item(data, locals())