diff --git a/src/plopp/core/limits.py b/src/plopp/core/limits.py index 689770c6..4378bab0 100644 --- a/src/plopp/core/limits.py +++ b/src/plopp/core/limits.py @@ -59,7 +59,7 @@ def find_limits( v = x.values finite_inds = np.isfinite(v) if np.sum(finite_inds) == 0: - raise ValueError("No finite values were found in array. Cannot compute limits.") + return (sc.scalar(np.nan, unit=x.unit), sc.scalar(np.nan, unit=x.unit)) finite_vals = v[finite_inds] finite_max = None if scale == "log": @@ -98,6 +98,8 @@ def fix_empty_range( """ Range correction in case xmin == xmax """ + if np.isnan(lims[0].value) or np.isnan(lims[1].value): + lims = (sc.scalar(0.0, unit=lims[0].unit), sc.scalar(0.0, unit=lims[1].unit)) if lims[0].value != lims[1].value: return lims if lims[0].value == 0.0: diff --git a/src/plopp/graphics/bbox.py b/src/plopp/graphics/bbox.py index b4e16481..1b9a6bab 100644 --- a/src/plopp/graphics/bbox.py +++ b/src/plopp/graphics/bbox.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Literal +import numpy as np import scipp as sc from ..core.limits import find_limits, fix_empty_range @@ -108,7 +109,11 @@ def axis_bounds( Whether to pad the limits. """ try: - values = fix_empty_range(find_limits(x, scale=scale, pad=pad)) + values = find_limits(x, scale=scale, pad=pad) + if np.isnan(values[0].value) or np.isnan(values[1].value): + return dict.fromkeys(keys, None) + else: + values = fix_empty_range(values) except ValueError: return dict.fromkeys(keys, None) bounds = dict(zip(keys, (val.value for val in values), strict=True)) diff --git a/tests/core/limits_test.py b/tests/core/limits_test.py index c864d456..4af86e5d 100644 --- a/tests/core/limits_test.py +++ b/tests/core/limits_test.py @@ -2,7 +2,6 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import numpy as np -import pytest import scipp as sc from plopp.core.limits import find_limits, fix_empty_range @@ -53,12 +52,15 @@ def test_find_limits_with_ninf(): assert sc.identical(lims[1], sc.scalar(10.0, unit='m')) -def test_find_limits_no_finite_values_raises(): +def test_find_limits_no_finite_values_returns_nan_range(): da = sc.DataArray( data=sc.array(dims=['x'], values=[np.nan, np.inf, -np.inf, np.nan], unit='m') ) - with pytest.raises(ValueError, match="No finite values were found in array"): - _ = find_limits(da) + lims = find_limits(da) + assert np.isnan(lims[0].value) + assert np.isnan(lims[1].value) + assert lims[0].unit == 'm' + assert lims[1].unit == 'm' def test_find_limits_all_zeros(): diff --git a/tests/graphics/artists_test.py b/tests/graphics/artists_test.py index b2641b0a..1a3b1ed8 100644 --- a/tests/graphics/artists_test.py +++ b/tests/graphics/artists_test.py @@ -3,7 +3,9 @@ from functools import partial +import numpy as np import pytest +import scipp as sc import plopp as pp from plopp.data import examples @@ -84,3 +86,50 @@ def test_opacity(self, set_backend, backend, func, data): assert artist.opacity in (None, 1.0) artist.opacity = 0.5 assert artist.opacity == 0.5 + + +LINE_CASES = {k: v for k, v in CASES.items() if k.startswith("line")} + + +@pytest.mark.parametrize( + ("backend", "func", "data"), LINE_CASES.values(), ids=LINE_CASES.keys() +) +class TestLineBBox: + def test_line_bbox(self, set_backend, backend, func, data): + fig = func(**data) + [artist] = fig.artists.values() + bbox = artist.bbox(xscale='linear', yscale='linear') + # Tolerance of 3 because there is padding around the line + assert np.isclose(bbox.xmin, data['obj'].coords['x'].min().value, atol=3.0) + assert np.isclose(bbox.xmax, data['obj'].coords['x'].max().value, atol=3.0) + + if data['obj'].variances is not None: + ymin = (data['obj'] - sc.stddevs(data['obj'])).min() + else: + ymin = data['obj'].min() + # Tolerance of 0.2 because there is padding below the line + assert np.isclose(bbox.ymin, ymin.value, atol=0.2) + + if data['obj'].variances is not None: + ymax = (data['obj'] + sc.stddevs(data['obj'])).max() + else: + ymax = data['obj'].max() + # Tolerance of 0.2 because there is padding above the line + assert np.isclose(bbox.ymax, ymax.value, atol=0.2) + + +LINE_BACKENDS = {k: v[0] for k, v in CASES.items() if k.startswith("line")} + + +@pytest.mark.parametrize(("backend"), LINE_BACKENDS.values(), ids=LINE_BACKENDS.keys()) +class TestLineBBoxAllNan: + def test_line_bbox_all_nan(self, set_backend, backend): + a = pp.data.data1d() + a.values[...] = np.nan + fig = pp.plot(obj=a) + [artist] = fig.artists.values() + bbox = artist.bbox(xscale='linear', yscale='linear') + assert bbox.xmin is not None + assert bbox.xmax is not None + assert bbox.ymin is None + assert bbox.ymax is None diff --git a/tests/plotting/plot_1d_test.py b/tests/plotting/plot_1d_test.py index 464a53c6..54ac1a9c 100644 --- a/tests/plotting/plot_1d_test.py +++ b/tests/plotting/plot_1d_test.py @@ -708,3 +708,9 @@ def test_plot_multiple_inputs_custom_type_keys(): fig = pp.plot({A: a, B: b}) assert len(fig.view.artists) == 2 + + +def test_plot_data_with_all_nans_does_not_raise(): + da = data_array(ndim=1) + da.values[...] = np.nan + _ = da.plot() diff --git a/tests/plotting/plot_2d_test.py b/tests/plotting/plot_2d_test.py index 9384dd8b..b278dc2c 100644 --- a/tests/plotting/plot_2d_test.py +++ b/tests/plotting/plot_2d_test.py @@ -521,3 +521,9 @@ def test_clabel(): da = data_array(ndim=2) fig = da.plot(clabel='MyColorLabel') assert fig.view.colormapper.clabel == 'MyColorLabel' + + +def test_plot_data_with_all_nans_does_not_raise(): + da = data_array(ndim=2) + da.values[...] = np.nan + _ = da.plot() diff --git a/tests/plotting/slicer_test.py b/tests/plotting/slicer_test.py index 241d6aec..72d8267d 100644 --- a/tests/plotting/slicer_test.py +++ b/tests/plotting/slicer_test.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +import numpy as np import pytest import scipp as sc from scipp.testing import assert_allclose, assert_identical @@ -240,6 +241,12 @@ def test_different_operations(self, operation, mode): da = data_array(ndim=2) SlicerPlot(da, keep=['xx'], mode=mode, operation=operation) + @pytest.mark.parametrize("mode", ["single", "range", "combined"]) + def test_slicer_with_first_all_nan_slice_does_not_raise(self, mode): + da = data_array(ndim=2) + da['yy', 0].values[...] = np.nan + SlicerPlot(da, keep=['xx'], mode=mode) + @pytest.mark.usefixtures("_parametrize_interactive_2d_backends") class TestSlicer2d: @@ -407,3 +414,9 @@ def test_create_with_non_dimension_coord(self): def test_different_operations(self, operation, mode): da = data_array(ndim=3) SlicerPlot(da, keep=['xx', 'yy'], mode=mode, operation=operation) + + @pytest.mark.parametrize("mode", ["single", "range", "combined"]) + def test_slicer_with_first_all_nan_slice_does_not_raise(self, mode): + da = data_array(ndim=3) + da['zz', 0].values[...] = np.nan + SlicerPlot(da, keep=['xx'], mode=mode)