From 54992bee1e4883ef8752c402d496945ea7039ee8 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 14:24:33 -0400 Subject: [PATCH 1/8] Add transfer function creation for colormaps. --- glue_jupyter/ipyvolume/volume/layer_artist.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/glue_jupyter/ipyvolume/volume/layer_artist.py b/glue_jupyter/ipyvolume/volume/layer_artist.py index 1d4b6bc6..479f4622 100644 --- a/glue_jupyter/ipyvolume/volume/layer_artist.py +++ b/glue_jupyter/ipyvolume/volume/layer_artist.py @@ -46,6 +46,15 @@ def _transfer_func_rgba(color, N=256, max_opacity=1): data[..., 3] = ramp*max_opacity return data +def _transfer_func_cmap(cmap, N=256, max_opacity=1): + data = np.zeros((N, 4), dtype=np.float32) + ramp = np.linspace(0, 1, N) + colors = cmap(ramp) + for i in range(3): + data[..., i] = [c[i] for c in colors] + data[..., 3] = ramp*max_opacity + return data + data0 = [[[1, 2]] * 2] * 2 @@ -136,5 +145,9 @@ def update(self): self.state.percentile = 100 def _update_transfer_function(self): - self.transfer_function.rgba = _transfer_func_rgba(self.state.color, - max_opacity=self.state.alpha) + if self.state.color_mode == "Fixed": + self.transfer_function.rgba = _transfer_func_rgba(self.state.color, + max_opacity=self.state.alpha) + else: + self.transfer_function.rgba = _transfer_func_cmap(self.state.cmap, + max_opacity=self.state.alpha) From debb3528ed2c180a1c540fe11bdb95783f5ff81f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 15:11:29 -0400 Subject: [PATCH 2/8] Make color widget more general. --- glue_jupyter/widgets/color.py | 43 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/glue_jupyter/widgets/color.py b/glue_jupyter/widgets/color.py index ff25d997..05f9a1b9 100644 --- a/glue_jupyter/widgets/color.py +++ b/glue_jupyter/widgets/color.py @@ -13,25 +13,31 @@ def __init__(self, state, **kwargs): super(Color, self).__init__(**kwargs) self.state = state + self.cmap_att = kwargs.get('cmap_att', 'cmap_att') + self.cmap_mode_attr = kwargs.get('cmap_mode_attr', 'cmap_mode') + self.widget_color = widgets.ColorPicker(description='color') link((self.state, 'color'), (self.widget_color, 'value'), color2hex) - cmap_mode_options = type(self.state).cmap_mode.get_choice_labels(self.state) + cmap_mode_options = getattr(type(self.state), self.cmap_mode_attr).get_choice_labels(self.state) self.widget_cmap_mode = widgets.RadioButtons(options=cmap_mode_options, description='cmap mode') - link((self.state, 'cmap_mode'), (self.widget_cmap_mode, 'value')) - - self.widget_cmap_att = LinkedDropdown(self.state, 'cmap_att', - ui_name='color attribute', - label='color attribute') - - self.widget_cmap_vmin = widgets.FloatText(description='color min') - self.widget_cmap_vmax = widgets.FloatText(description='color max') - self.widget_cmap_v = widgets.VBox([self.widget_cmap_vmin, self.widget_cmap_vmax]) - link((self.state, 'cmap_vmin'), (self.widget_cmap_vmin, 'value'), lambda value: value or 0) - link((self.state, 'cmap_vmax'), (self.widget_cmap_vmax, 'value'), lambda value: value or 1) + link((self.state, self.cmap_mode_attr), (self.widget_cmap_mode, 'value')) + + children = [self.widget_cmap_mode, self.widget_color] + if self.cmap_att is not None: + self.widget_cmap_att = LinkedDropdown(self.state, 'cmap_att', + ui_name='color attribute', + label='color attribute') + self.widget_cmap_vmin = widgets.FloatText(description='color min') + self.widget_cmap_vmax = widgets.FloatText(description='color max') + self.widget_cmap_v = widgets.VBox([self.widget_cmap_vmin, self.widget_cmap_vmax]) + link((self.state, 'cmap_vmin'), (self.widget_cmap_vmin, 'value'), lambda value: value or 0) + link((self.state, 'cmap_vmax'), (self.widget_cmap_vmax, 'value'), lambda value: value or 1) + children.extend((self.widget_cmap_att, self.widget_cmap_v)) self.widget_cmap = widgets.Dropdown(options=colormaps, description='colormap') + children.append(self.widget_cmap) link((self.state, 'cmap'), (self.widget_cmap, 'label'), lambda cmap: colormaps.name_from_cmap(cmap), lambda name: colormaps[name]) @@ -39,10 +45,9 @@ def __init__(self, state, **kwargs): lambda value: None if value == cmap_mode_options[0] else 'none') dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap.layout, 'display'), lambda value: None if value == cmap_mode_options[1] else 'none') - dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_att.layout, 'display'), - lambda value: None if value == cmap_mode_options[1] else 'none') - dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_v.layout, 'display'), - lambda value: None if value == cmap_mode_options[1] else 'none') - self.children = (self.widget_cmap_mode, self.widget_color, - self.widget_cmap_att, self.widget_cmap_v, - self.widget_cmap) + if self.cmap_att is not None: + dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_att.layout, 'display'), + lambda value: None if value == cmap_mode_options[1] else 'none') + dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_v.layout, 'display'), + lambda value: None if value == cmap_mode_options[1] else 'none') + self.children = tuple(children) From 4e379c58334d155488bf2e5d0b26bb6020609c59 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 15:11:59 -0400 Subject: [PATCH 3/8] Update volume layer artist and widget to allow changing color mode and colormap. --- glue_jupyter/ipyvolume/volume/layer_artist.py | 2 +- glue_jupyter/ipyvolume/volume/layer_style_widget.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/glue_jupyter/ipyvolume/volume/layer_artist.py b/glue_jupyter/ipyvolume/volume/layer_artist.py index 479f4622..2f98699c 100644 --- a/glue_jupyter/ipyvolume/volume/layer_artist.py +++ b/glue_jupyter/ipyvolume/volume/layer_artist.py @@ -93,7 +93,7 @@ def __init__(self, ipyvolume_viewer=None, state=None, layer=None, layer_state=No link((self.state, 'opacity_scale'), (self.volume, 'opacity_scale')) - on_change([(self.state, 'color', 'alpha')])(self._update_transfer_function) + on_change([(self.state, 'color', 'alpha', 'color_mode', 'cmap')])(self._update_transfer_function) def clear(self): pass diff --git a/glue_jupyter/ipyvolume/volume/layer_style_widget.py b/glue_jupyter/ipyvolume/volume/layer_style_widget.py index 207d0ff8..82853307 100644 --- a/glue_jupyter/ipyvolume/volume/layer_style_widget.py +++ b/glue_jupyter/ipyvolume/volume/layer_style_widget.py @@ -1,7 +1,7 @@ -from ipywidgets import (Checkbox, VBox, ColorPicker, Dropdown, FloatSlider, +from ipywidgets import (Checkbox, VBox, Dropdown, FloatSlider, FloatLogSlider) -from glue.utils import color2hex +from glue_jupyter.widgets import Color from ...link import link, dlink @@ -53,8 +53,7 @@ def __init__(self, layer_state): self.widget_clamp_max = Checkbox(description='clamp maximum', value=self.state.clamp_max) link((self.state, 'clamp_max'), (self.widget_clamp_max, 'value')) - self.widget_color = ColorPicker(value=color2hex(self.state.color), description='color') - link((self.state, 'color'), (self.widget_color, 'value'), color2hex) + self.widget_color = Color(state=self.state, cmap_mode_attr='color_mode', cmap_att=None) if self.state.alpha is None: self.state.alpha = 1 From e5219611656805879bceacdda13aa8d04dbc3413 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 15:48:45 -0400 Subject: [PATCH 4/8] Add support for stretches as well. --- glue_jupyter/ipyvolume/volume/layer_artist.py | 16 +++++++++++----- .../ipyvolume/volume/layer_style_widget.py | 7 ++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/glue_jupyter/ipyvolume/volume/layer_artist.py b/glue_jupyter/ipyvolume/volume/layer_artist.py index 2f98699c..f2c142c8 100644 --- a/glue_jupyter/ipyvolume/volume/layer_artist.py +++ b/glue_jupyter/ipyvolume/volume/layer_artist.py @@ -36,19 +36,23 @@ def __init__(self, layer=None, **kwargs): self.clamp_max = False -def _transfer_func_rgba(color, N=256, max_opacity=1): +def _transfer_func_rgba(color, N=256, max_opacity=1, stretch=None): r, g, b = matplotlib.colors.to_rgb(color) data = np.zeros((N, 4), dtype=np.float32) ramp = np.linspace(0, 1, N) + if stretch is not None: + ramp = stretch(ramp) data[..., 0] = r data[..., 1] = g data[..., 2] = b data[..., 3] = ramp*max_opacity return data -def _transfer_func_cmap(cmap, N=256, max_opacity=1): +def _transfer_func_cmap(cmap, N=256, max_opacity=1, stretch=None): data = np.zeros((N, 4), dtype=np.float32) ramp = np.linspace(0, 1, N) + if stretch is not None: + ramp = stretch(ramp) colors = cmap(ramp) for i in range(3): data[..., i] = [c[i] for c in colors] @@ -93,7 +97,7 @@ def __init__(self, ipyvolume_viewer=None, state=None, layer=None, layer_state=No link((self.state, 'opacity_scale'), (self.volume, 'opacity_scale')) - on_change([(self.state, 'color', 'alpha', 'color_mode', 'cmap')])(self._update_transfer_function) + on_change([(self.state, 'color', 'alpha', 'color_mode', 'cmap', 'stretch', 'stretch_parameters')])(self._update_transfer_function) def clear(self): pass @@ -147,7 +151,9 @@ def update(self): def _update_transfer_function(self): if self.state.color_mode == "Fixed": self.transfer_function.rgba = _transfer_func_rgba(self.state.color, - max_opacity=self.state.alpha) + max_opacity=self.state.alpha, + stretch=lambda x: self.state.stretch_object(x, **self.state.stretch_parameters)) else: self.transfer_function.rgba = _transfer_func_cmap(self.state.cmap, - max_opacity=self.state.alpha) + max_opacity=self.state.alpha, + stretch=lambda x: self.state.stretch_object(x, **self.state.stretch_parameters)) diff --git a/glue_jupyter/ipyvolume/volume/layer_style_widget.py b/glue_jupyter/ipyvolume/volume/layer_style_widget.py index 82853307..875be827 100644 --- a/glue_jupyter/ipyvolume/volume/layer_style_widget.py +++ b/glue_jupyter/ipyvolume/volume/layer_style_widget.py @@ -2,6 +2,7 @@ FloatLogSlider) from glue_jupyter.widgets import Color +from glue_jupyter.widgets.linked_dropdown import LinkedDropdown from ...link import link, dlink @@ -67,6 +68,10 @@ def __init__(self, layer_state): value=self.state.opacity_scale) link((self.state, 'opacity_scale'), (self.widget_opacity_scale, 'value')) + self.widget_stretch = LinkedDropdown(self.state, 'stretch', + ui_name='stretch', + label='stretch') + # FIXME: this should be fixed # self.widget_reset_zoom = Button(description="Reset zoom") # self.widget_reset_zoom.on_click(self.state.viewer_state.reset_limits) @@ -76,4 +81,4 @@ def __init__(self, layer_state): self.widget_clamp_min, self.widget_clamp_max, self.widget_max_resolution, # self.widget_reset_zoom, self.widget_color, self.widget_opacity, - self.widget_opacity_scale]) + self.widget_opacity_scale, self.widget_stretch]) From 3f0ddc7081554b0c72239f8df6b29228f57cd2a1 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 16:49:53 -0400 Subject: [PATCH 5/8] Add tests of volume colormap mode and stretch widgets. --- .../ipyvolume/tests/test_ipyvolume.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/glue_jupyter/ipyvolume/tests/test_ipyvolume.py b/glue_jupyter/ipyvolume/tests/test_ipyvolume.py index d8a4b996..8b878c65 100644 --- a/glue_jupyter/ipyvolume/tests/test_ipyvolume.py +++ b/glue_jupyter/ipyvolume/tests/test_ipyvolume.py @@ -2,7 +2,9 @@ import nbformat import numpy as np +from glue.config import stretches from glue.core.roi import PolygonalROI, Projected3dROI +from matplotlib import colormaps from nbconvert.preprocessors import ExecutePreprocessor DATA = os.path.join(os.path.dirname(__file__), 'data') @@ -177,6 +179,49 @@ def test_volshow_multiple_subsets(app, data_unlinked, data_volume): assert not viewer.layers[2].enabled +def test_volshow_cmap_mode(app, data_volume): + + assert data_volume in app.data_collection + v = app.volshow(data=data_volume) + + layer = v.layers[0] + layer_widget = v.layer_options.layers[-1]['layer_panel'] + + assert layer.state.color_mode == 'Fixed' + assert layer.state.cmap.name == 'gray' + + layer.state.color_mode = 'Linear' + assert layer_widget.widget_color.widget_cmap_mode.label == 'Linear' + assert layer_widget.widget_color.widget_cmap.label == 'Gray' + assert layer.state.cmap.name == 'gray' + + layer.state.cmap = colormaps['viridis'] + assert layer_widget.widget_color.widget_cmap.label == 'Viridis' + + layer_widget.widget_color.widget_cmap.label = 'Hot' + assert layer.state.cmap == colormaps['hot'] + + +def test_volshow_stretch(app, data_volume): + + assert data_volume in app.data_collection + v = app.volshow(data=data_volume) + + layer = v.layers[0] + layer_widget = v.layer_options.layers[-1]['layer_panel'] + + assert layer.state.stretch == 'linear' + assert [item[1] for item in layer_widget.widget_stretch.options] == [item for item in stretches.members] + assert layer_widget.widget_stretch.value == 'linear' + + layer.state.stretch = 'log' + assert layer_widget.widget_stretch.value == 'log' + + layer_widget.widget_stretch.value = 'sqrt' + assert layer.state.stretch == 'sqrt' + + + def test_notebook(): # Run an actual notebook From 942a969439a1568ff25cf570bc4b3586bccd2e2a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 3 Jul 2025 17:07:10 -0400 Subject: [PATCH 6/8] Codestyle fixes. --- .../ipyvolume/tests/test_ipyvolume.py | 12 ++++----- glue_jupyter/ipyvolume/volume/layer_artist.py | 11 +++++--- glue_jupyter/widgets/color.py | 27 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/glue_jupyter/ipyvolume/tests/test_ipyvolume.py b/glue_jupyter/ipyvolume/tests/test_ipyvolume.py index 8b878c65..bc7117a8 100644 --- a/glue_jupyter/ipyvolume/tests/test_ipyvolume.py +++ b/glue_jupyter/ipyvolume/tests/test_ipyvolume.py @@ -180,13 +180,13 @@ def test_volshow_multiple_subsets(app, data_unlinked, data_volume): def test_volshow_cmap_mode(app, data_volume): - + assert data_volume in app.data_collection v = app.volshow(data=data_volume) layer = v.layers[0] layer_widget = v.layer_options.layers[-1]['layer_panel'] - + assert layer.state.color_mode == 'Fixed' assert layer.state.cmap.name == 'gray' @@ -203,15 +203,16 @@ def test_volshow_cmap_mode(app, data_volume): def test_volshow_stretch(app, data_volume): - + assert data_volume in app.data_collection v = app.volshow(data=data_volume) layer = v.layers[0] layer_widget = v.layer_options.layers[-1]['layer_panel'] - + assert layer.state.stretch == 'linear' - assert [item[1] for item in layer_widget.widget_stretch.options] == [item for item in stretches.members] + assert [item[1] for item in layer_widget.widget_stretch.options] == \ + [item for item in stretches.members] assert layer_widget.widget_stretch.value == 'linear' layer.state.stretch = 'log' @@ -221,7 +222,6 @@ def test_volshow_stretch(app, data_volume): assert layer.state.stretch == 'sqrt' - def test_notebook(): # Run an actual notebook diff --git a/glue_jupyter/ipyvolume/volume/layer_artist.py b/glue_jupyter/ipyvolume/volume/layer_artist.py index f2c142c8..9ebf3507 100644 --- a/glue_jupyter/ipyvolume/volume/layer_artist.py +++ b/glue_jupyter/ipyvolume/volume/layer_artist.py @@ -48,6 +48,7 @@ def _transfer_func_rgba(color, N=256, max_opacity=1, stretch=None): data[..., 3] = ramp*max_opacity return data + def _transfer_func_cmap(cmap, N=256, max_opacity=1, stretch=None): data = np.zeros((N, 4), dtype=np.float32) ramp = np.linspace(0, 1, N) @@ -97,7 +98,9 @@ def __init__(self, ipyvolume_viewer=None, state=None, layer=None, layer_state=No link((self.state, 'opacity_scale'), (self.volume, 'opacity_scale')) - on_change([(self.state, 'color', 'alpha', 'color_mode', 'cmap', 'stretch', 'stretch_parameters')])(self._update_transfer_function) + on_change([(self.state, 'color', 'alpha', 'color_mode', + 'cmap', 'stretch', 'stretch_parameters' + )])(self._update_transfer_function) def clear(self): pass @@ -149,11 +152,13 @@ def update(self): self.state.percentile = 100 def _update_transfer_function(self): + def stretch(x): + return self.state.stretch_object(x, **self.state.stretch_parameters) if self.state.color_mode == "Fixed": self.transfer_function.rgba = _transfer_func_rgba(self.state.color, max_opacity=self.state.alpha, - stretch=lambda x: self.state.stretch_object(x, **self.state.stretch_parameters)) + stretch=stretch) else: self.transfer_function.rgba = _transfer_func_cmap(self.state.cmap, max_opacity=self.state.alpha, - stretch=lambda x: self.state.stretch_object(x, **self.state.stretch_parameters)) + stretch=stretch) diff --git a/glue_jupyter/widgets/color.py b/glue_jupyter/widgets/color.py index 05f9a1b9..37576c29 100644 --- a/glue_jupyter/widgets/color.py +++ b/glue_jupyter/widgets/color.py @@ -19,22 +19,27 @@ def __init__(self, state, **kwargs): self.widget_color = widgets.ColorPicker(description='color') link((self.state, 'color'), (self.widget_color, 'value'), color2hex) - cmap_mode_options = getattr(type(self.state), self.cmap_mode_attr).get_choice_labels(self.state) + cmap_mode_options = getattr(type(self.state), + self.cmap_mode_attr).get_choice_labels(self.state) self.widget_cmap_mode = widgets.RadioButtons(options=cmap_mode_options, description='cmap mode') link((self.state, self.cmap_mode_attr), (self.widget_cmap_mode, 'value')) - + children = [self.widget_cmap_mode, self.widget_color] if self.cmap_att is not None: - self.widget_cmap_att = LinkedDropdown(self.state, 'cmap_att', - ui_name='color attribute', - label='color attribute') - self.widget_cmap_vmin = widgets.FloatText(description='color min') - self.widget_cmap_vmax = widgets.FloatText(description='color max') - self.widget_cmap_v = widgets.VBox([self.widget_cmap_vmin, self.widget_cmap_vmax]) - link((self.state, 'cmap_vmin'), (self.widget_cmap_vmin, 'value'), lambda value: value or 0) - link((self.state, 'cmap_vmax'), (self.widget_cmap_vmax, 'value'), lambda value: value or 1) - children.extend((self.widget_cmap_att, self.widget_cmap_v)) + self.widget_cmap_att = LinkedDropdown(self.state, 'cmap_att', + ui_name='color attribute', + label='color attribute') + self.widget_cmap_vmin = widgets.FloatText(description='color min') + self.widget_cmap_vmax = widgets.FloatText(description='color max') + self.widget_cmap_v = widgets.VBox([self.widget_cmap_vmin, self.widget_cmap_vmax]) + link((self.state, 'cmap_vmin'), + (self.widget_cmap_vmin, 'value'), + lambda value: value or 0) + link((self.state, 'cmap_vmax'), + (self.widget_cmap_vmax, 'value'), + lambda value: value or 1) + children.extend((self.widget_cmap_att, self.widget_cmap_v)) self.widget_cmap = widgets.Dropdown(options=colormaps, description='colormap') children.append(self.widget_cmap) From 6ee27aa2d724a323fa0ee7265c61a8be14328a32 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sat, 5 Jul 2025 02:14:34 -0400 Subject: [PATCH 7/8] For volume subsets, only use a regular color picker. --- glue_jupyter/ipyvolume/volume/layer_style_widget.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/glue_jupyter/ipyvolume/volume/layer_style_widget.py b/glue_jupyter/ipyvolume/volume/layer_style_widget.py index 875be827..d6c9f3f9 100644 --- a/glue_jupyter/ipyvolume/volume/layer_style_widget.py +++ b/glue_jupyter/ipyvolume/volume/layer_style_widget.py @@ -1,6 +1,9 @@ -from ipywidgets import (Checkbox, VBox, Dropdown, FloatSlider, +from ipywidgets import (Checkbox, VBox, ColorPicker, Dropdown, FloatSlider, FloatLogSlider) +from glue.core.subset import Subset +from glue.utils import color2hex + from glue_jupyter.widgets import Color from glue_jupyter.widgets.linked_dropdown import LinkedDropdown @@ -54,7 +57,11 @@ def __init__(self, layer_state): self.widget_clamp_max = Checkbox(description='clamp maximum', value=self.state.clamp_max) link((self.state, 'clamp_max'), (self.widget_clamp_max, 'value')) - self.widget_color = Color(state=self.state, cmap_mode_attr='color_mode', cmap_att=None) + if isinstance(layer_state.layer, Subset): + self.widget_color = ColorPicker(value=color2hex(self.state.color), description='color') + link((self.state, 'color'), (self.widget_color, 'value'), color2hex) + else: + self.widget_color = Color(state=self.state, cmap_mode_attr='color_mode', cmap_att=None) if self.state.alpha is None: self.state.alpha = 1 From 827dde629be948bdd89dcb1a76ca811489a47911 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 24 Nov 2025 17:15:46 -0500 Subject: [PATCH 8/8] Bump glue-vispy-viewers version. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 24886092..1a41e487 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ setup_requires = setuptools_scm install_requires = glue-core>=1.23.0 - glue-vispy-viewers[jupyter]>=1.2.1 + glue-vispy-viewers[jupyter]>=1.3.0 notebook>=4.0 ipython_genutils>=0.2 ipympl>=0.3.0