diff --git a/glue_jupyter/bqplot/image/layer_artist.py b/glue_jupyter/bqplot/image/layer_artist.py index dffff1a4..7a546872 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 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.attribute_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 f65f4950..f045f4ed 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 UnitConverter class BqplotImageViewerState(ImageViewerState): @@ -31,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']) @@ -53,6 +55,8 @@ def __init__(self, *args, **kwargs): 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_display_unit', self._convert_units_c_limits, echo_old=True) + self._update_levels() def _update_priority(self, name): @@ -66,9 +70,37 @@ 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 _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 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): 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..3ef57110 --- /dev/null +++ b/glue_jupyter/bqplot/image/tests/test_visual.py @@ -0,0 +1,39 @@ +import numpy as np +from numpy.testing import assert_allclose + +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].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'] + + figure = image.figure_widget + figure.layout = {"width": "400px", "height": "250px"} + return figure diff --git a/glue_jupyter/tests/images/py311-test-visual.json b/glue_jupyter/tests/images/py311-test-visual.json index ffafa5e4..9e0891fa 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]": "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 diff --git a/setup.cfg b/setup.cfg index 74f4df05..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.17.1 + glue-core>=1.20.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