Skip to content
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

Horizontal layer buttons #2566

Merged
merged 33 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bf22850
Layers now represented as horizontal buttons
javerbukh Nov 3, 2023
2de03cf
Fix codestyle
javerbukh Nov 14, 2023
6483a01
Fix codestlye and remove comments
javerbukh Nov 14, 2023
3c31e18
Viewer multiselect mixed state working
javerbukh Nov 14, 2023
9ab3162
Working on handling edge cases of mixed state
javerbukh Nov 15, 2023
f34e42b
Use messages in LayerSelect to call parts of _on_layers_changed
javerbukh Nov 16, 2023
1fd124f
Remove unused elements
javerbukh Nov 16, 2023
1ef9bdb
Fix button colors not updating after unmix state
javerbukh Nov 16, 2023
93ce319
Fix codestyle
javerbukh Nov 16, 2023
fdf0703
Attempt 2 at fixing code style
javerbukh Nov 16, 2023
85c1a3b
Fix CI, put buttons in icon order
javerbukh Nov 16, 2023
7b2ef3d
Code style again
javerbukh Nov 16, 2023
10aaa91
Fix more tests
javerbukh Nov 16, 2023
60ad145
Everything handled by _on_layers_changed
javerbukh Nov 17, 2023
639eb87
button styling/tooltip based on (mixed)-color/visibility
kecnry Nov 17, 2023
17f4e33
improve tab styling
kecnry Nov 17, 2023
e4650ff
avoid second loop when applying RGB presets
kecnry Nov 17, 2023
f9640e0
improved handling of colormap and mixed colormode
kecnry Nov 17, 2023
9a3c30e
remove elevation
kecnry Nov 17, 2023
d3baec2
use all_color_to_label in tab styling
kecnry Nov 17, 2023
939d3be
Mixed visibility and color represented in button
javerbukh Nov 17, 2023
611ea92
improved handling of subset color when in colormap
kecnry Nov 17, 2023
1f56e99
improved dark-theme support
kecnry Nov 17, 2023
fb64d0f
tooltip and RGB preset support for mixed visibility
kecnry Nov 17, 2023
fd4353d
Visible is set to layer visible and bitmap visible
javerbukh Nov 17, 2023
5d50565
avoid displacement when no tabs selected
kecnry Nov 17, 2023
f0a7966
Fix CI tests
javerbukh Nov 17, 2023
37c9605
RGB presets: handle unmixing state
kecnry Nov 17, 2023
10d1f20
Address review comments
javerbukh Nov 17, 2023
02661cc
Forgot one change
javerbukh Nov 17, 2023
a0e8ee2
for styling, always check if item in list, not as substring
kecnry Nov 20, 2023
bea110d
Update changelog
javerbukh Nov 20, 2023
32a75df
Add tests
javerbukh Nov 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def to_unit(self, data, cid, values, original_units, target_units):
'plugin-subset-select': 'components/plugin_subset_select.vue',
'plugin-viewer-select': 'components/plugin_viewer_select.vue',
'plugin-layer-select': 'components/plugin_layer_select.vue',
'plugin-layer-select-tabs': 'components/plugin_layer_select_tabs.vue',
'plugin-editable-select': 'components/plugin_editable_select.vue',
'plugin-add-results': 'components/plugin_add_results.vue',
'plugin-auto-label': 'components/plugin_auto_label.vue',
Expand Down
20 changes: 20 additions & 0 deletions jdaviz/components/plugin_layer_select_tabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<div>
<v-row>
<v-col v-for="(item, index) in items">
<v-btn :rounded="item.is_subset" :color="!item.mixed_color ? item.color : 'gray'" @click="() => {if (!multiselect){$emit('update:selected', item.label)} else if(selected.indexOf(item.label) === -1) {$emit('update:selected', selected.concat(item.label))} else {$emit('update:selected', selected.filter(select => select != item.label))} }">{{ item.icon }}</v-btn>


</v-col>
</v-row>
<v-row>
<span>{{ selected }}</span>
</v-row>
</div>
</template>

<script>
module.exports = {
props: ['items', 'selected', 'multiselect']
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,12 @@

<!-- LAYER OPTIONS -->
<j-plugin-section-header>Layer options</j-plugin-section-header>
<plugin-layer-select
<plugin-layer-select-tabs
:items="layer_items"
:selected.sync="layer_selected"
:multiselect="multiselect"
:show_if_single_entry="true"
:label="multiselect ? 'Layers': 'Layer'"
:hint="multiselect ? 'Select layers to set options simultaneously' : 'Select the data or subset to set options.'"
label="Layers"
hint="Select the data or subset to set options."
/>

<j-plugin-section-header v-if="layer_selected.length && (line_visible_sync.in_subscribed_states || subset_visible_sync.in_subscribed_states)">Layer Visibility</j-plugin-section-header>
Expand Down
142 changes: 132 additions & 10 deletions jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,6 @@ def _apply_default_selection(self, skip_if_current_valid=True):
self.selected = [s for s in self.labels if s in self.selected]
return
is_valid = False

is_valid = self.selected in self.labels
if callable(self.default_mode):
# callable was defined and passed by the plugin or inheriting component.
Expand Down Expand Up @@ -1232,19 +1231,20 @@ def __init__(self, plugin, items, selected, viewer,
default_mode=default_mode)

self.hub.subscribe(self, AddDataMessage,
handler=lambda _: self._on_layers_changed())
handler=lambda _: self._on_data_added())
self.hub.subscribe(self, RemoveDataMessage,
handler=lambda _: self._on_layers_changed())
self.hub.subscribe(self, SubsetCreateMessage,
handler=lambda _: self._on_layers_changed())
handler=lambda _: self._on_subset_created())
# will need SubsetUpdateMessage for name only (style shouldn't force a full refresh)
# self.hub.subscribe(self, SubsetUpdateMessage,
# handler=lambda _: self._on_layers_changed())
self.hub.subscribe(self, SubsetDeleteMessage,
handler=lambda _: self._on_layers_changed())

self.app.state.add_callback('layer_icons', lambda _: self._on_layers_changed())
self.app.state.add_callback('layer_icons', lambda _: self._on_layers_changed('layer_icons_changed')) # noqa
self.add_observe(viewer, self._on_viewer_changed)
self.add_observe(selected, self._on_layers_changed)
self._on_layers_changed()

def _get_viewer(self, viewer):
Expand All @@ -1255,14 +1255,22 @@ def _get_viewer(self, viewer):
except TypeError:
return self.app.get_viewer_by_id(viewer)

def _layer_to_dict(self, layer):
def _layer_to_dict(self, layer, label_to_color=None, label_mixed_color=None):
javerbukh marked this conversation as resolved.
Show resolved Hide resolved
is_subset = False if hasattr(layer, 'state') and hasattr(layer.state, 'bitmap_visible') else True # noqa
javerbukh marked this conversation as resolved.
Show resolved Hide resolved
d = {"label": layer.layer.label,
"color": layer.state.color,
"icon": self.app.state.layer_icons.get(layer.layer.label)}
"icon": self.app.state.layer_icons.get(layer.layer.label),
"visible": (layer.state.bitmap_visible
if hasattr(layer, 'state') and hasattr(layer.state, 'bitmap_visible')
else layer.visible),
"mixed_color": (False if not label_mixed_color
else label_mixed_color[layer.layer.label]),
javerbukh marked this conversation as resolved.
Show resolved Hide resolved
"is_subset": is_subset}
return d

def _on_viewer_changed(self, msg=None):
# we don't want to update the layers if we're just toggling between single and multi-select
# we don't want to update the layers if we're just toggling
# between single and multi-select
old, new = msg['old'], msg['new']
if not isinstance(old, list):
old = [old]
Expand All @@ -1271,10 +1279,72 @@ def _on_viewer_changed(self, msg=None):
if new != old:
self._clear_cache()
self._on_layers_changed()
added_viewers = list(set(new) - set(old))
removed_viewers = list(set(old) - set(new))
for old_viewer in removed_viewers:
if self._get_viewer(old_viewer) is None:
continue
for layer in self._get_viewer(old_viewer).state.layers:
layer.remove_callback('color', lambda _: self._on_layers_changed('color_change')) # noqa
for new_viewer in added_viewers:
if self._get_viewer(new_viewer) is None:
continue
for layer in self._get_viewer(new_viewer).state.layers:
layer.add_callback('color', lambda _: self._on_layers_changed('color_change'))

def _on_subset_created(self, msg=None):
print("SUBSET CREATED", self.plugin.app.data_collection.subset_groups[-1].label)
new_subset_label = self.plugin.app.data_collection.subset_groups[-1].label
viewer = self.viewer if isinstance(self.viewer, list) else [self.viewer]
for current_viewer in viewer:
for layer in self._get_viewer(current_viewer).state.layers:
if layer.layer.label == new_subset_label:
# A subset layer is created for each data in the viewer, this is an issue
# for determining which layer to watch for the color change event
layer.add_callback('color', lambda _: self._on_layers_changed('color_change', layer=layer)) # noqa
# TODO: Add ability to add new item to self.items instead of recompiling
# self._on_layers_changed('subset_added')
return

def _on_data_added(self, msg=None):
print("DATA ADDED", self.plugin.app.data_collection[-1].label)
new_data_label = self.plugin.app.data_collection[-1].label
viewer = self.viewer if isinstance(self.viewer, list) else [self.viewer]
for current_viewer in viewer:
for layer in self._get_viewer(current_viewer).state.layers:
if layer.layer.label == new_data_label:
# Add a callback to the layer's color attribute to call
# _on_layers_changed whenever the color changes
# TODO: find out if this conflicts with another color change event
# and is causing the lag in the color picker
layer.add_callback('color', lambda _: self._on_layers_changed('color_change', layer=layer)) # noqa
# self._on_layers_changed('data_added')

@observe('filters')
def _on_layers_changed(self, msg=None):
def _on_layers_changed(self, msg=None, **kwargs):
# NOTE: _on_layers_changed is passed without a msg object during init
print("LAYERS CHANGED", msg if msg and isinstance(msg, str) else None)
if msg and msg == 'layer_icons_changed':
# Trying to figure out the difference between watching `layer_icons` and
# subscribing to Data add/remove messages
print("LAYER ICON CHANGED")
elif msg and msg == 'color_change' and isinstance(self.selected, str):
# Update the items index for only the layer affected
# and only if one label is selected
layer = kwargs.pop('layer', None)
if not layer:
return
index = [index for index, item in enumerate(self.items) if item['label'] == layer.layer.label] # noqa
if not index:
print("Unable to change color")
index = index[0]
self.items[index]['color'] = layer.color
self.plugin.send_state('layer_items')
# Is this faster?
# items = self.items
# self.items = []
# self.items = items
return

viewer_names = self.viewer
if not isinstance(viewer_names, list):
Expand All @@ -1288,9 +1358,61 @@ def _on_layers_changed(self, msg=None):
# based on which was found _first.
layer_labels = [layer.layer.label for layer in layers]
_, inds = np.unique(layer_labels, return_index=True)
layers = [layers[i] for i in inds]
layers_unique = [layers[i] for i in inds]

selected_labels = self.selected
selected_color = None
mixed_selected_color = False
label_to_color = {}
label_mixed_color = {}

# Go through all layers and determine what colors go to what layers based
# on what label they use. Also determine if those layers with shared labels
# also share color or visibility. If not, show that as a mixed state.
for layer in layers:
label = layer.layer.label
color = layer.state.color.lower()
# This handles the subset case since each subset can only be one color,
# even though there is a layer with the subsets name for each data layer that
# subset is applied to
if not hasattr(layer.state, 'bitmap_visible'):
if label not in label_to_color:
label_to_color[label] = [color]
label_mixed_color[label] = False
continue

# Determine whether all currently selected layers share the same color. If so
# then there is no mixed color state. Otherwise there is.
if selected_labels and label in selected_labels and selected_color is None:
selected_color = color
elif selected_labels and label in selected_labels and color != selected_color:
mixed_selected_color = True

# label_to_color tracks all colors per layer label
if label not in label_to_color:
label_to_color[label] = [color]
label_mixed_color[label] = False
else:
label_to_color[label] += [color]

# If there is more than one unique color per label or the label is
# selected and not the same color as other selected labels, then the
# current state of the layer's color is mixed
for k, v in label_to_color.items():
if (len(np.unique(v)) > 1 or
(selected_labels and k in selected_labels and mixed_selected_color)):
label_mixed_color[k] = True

# Send layers with unique labels and what colors are associated
# with that label and if layers with that label are in a mixed color state
items = manual_items + [self._layer_to_dict(layer, label_to_color, label_mixed_color)
for layer in layers_unique]

def _sort_by_icon(items_dict):
return items_dict['icon']
items.sort(key=_sort_by_icon)
self.items = items

self.items = manual_items + [self._layer_to_dict(layer) for layer in layers]
self._apply_default_selection()

@cached_property
Expand Down
Loading