diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 4eca2e6b0bdb..25cafef60d79 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3,6 +3,7 @@ import logging import math from numbers import Integral, Number +from datetime import timedelta import numpy as np from numpy import ma @@ -3180,7 +3181,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, errors. - *None*: No errorbar. - Note that all error arrays should have *positive* values. + Note that all error arrays should have *non-negative* values. See :doc:`/gallery/statistics/errorbar_features` for an example on the usage of ``xerr`` and ``yerr``. @@ -3284,6 +3285,18 @@ def errorbar(self, x, y, yerr=None, xerr=None, if len(x) != len(y): raise ValueError("'x' and 'y' must have the same size") + def has_negative_values(array): + if array is None: + return False + try: + return np.any(array < 0) + except TypeError: # if array contains 'datetime.timedelta' types + return np.any(array < timedelta(0)) + + if has_negative_values(xerr) or has_negative_values(yerr): + raise ValueError( + "'xerr' and 'yerr' must have non-negative values") + if isinstance(errorevery, Integral): errorevery = (0, errorevery) if isinstance(errorevery, tuple): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3168fe86ad66..d270136f9667 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3513,6 +3513,25 @@ def test_errorbar_every_invalid(): ax.errorbar(x, y, yerr, errorevery='foobar') +def test_xerr_yerr_positive(): + ax = plt.figure().subplots() + + error_message = "'xerr' and 'yerr' must have non-negative values" + + with pytest.raises(ValueError, match=error_message): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, match=error_message): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]]) + with pytest.raises(ValueError, match=error_message): + ax.errorbar(x=[0], y=[0], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, match=error_message): + x = np.arange(5) + y = [datetime.datetime(2021, 9, i * 2 + 1) for i in x] + ax.errorbar(x=x, + y=y, + yerr=datetime.timedelta(days=-10)) + + @check_figures_equal() def test_errorbar_every(fig_test, fig_ref): x = np.linspace(0, 1, 15)