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

Implement unit conversion for contour levels #423

Merged
merged 10 commits into from
Apr 17, 2024
12 changes: 12 additions & 0 deletions glue_jupyter/bqplot/image/layer_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 35 additions & 3 deletions glue_jupyter/bqplot/image/state.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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'])
Expand All @@ -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):
Expand All @@ -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
8 changes: 4 additions & 4 deletions glue_jupyter/bqplot/image/tests/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ 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
layer.state.contour_visible = True
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
Expand Down Expand Up @@ -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):
Expand Down
39 changes: 39 additions & 0 deletions glue_jupyter/bqplot/image/tests/test_visual.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions glue_jupyter/tests/images/py311-test-visual.json
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading