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

stylized layer icons in legend (future data-menu) #3220

Merged
merged 20 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
New Features
------------

* New design for viewer legend. [#3220]

Cubeviz
^^^^^^^

Expand Down
30 changes: 12 additions & 18 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
from echo import CallbackProperty, DictCallbackProperty, ListCallbackProperty
from ipygoldenlayout import GoldenLayout
from ipysplitpanes import SplitPanes
import matplotlib.cm as cm
import numpy as np
from glue.config import colormaps, data_translator, settings as glue_settings
from glue.config import data_translator, settings as glue_settings
from glue.core import HubListener
from glue.core.link_helpers import LinkSame, LinkSameWithUnits
from glue.core.message import (DataCollectionAddMessage,
Expand Down Expand Up @@ -126,6 +125,7 @@ def to_unit(self, data, cid, values, original_units, target_units):
'j-viewer-data-select': 'components/viewer_data_select.vue',
'j-viewer-data-select-item': 'components/viewer_data_select_item.vue',
'j-layer-viewer-icon': 'components/layer_viewer_icon.vue',
'j-layer-viewer-icon-stylized': 'components/layer_viewer_icon_stylized.vue',
'j-tray-plugin': 'components/tray_plugin.vue',
'j-play-pause-widget': 'components/play_pause_widget.vue',
'j-plugin-section-header': 'components/plugin_section_header.vue',
Expand Down Expand Up @@ -297,21 +297,6 @@ def __init__(self, configuration=None, *args, **kwargs):
# can reference their state easily since glue does not store viewers
self._viewer_store = {}

# Add new and inverse colormaps to Glue global state. Also see ColormapRegistry in
# https://github.com/glue-viz/glue/blob/main/glue/config.py
new_cms = (['Rainbow', cm.rainbow],
['Seismic', cm.seismic],
['Reversed: Gray', cm.gray_r],
['Reversed: Viridis', cm.viridis_r],
['Reversed: Plasma', cm.plasma_r],
['Reversed: Inferno', cm.inferno_r],
['Reversed: Magma', cm.magma_r],
['Reversed: Hot', cm.hot_r],
['Reversed: Rainbow', cm.rainbow_r])
for cur_cm in new_cms:
if cur_cm not in colormaps.members:
colormaps.add(*cur_cm)

from jdaviz.core.events import PluginTableAddedMessage, PluginPlotAddedMessage
self._plugin_tables = {}
self.hub.subscribe(self, PluginTableAddedMessage,
Expand Down Expand Up @@ -603,7 +588,16 @@ def _on_layers_changed(self, msg):
self.state.layer_icons = {**self.state.layer_icons,
layer_name: orientation_icons.get(layer_name,
wcs_only_refdata_icon)}
elif is_not_child:
elif not is_not_child:
parent_icon = self.state.layer_icons.get(self._get_assoc_data_parent(layer_name))
index = len([ln for ln, ic in self.state.layer_icons.items()
if not ic[:4] == 'mdi-' and
self._get_assoc_data_parent(ln) == parent_icon]) + 1
self.state.layer_icons = {
**self.state.layer_icons,
layer_name: f"{parent_icon}{index}"
}
else:
self.state.layer_icons = {
**self.state.layer_icons,
layer_name: alpha_index(len([ln for ln, ic in self.state.layer_icons.items()
Expand Down
107 changes: 107 additions & 0 deletions jdaviz/components/layer_viewer_icon_stylized.vue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specify cursor: default until the icons are clickable (and then pointer)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, done via passing disabled to the underlying v-btn.

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template>
<j-tooltip :tooltipcontent="tooltipContent(tooltip, label, visible, colormode, colors, linewidth, is_subset)">
<v-btn
:rounded="is_subset"
@click="(e) => $emit('click', e)"
:style="'padding: 0px; margin-bottom: 4px; background: '+visibilityBackgroundStyle(visible)+', '+colorBackgroundStyle(colors, cmap_samples)+'; '+btn_style"
width="30px"
min-width="30px"
height="30px"
:disabled="disabled"
>
<span :style="'color: white; text-shadow: 0px 0px 3px black; '+borderStyle(linewidth)">
{{ icon }}
</span>
</v-btn>
</j-tooltip>
</template>

<script>
module.exports = {
// tooltip: undefined will use default generated, empty will skip tooltips, any other string will be used directly
props: ['label', 'icon', 'visible', 'is_subset', 'colors', 'linewidth', 'colormode', 'cmap_samples', 'btn_style', 'tooltip', 'disabled'],
methods: {
tooltipContent(tooltip, label, visible, colormode, colors, linewidth, is_subset) {
if (tooltip !== undefined) {
return tooltip
}
var tooltip = label
if (visible === 'mixed') {
tooltip += '<br/>Visibility: mixed'
} else if (!visible) {
tooltip += '<br/>Visibility: hidden'
}
if (colormode === 'mixed' && !is_subset) {
tooltip += '<br/>Color mode: mixed'
}
if (colors.length > 1) {
if (colormode === 'Colormaps' && !is_subset) {
tooltip += '<br/>Colormap: mixed'
} else if (colormode === 'mixed' && !is_subset) {
tooltip += '<br/>Color/colormap: mixed'
} else {
tooltip += '<br/>Color: mixed'
}
}
if (linewidth == 'mixed' && !is_subset) {
tooltip += '<br/>Linewidth: mixed'
}
return tooltip
},
visibilityBackgroundStyle(visible) {
if (visible === 'mixed'){
return 'repeating-linear-gradient(30deg, rgba(0,0,0,0.3), rgba(0,0,0,0.3) 3px, transparent 3px, transparent 3px, transparent 10px)'
}
else if (visible) {
return 'repeating-linear-gradient(30deg, transparent, transparent 10px)'
} else {
return 'repeating-linear-gradient(30deg, rgba(0,0,0,0.4), rgba(0,0,0,0.4) 8px, transparent 8px, transparent 8px, transparent 10px)'
}
},
colorBackgroundStyle(colors, cmap_samples) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect that these loops to evaluate the colormap for the icons cost much. But is there a way to prove to ourselves that this is true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could try to profile it? All this is doing though is taking the input lists generated in python and joining them into a string, with a bit of math to set the widths of each entry, so I really don't expect it to show up in anything.

const strip_width = 42 / colors.length
var cmap_strip_width = strip_width
var style_colors = []
var style = 'repeating-linear-gradient( 135deg, '

for ([mi, color_or_cmap] of colors.entries()) {
if (color_or_cmap == 'from_list') {
/* follow-up: use actual colors from the DQ plugins */
color_or_cmap = 'rainbow'
Comment on lines +69 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what color DQ should get. We could take the first N colors from the DQ colormap like you're probably saying in this comment, but they're (intentionally) random, and this might not be the most visually meaningful way to represent DQ.

}

if (color_or_cmap.startsWith('#')) {
style_colors = [color_or_cmap]
} else {
style_colors = cmap_samples[color_or_cmap]
}

cmap_strip_width = strip_width / style_colors.length
for ([ci, color] of style_colors.entries()) {
var start = mi*strip_width + ci*cmap_strip_width
var end = mi*strip_width+(ci+1)*cmap_strip_width
style += color + ' '+start+'px, ' + color + ' '+end+'px'
if (ci !== style_colors.length-1) {
style += ', '
}
}
if (mi !== colors.length-1) {
style += ', '
}
}

style += ')'
return style
},
borderStyle(linewidth) {
if (linewidth != 'mixed' && linewidth > 0) {
return 'border-bottom: '+Math.min(linewidth, 5)+'px solid white'
}
return ''
},
}
};
</script>

<style scoped>
</style>
90 changes: 11 additions & 79 deletions jdaviz/components/plugin_layer_select_tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,17 @@
</div>
<span style="display: inline-block; white-space: nowrap; margin-left: -24px; width: calc(100% + 48px); overflow-x: scroll; overflow-y: hidden">
<span v-for="(item, index) in items" :class="selectedAsList.includes(item.label) ? 'layer-tab-selected' : ''" :style="'display: inline-block; padding: 8px; '+(selectedAsList.includes(item.label) ? 'border-top: 3px solid #00617E' : 'border-top: 3px solid transparent')">
<j-tooltip :tooltipcontent="tooltipContent(item)">
<v-btn
:rounded="item.is_subset"
@click="() => {if (!multiselect){$emit('update:selected', item.label)} else if(!selectedAsList.includes(item.label)) {$emit('update:selected', selected.concat(item.label))} else if (selected.length > 1) {$emit('update:selected', selected.filter(select => select != item.label))} }"
:style="'padding: 0px; margin-bottom: 4px; background: '+visibilityStyle(item)+', '+colorStyle(item)"
width="30px"
min-width="30px"
height="30px"
>
<span style="color: white; text-shadow: 0px 0px 3px black">
{{ item.icon }}
</span>
</v-btn>
</j-tooltip>
</span>
<j-layer-viewer-icon-stylized
:label="item.label"
:icon="item.icon"
:visible="item.visible"
:is_subset="item.is_subset"
:colors="item.colors"
:linewidth="item.linewidth"
:colormode="colormode"
:cmap_samples="cmap_samples"
@click="() => {if (!multiselect){$emit('update:selected', item.label)} else if(!selectedAsList.includes(item.label)) {$emit('update:selected', selected.concat(item.label))} else if (selected.length > 1) {$emit('update:selected', selected.filter(select => select != item.label))} }"
/>
</span>
</div>
</template>
Expand All @@ -44,70 +40,6 @@ module.exports = {
return [this.$props.selected]
}
},
methods: {
tooltipContent(item) {
var tooltip = item.label
if (item.visible === 'mixed') {
tooltip += '<br/>Visibility: mixed'
} else if (!item.visible) {
tooltip += '<br/>Visibility: hidden'
}
if (this.$props.colormode === 'mixed' && !item.is_subset) {
tooltip += '<br/>Color mode: mixed'
}
if (item.colors.length > 1) {
if (this.$props.colormode === 'Colormaps') {
tooltip += '<br/>Colormap: mixed'
} else if (this.$props.colormode === 'mixed') {
tooltip += '<br/>Color/colormap: mixed'
} else {
tooltip += '<br/>Color: mixed'
}
}
return tooltip
},
visibilityStyle(item) {
if (item.visible === 'mixed'){
return 'repeating-linear-gradient(30deg, rgba(0,0,0,0.3), rgba(0,0,0,0.3) 3px, transparent 3px, transparent 3px, transparent 10px)'
}
else if (item.visible) {
return 'repeating-linear-gradient(30deg, transparent, transparent 10px)'
} else {
return 'repeating-linear-gradient(30deg, rgba(0,0,0,0.4), rgba(0,0,0,0.4) 8px, transparent 8px, transparent 8px, transparent 10px)'
}
},
colorStyle(item) {
const strip_width = 42 / item.colors.length
var cmap_strip_width = strip_width
var colors = []
var style = 'repeating-linear-gradient( 135deg, '

for ([mi, color_or_cmap] of item.colors.entries()) {
if (color_or_cmap.startsWith('#')) {
colors = [color_or_cmap]
} else {
colors = this.$props.cmap_samples[color_or_cmap]
}

cmap_strip_width = strip_width / colors.length
for ([ci, color] of colors.entries()) {
var start = mi*strip_width + ci*cmap_strip_width
var end = mi*strip_width+(ci+1)*cmap_strip_width
style += color + ' '+start+'px, ' + color + ' '+end+'px'
if (ci !== colors.length-1) {
style += ', '
}
}
if (mi !== item.colors.length-1) {
style += ', '
}
}

style += ')'
return style

}
}
};

</script>
26 changes: 21 additions & 5 deletions jdaviz/configs/default/plugins/data_menu/data_menu.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from traitlets import Dict, Unicode

from jdaviz.core.template_mixin import TemplateMixin
from jdaviz.core.template_mixin import TemplateMixin, LayerSelectMixin
from jdaviz.core.user_api import UserApiWrapper
from jdaviz.core.events import IconsUpdatedMessage
from jdaviz.core.events import IconsUpdatedMessage, AddDataMessage
from jdaviz.utils import cmap_samples, is_not_wcs_only

__all__ = ['DataMenu']


class DataMenu(TemplateMixin):
"""Viewer Data Menu"""
class DataMenu(TemplateMixin, LayerSelectMixin):
"""Viewer Data Menu

Only the following attributes and methods are available through the
:ref:`public API <plugin-apis>`:

* ``layer`` (:class:`~jdaviz.core.template_mixin.LayerSelect`):
"""
template_file = __file__, "data_menu.vue"

viewer_id = Unicode().tag(sync=True)
Expand All @@ -19,18 +26,26 @@

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

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

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

# 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)

# 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.viewer_icons = dict(self.app.state.viewer_icons)
self.layer_icons = dict(self.app.state.layer_icons)

@property
def user_api(self):
expose = []
expose = ['layer']

Check warning on line 48 in jdaviz/configs/default/plugins/data_menu/data_menu.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/default/plugins/data_menu/data_menu.py#L48

Added line #L48 was not covered by tests
return UserApiWrapper(self, expose=expose)

def set_viewer_id(self):
Expand All @@ -41,6 +56,7 @@
try:
self.viewer_id = getattr(self._viewer, '_reference_id', '')
self.viewer_reference = self._viewer.reference
self.layer.viewer = self._viewer.reference
except AttributeError:
return

Expand Down
Loading
Loading