Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/plopp/core/limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion src/plopp/graphics/bbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is really meant to be treated equivalently to the case that raises ValueError then it could make sense to raise ValueError here too instead of returning the dict, so that we avoid duplicating that logic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here we want to make a bbox with None, so that it gets ignored.

Say you plot 2 lines, and one of them has all NaN. I would expect to just see one line instead of having an error raised. You might then wonder why you have just one line, and in that case raising an error may be safer. But in the case of interactive plots, where one slice somewhere may have all nans and the others are fine, I think it makes sense to not show the data instead of raising.

@jokasimr jokasimr Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mean to raise an error that is visible to the user. I meant that the ValueError will be catched by the surrounding try/catch, and then we return the same dict.fromkeys anyway but with less duplication.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what you meant was:

        values = find_limits(x, scale=scale, pad=pad)
        if np.isnan(values[0].value) or np.isnan(values[1].value):
            raise ValueError("NaN limits")

?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

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))
Expand Down
10 changes: 6 additions & 4 deletions tests/core/limits_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
49 changes: 49 additions & 0 deletions tests/graphics/artists_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions tests/plotting/plot_1d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
6 changes: 6 additions & 0 deletions tests/plotting/plot_2d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
13 changes: 13 additions & 0 deletions tests/plotting/slicer_test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Loading