From d91d21648f74d4bb0f9dd131cc73b48fa8c8dc84 Mon Sep 17 00:00:00 2001 From: "Marten H. van Kerkwijk" Date: Thu, 10 Apr 2025 12:03:57 -0400 Subject: [PATCH 1/3] BUG: ensure that errorbar does not error on masked negative errors. errorbar checks that errors are not negative, but a bit convolutedly, in order to avoid triggering on nan. Unfortunately, the work-around for nan means that possible masks get discarded, and hence passing in a masked error array that has a negative but masked value leads to an exception. This PR solves that by simply combining the test for negative values with the indirect isnan test (err == err), so that if a masked array is passed, the test values are masked and ignored in the check. As a bonus, this also means that astropy's ``Masked`` arrays can now be used -- those refuse to write output of tests to unmasked values since that would discard the mask (which is indeed the underlying problem here). --- lib/matplotlib/axes/_axes.py | 3 +-- lib/matplotlib/tests/test_axes.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b46cbce39c58..11edd908b2b6 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3756,8 +3756,7 @@ def apply_mask(arrays, mask): f"'{dep_axis}err' must not contain None. " "Use NaN if you want to skip a value.") - res = np.zeros(err.shape, dtype=bool) # Default in case of nan - if np.any(np.less(err, -err, out=res, where=(err == err))): + if np.any((err < -err) & (err == err)): # like err<0, but also works for timedelta and nan. raise ValueError( f"'{dep_axis}err' must not contain negative values") diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 70d1671cafa3..64cfb3ab2e61 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4538,6 +4538,19 @@ def test_errorbar_nan(fig_test, fig_ref): ax.errorbar([4], [3], [6], fmt="C0") +@check_figures_equal() +def test_errorbar_masked_negative(fig_test, fig_ref): + ax = fig_test.add_subplot() + xs = range(5) + mask = np.array([False, False, True, True, False]) + ys = np.ma.array([1, 2, 2, 2, 3], mask=mask) + es = np.ma.array([4, 5, -1, -10, 6], mask=mask) + ax.errorbar(xs, ys, es) + ax = fig_ref.add_subplot() + ax.errorbar([0, 1], [1, 2], [4, 5]) + ax.errorbar([4], [3], [6], fmt="C0") + + @image_comparison(['hist_stacked_stepfilled.png', 'hist_stacked_stepfilled.png']) def test_hist_stacked_stepfilled(): # make some data From bbe738f14aef14f6238067db1dbab9e7b90768e0 Mon Sep 17 00:00:00 2001 From: "Marten H. van Kerkwijk" Date: Thu, 10 Apr 2025 12:43:58 -0400 Subject: [PATCH 2/3] BUG: ensure we never do nan < nan --- lib/matplotlib/axes/_axes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 11edd908b2b6..82ddc53904b3 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3756,8 +3756,12 @@ def apply_mask(arrays, mask): f"'{dep_axis}err' must not contain None. " "Use NaN if you want to skip a value.") - if np.any((err < -err) & (err == err)): - # like err<0, but also works for timedelta and nan. + # Raise if any errors are negative, but not if they are nan. + # To avoid nan comparisons (which lead to warnings on some + # platforms), we select with `err==err` (which is False for nan). + # Also, since datetime.timedelta cannot be compared with 0, + # we compare with the negative error instead. + if np.any((check := err[err == err]) < -check): raise ValueError( f"'{dep_axis}err' must not contain negative values") # This is like From ebeb83a05920f864d525740e78ec3e41e890bb1e Mon Sep 17 00:00:00 2001 From: Marten Henric van Kerkwijk Date: Sun, 13 Apr 2025 08:18:44 -0400 Subject: [PATCH 3/3] MAINT: make test code a little more explicit in what's done. --- lib/matplotlib/tests/test_axes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 64cfb3ab2e61..c1758d2ec3e0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4532,10 +4532,10 @@ def test_errorbar_nan(fig_test, fig_ref): xs = range(5) ys = np.array([1, 2, np.nan, np.nan, 3]) es = np.array([4, 5, np.nan, np.nan, 6]) - ax.errorbar(xs, ys, es) + ax.errorbar(xs, ys, yerr=es) ax = fig_ref.add_subplot() - ax.errorbar([0, 1], [1, 2], [4, 5]) - ax.errorbar([4], [3], [6], fmt="C0") + ax.errorbar([0, 1], [1, 2], yerr=[4, 5]) + ax.errorbar([4], [3], yerr=[6], fmt="C0") @check_figures_equal() @@ -4545,10 +4545,10 @@ def test_errorbar_masked_negative(fig_test, fig_ref): mask = np.array([False, False, True, True, False]) ys = np.ma.array([1, 2, 2, 2, 3], mask=mask) es = np.ma.array([4, 5, -1, -10, 6], mask=mask) - ax.errorbar(xs, ys, es) + ax.errorbar(xs, ys, yerr=es) ax = fig_ref.add_subplot() - ax.errorbar([0, 1], [1, 2], [4, 5]) - ax.errorbar([4], [3], [6], fmt="C0") + ax.errorbar([0, 1], [1, 2], yerr=[4, 5]) + ax.errorbar([4], [3], yerr=[6], fmt="C0") @image_comparison(['hist_stacked_stepfilled.png', 'hist_stacked_stepfilled.png'])