From fe41a587f5dc42df2affed305651705450c7e192 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 27 Feb 2024 15:13:34 +0000 Subject: [PATCH 01/10] Initial work on unit conversion for contour levels --- glue_jupyter/bqplot/image/state.py | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py index f65f4950..bda42d00 100644 --- a/glue_jupyter/bqplot/image/state.py +++ b/glue_jupyter/bqplot/image/state.py @@ -19,6 +19,7 @@ class BqplotImageViewerState(ImageViewerState): class BqplotImageLayerState(ImageLayerState): c_min = DDCProperty(docstring='The lower level used for the contours') c_max = DDCProperty(docstring='The upper level used for the contours') + c_display_unit = DDSCProperty(docstring='The units to use to display contour levels') level_mode = DDSCProperty(0, docstring='How to distribute the contour levels') n_levels = DDCProperty(5, docstring='The number of levels, in Linear mode') levels = CallbackProperty(docstring='List of values where to create the contour lines') @@ -48,11 +49,21 @@ def __init__(self, *args, **kwargs): percentile='contour_percentile', lower='c_min', upper='c_max') + def format_unit(unit): + if unit is None: + return 'Native units' + else: + return unit + + BqplotImageLayerState.c_display_unit.set_display_func(self, format_unit) + self.add_callback('n_levels', self._update_levels) self.add_callback('c_min', self._update_levels) self.add_callback('c_max', self._update_levels) self.add_callback('level_mode', self._update_levels) self.add_callback('levels', self._update_labels) + self.add_callback('c_display_unit', self._convert_units_c_limits, echo_old=True) + self._update_levels() def _update_priority(self, name): @@ -72,3 +83,32 @@ def _update_levels(self, ignore=None): def _update_labels(self, ignore=None): # TODO: we may want to have ways to configure this in the future self.labels = ["{0:.4g}".format(level) for level in self.levels] + + def _convert_units_c_limits(self, old_unit, new_unit): + + if ( + getattr(self, '_previous_attribute', None) is self.attribute and + old_unit != new_unit and + self.layer is not None + ): + + limits = np.hstack([self.c_min, self.c_max, self.levels]) + + converter = UnitConverter() + + limits_native = converter.to_native(self.layer, + self.attribute, limits, + old_unit) + + limits_new = converter.to_unit(self.layer, + self.attribute, limits_native, + new_unit) + + with delay_callback(self, 'c_min', 'c_max', 'levels'): + self.c_min, self.c_max = sorted(limits_new[:2]) + self.levels = tuple(limits_new[2:]) + + # Make sure that we keep track of what attribute the limits + # are for - if the attribute changes, we should not try and + # update the limits. + self._previous_attribute = self.attribute From 8639658835771a3f22032a73833f8b3c231f8623 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 28 Feb 2024 14:27:32 +0000 Subject: [PATCH 02/10] Added visual test for units of contours and fix issues --- glue_jupyter/bqplot/image/layer_artist.py | 12 ++++++ glue_jupyter/bqplot/image/state.py | 22 ++++++++-- .../bqplot/image/tests/test_visual.py | 40 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 glue_jupyter/bqplot/image/tests/test_visual.py diff --git a/glue_jupyter/bqplot/image/layer_artist.py b/glue_jupyter/bqplot/image/layer_artist.py index dffff1a4..66523c58 100644 --- a/glue_jupyter/bqplot/image/layer_artist.py +++ b/glue_jupyter/bqplot/image/layer_artist.py @@ -4,6 +4,7 @@ from glue.viewers.image.layer_artist import BaseImageLayerArtist, ImageLayerArtist, ImageSubsetArray from glue.viewers.image.state import ImageSubsetLayerState from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE +from glue.core.units import find_unit_choices, UnitConverter from ...link import link from bqplot_image_gl import Contour @@ -88,6 +89,17 @@ def _update_contour_lines(self): self.contour_artist.contour_lines = [] return + # As the levels may be specified in a different unit we should convert + # the data to match the units of the levels (we do it this way around + # so that the labels are shown in the new units) + + converter = UnitConverter() + + contour_data = converter.to_unit(self.state.layer, + self.state.attribute, + contour_data, + self.state.c_display_unit) + for level in self.state.levels: if level not in self._contour_line_cache: contour_line_set = skimage.measure.find_contours(contour_data.T, level) diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py index bda42d00..edbf5a98 100644 --- a/glue_jupyter/bqplot/image/state.py +++ b/glue_jupyter/bqplot/image/state.py @@ -1,11 +1,12 @@ import numpy as np -from echo import CallbackProperty +from echo import CallbackProperty, delay_callback from glue.viewers.matplotlib.state import (DeferredDrawCallbackProperty as DDCProperty, DeferredDrawSelectionCallbackProperty as DDSCProperty) from glue.viewers.image.state import ImageViewerState, ImageLayerState from glue.core.state_objects import StateAttributeLimitsHelper +from glue.core.units import find_unit_choices, UnitConverter class BqplotImageViewerState(ImageViewerState): @@ -62,8 +63,10 @@ def format_unit(unit): self.add_callback('c_max', self._update_levels) self.add_callback('level_mode', self._update_levels) self.add_callback('levels', self._update_labels) + self.add_callback('attribute', self._update_c_display_unit_choices) self.add_callback('c_display_unit', self._convert_units_c_limits, echo_old=True) + self._update_c_display_unit_choices() self._update_levels() def _update_priority(self, name): @@ -77,13 +80,26 @@ def _update_priority(self, name): def _update_levels(self, ignore=None): if self.level_mode == "Linear": - # TODO: this is exclusive begin/end point, is that a good choise? - self.levels = np.linspace(self.c_min, self.c_max, self.n_levels+2)[1:-1].tolist() + self.levels = np.linspace(self.c_min, self.c_max, self.n_levels).tolist() def _update_labels(self, ignore=None): # TODO: we may want to have ways to configure this in the future self.labels = ["{0:.4g}".format(level) for level in self.levels] + def _update_c_display_unit_choices(self, *args): + + if self.layer is None: + BqplotImageLayerState.c_display_unit.set_choices(self, []) + return + + component = self.layer.get_component(self.attribute) + if component.units: + c_choices = find_unit_choices([(self.layer, self.attribute, component.units)]) + else: + c_choices = [''] + BqplotImageLayerState.c_display_unit.set_choices(self, c_choices) + self.c_display_unit = component.units + def _convert_units_c_limits(self, old_unit, new_unit): if ( diff --git a/glue_jupyter/bqplot/image/tests/test_visual.py b/glue_jupyter/bqplot/image/tests/test_visual.py new file mode 100644 index 00000000..89365af8 --- /dev/null +++ b/glue_jupyter/bqplot/image/tests/test_visual.py @@ -0,0 +1,40 @@ +import numpy as np +from numpy.testing import assert_allclose +import matplotlib.pyplot as plt + +from glue_jupyter import jglue +from glue_jupyter.tests.helpers import visual_widget_test + + +@visual_widget_test +def test_contour_units( + tmp_path, + page_session, + solara_test, +): + + x = np.linspace(-7, 7, 88) + y = np.linspace(-6, 6, 69) + X, Y = np.meshgrid(x, y) + Z = np.exp(-(X * X + Y * Y) / 4) + + app = jglue() + data = app.add_data(data={"x": X, "y": Y, "z": Z})[0] + data.get_component("z").units = 'km' + image = app.imshow(show=False) + image.state.layers[0].attribute = data.id['z'] + image.state.layers[0].contour_visible = True + image.state.layers[0].c_min = 0.1 + image.state.layers[0].c_max = 0.9 + image.state.layers[0].n_levels = 5 + + assert_allclose(image.state.layers[0].levels, [0.1, 0.3, 0.5, 0.7, 0.9]) + + image.state.layers[0].c_display_unit = 'm' + + assert_allclose(image.state.layers[0].levels, [100, 300, 500, 700, 900]) + assert image.state.layers[0].labels == ['100', '300', '500', '700', '900'] + + figure = image.figure_widget + figure.layout = {"width": "400px", "height": "250px"} + return figure From 8b10cc3eb1d5e11a5181920e10c82a057492f003 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 28 Feb 2024 14:40:17 +0000 Subject: [PATCH 03/10] Updated figure hash for visual test of contour --- glue_jupyter/tests/images/py311-test-visual.json | 1 + 1 file changed, 1 insertion(+) diff --git a/glue_jupyter/tests/images/py311-test-visual.json b/glue_jupyter/tests/images/py311-test-visual.json index ffafa5e4..cefcdb48 100644 --- a/glue_jupyter/tests/images/py311-test-visual.json +++ b/glue_jupyter/tests/images/py311-test-visual.json @@ -1,4 +1,5 @@ { + "glue_jupyter.bqplot.image.tests.test_visual.test_contour_units[chromium]": "9624f0d0af9b0055b1f738c107820dc395598597fce1c40cbf62c2532b5db7e9", "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d[chromium]": "fbdd9fe2649a0d72813c03e77af6233909df64207cb834f28da479f50b9e7a1d", "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d_density[chromium]": "d843a816a91e37cb0212c7caae913d7563f6c2eb42b49fa18345a5952e093b2f" } \ No newline at end of file From 9974233dd12b264a2113f6056b241ba8d569d8f0 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 28 Feb 2024 14:41:16 +0000 Subject: [PATCH 04/10] Fix code style --- glue_jupyter/bqplot/image/layer_artist.py | 2 +- glue_jupyter/bqplot/image/tests/test_visual.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/glue_jupyter/bqplot/image/layer_artist.py b/glue_jupyter/bqplot/image/layer_artist.py index 66523c58..3567d3e7 100644 --- a/glue_jupyter/bqplot/image/layer_artist.py +++ b/glue_jupyter/bqplot/image/layer_artist.py @@ -4,7 +4,7 @@ from glue.viewers.image.layer_artist import BaseImageLayerArtist, ImageLayerArtist, ImageSubsetArray from glue.viewers.image.state import ImageSubsetLayerState from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE -from glue.core.units import find_unit_choices, UnitConverter +from glue.core.units import UnitConverter from ...link import link from bqplot_image_gl import Contour diff --git a/glue_jupyter/bqplot/image/tests/test_visual.py b/glue_jupyter/bqplot/image/tests/test_visual.py index 89365af8..29203974 100644 --- a/glue_jupyter/bqplot/image/tests/test_visual.py +++ b/glue_jupyter/bqplot/image/tests/test_visual.py @@ -1,6 +1,5 @@ import numpy as np from numpy.testing import assert_allclose -import matplotlib.pyplot as plt from glue_jupyter import jglue from glue_jupyter.tests.helpers import visual_widget_test From c87382271ddbfa346bbd0d517ff1a18c245a67e6 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 28 Feb 2024 15:05:40 +0000 Subject: [PATCH 05/10] Fixed tests --- glue_jupyter/bqplot/image/tests/test_viewer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/glue_jupyter/bqplot/image/tests/test_viewer.py b/glue_jupyter/bqplot/image/tests/test_viewer.py index 7365f06e..fb083a75 100644 --- a/glue_jupyter/bqplot/image/tests/test_viewer.py +++ b/glue_jupyter/bqplot/image/tests/test_viewer.py @@ -40,7 +40,7 @@ def test_contour_levels(app, data_image, data_volume): layer.state.c_min = 0 layer.state.c_max = 10 layer.state.n_levels = 3 - assert layer.state.levels == [2.5, 5, 7.5] + assert layer.state.levels == [0, 5, 10] # since we start invisible, we don't compute the contour lines assert len(layer.contour_artist.contour_lines) == 0 # make the visible, so we trigger a compute @@ -48,9 +48,9 @@ def test_contour_levels(app, data_image, data_volume): assert len(layer.contour_artist.contour_lines) == 3 layer.state.level_mode = 'Custom' layer.state.n_levels = 1 - assert layer.state.levels == [2.5, 5, 7.5] + assert layer.state.levels == [0, 5, 10] layer.state.level_mode = 'Linear' - assert layer.state.levels == [5] + assert layer.state.levels == [0] assert len(layer.contour_artist.contour_lines) == 1 # test the visual attributes @@ -81,7 +81,7 @@ def test_contour_state(app, data_image): {'level_mode': 'Linear', 'levels': [2, 3]} ) # Without priority of levels, this gets set to [2, 3] - assert layer.state.levels == [2.5, 5, 7.5] + assert layer.state.levels == [0, 5, 10] def test_add_markers_zoom(app, data_image, data_volume, dataxyz): From 3ce8770121f589b0a76de7ed5e958121dcc2ccc9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 16 Apr 2024 14:59:34 +0100 Subject: [PATCH 06/10] Re-use attribute_display_unit from glue-core --- glue_jupyter/bqplot/image/layer_artist.py | 2 +- glue_jupyter/bqplot/image/state.py | 21 +------------------ .../bqplot/image/tests/test_visual.py | 2 +- setup.cfg | 3 +-- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/glue_jupyter/bqplot/image/layer_artist.py b/glue_jupyter/bqplot/image/layer_artist.py index 3567d3e7..7a546872 100644 --- a/glue_jupyter/bqplot/image/layer_artist.py +++ b/glue_jupyter/bqplot/image/layer_artist.py @@ -98,7 +98,7 @@ def _update_contour_lines(self): contour_data = converter.to_unit(self.state.layer, self.state.attribute, contour_data, - self.state.c_display_unit) + self.state.attribute_display_unit) for level in self.state.levels: if level not in self._contour_line_cache: diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py index edbf5a98..24431700 100644 --- a/glue_jupyter/bqplot/image/state.py +++ b/glue_jupyter/bqplot/image/state.py @@ -20,7 +20,6 @@ class BqplotImageViewerState(ImageViewerState): class BqplotImageLayerState(ImageLayerState): c_min = DDCProperty(docstring='The lower level used for the contours') c_max = DDCProperty(docstring='The upper level used for the contours') - c_display_unit = DDSCProperty(docstring='The units to use to display contour levels') level_mode = DDSCProperty(0, docstring='How to distribute the contour levels') n_levels = DDCProperty(5, docstring='The number of levels, in Linear mode') levels = CallbackProperty(docstring='List of values where to create the contour lines') @@ -56,17 +55,13 @@ def format_unit(unit): else: return unit - BqplotImageLayerState.c_display_unit.set_display_func(self, format_unit) - self.add_callback('n_levels', self._update_levels) self.add_callback('c_min', self._update_levels) self.add_callback('c_max', self._update_levels) self.add_callback('level_mode', self._update_levels) self.add_callback('levels', self._update_labels) - self.add_callback('attribute', self._update_c_display_unit_choices) - self.add_callback('c_display_unit', self._convert_units_c_limits, echo_old=True) + self.add_callback('attribute_display_unit', self._convert_units_c_limits, echo_old=True) - self._update_c_display_unit_choices() self._update_levels() def _update_priority(self, name): @@ -86,20 +81,6 @@ def _update_labels(self, ignore=None): # TODO: we may want to have ways to configure this in the future self.labels = ["{0:.4g}".format(level) for level in self.levels] - def _update_c_display_unit_choices(self, *args): - - if self.layer is None: - BqplotImageLayerState.c_display_unit.set_choices(self, []) - return - - component = self.layer.get_component(self.attribute) - if component.units: - c_choices = find_unit_choices([(self.layer, self.attribute, component.units)]) - else: - c_choices = [''] - BqplotImageLayerState.c_display_unit.set_choices(self, c_choices) - self.c_display_unit = component.units - def _convert_units_c_limits(self, old_unit, new_unit): if ( diff --git a/glue_jupyter/bqplot/image/tests/test_visual.py b/glue_jupyter/bqplot/image/tests/test_visual.py index 29203974..3ef57110 100644 --- a/glue_jupyter/bqplot/image/tests/test_visual.py +++ b/glue_jupyter/bqplot/image/tests/test_visual.py @@ -29,7 +29,7 @@ def test_contour_units( assert_allclose(image.state.layers[0].levels, [0.1, 0.3, 0.5, 0.7, 0.9]) - image.state.layers[0].c_display_unit = 'm' + image.state.layers[0].attribute_display_unit = 'm' assert_allclose(image.state.layers[0].levels, [100, 300, 500, 700, 900]) assert image.state.layers[0].labels == ['100', '300', '500', '700', '900'] diff --git a/setup.cfg b/setup.cfg index 74f4df05..e141b06f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ python_requires = >=3.8 setup_requires = setuptools_scm install_requires = - glue-core>=1.17.1 + glue-core>=1.19.0 glue-vispy-viewers>=1.0 notebook>=4.0 ipympl>=0.3.0 @@ -33,7 +33,6 @@ test = pytest pytest-cov nbconvert>=6.4.5 - glue-core!=1.2.4; python_version == '3.10' visualtest = playwright pytest-playwright From 2e0f056d605e64ef0333830af57100bc4b5a23df Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 16 Apr 2024 15:00:59 +0100 Subject: [PATCH 07/10] Fix code style --- glue_jupyter/bqplot/image/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py index 24431700..3985cd7b 100644 --- a/glue_jupyter/bqplot/image/state.py +++ b/glue_jupyter/bqplot/image/state.py @@ -6,7 +6,7 @@ from glue.viewers.image.state import ImageViewerState, ImageLayerState from glue.core.state_objects import StateAttributeLimitsHelper -from glue.core.units import find_unit_choices, UnitConverter +from glue.core.units import UnitConverter class BqplotImageViewerState(ImageViewerState): From bc26685415a0f6c1c51a0d134c352d2717fd1e43 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 17 Apr 2024 20:50:53 +0100 Subject: [PATCH 08/10] Bumped minimum required glue-core version to v1.20.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e141b06f..8ec43920 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ python_requires = >=3.8 setup_requires = setuptools_scm install_requires = - glue-core>=1.19.0 + glue-core>=1.20.0 glue-vispy-viewers>=1.0 notebook>=4.0 ipympl>=0.3.0 From 855429a475c8d134b45ba2b7ef11d2f5c0ddd8c7 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 17 Apr 2024 20:55:28 +0100 Subject: [PATCH 09/10] Remove unused function --- glue_jupyter/bqplot/image/state.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py index 3985cd7b..f045f4ed 100644 --- a/glue_jupyter/bqplot/image/state.py +++ b/glue_jupyter/bqplot/image/state.py @@ -32,6 +32,7 @@ class BqplotImageLayerState(ImageLayerState): contour_visible = CallbackProperty(False, 'whether to show the image as contours') def __init__(self, *args, **kwargs): + super(BqplotImageLayerState, self).__init__(*args, **kwargs) BqplotImageLayerState.level_mode.set_choices(self, ['Linear', 'Custom']) @@ -49,12 +50,6 @@ def __init__(self, *args, **kwargs): percentile='contour_percentile', lower='c_min', upper='c_max') - def format_unit(unit): - if unit is None: - return 'Native units' - else: - return unit - self.add_callback('n_levels', self._update_levels) self.add_callback('c_min', self._update_levels) self.add_callback('c_max', self._update_levels) From 2035138047eba546e587cde1b7517e8e206b86a2 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Wed, 17 Apr 2024 21:04:32 +0100 Subject: [PATCH 10/10] Updated visual test hash --- glue_jupyter/tests/images/py311-test-visual.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_jupyter/tests/images/py311-test-visual.json b/glue_jupyter/tests/images/py311-test-visual.json index cefcdb48..9e0891fa 100644 --- a/glue_jupyter/tests/images/py311-test-visual.json +++ b/glue_jupyter/tests/images/py311-test-visual.json @@ -1,5 +1,5 @@ { - "glue_jupyter.bqplot.image.tests.test_visual.test_contour_units[chromium]": "9624f0d0af9b0055b1f738c107820dc395598597fce1c40cbf62c2532b5db7e9", + "glue_jupyter.bqplot.image.tests.test_visual.test_contour_units[chromium]": "fa4f68c5c62e1437c1666c656ba02376396f6c75b6f7956f712c760569a2045b", "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d[chromium]": "fbdd9fe2649a0d72813c03e77af6233909df64207cb834f28da479f50b9e7a1d", "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d_density[chromium]": "d843a816a91e37cb0212c7caae913d7563f6c2eb42b49fa18345a5952e093b2f" } \ No newline at end of file