diff --git a/doc/users/next_whats_new/errorbar_errorevery.rst b/doc/users/next_whats_new/errorbar_errorevery.rst new file mode 100644 index 000000000000..f773f6574389 --- /dev/null +++ b/doc/users/next_whats_new/errorbar_errorevery.rst @@ -0,0 +1,20 @@ +``errorbar`` *errorevery* parameter matches *markevery* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to the *markevery* parameter to `~.Axes.plot`, the *errorevery* +parameter of `~.Axes.errorbar` now accept slices and NumPy fancy indexes (which +must match the size of *x*). + +.. plot:: + + x = np.linspace(0, 1, 15) + y = x * (1-x) + yerr = y/6 + + fig, ax = plt.subplots(2, constrained_layout=True) + ax[0].errorbar(x, y, yerr, capsize=2) + ax[0].set_title('errorevery unspecified') + + ax[1].errorbar(x, y, yerr, capsize=2, + errorevery=[False, True, True, False, True] * 3) + ax[1].set_title('errorevery=[False, True, True, False, True] * 3') diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 430417d7367b..4b7fbe68d85b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2,7 +2,7 @@ import itertools import logging import math -from numbers import Number +from numbers import Integral, Number import numpy as np from numpy import ma @@ -3147,17 +3147,6 @@ def errorbar(self, x, y, yerr=None, xerr=None, kwargs = {k: v for k, v in kwargs.items() if v is not None} kwargs.setdefault('zorder', 2) - try: - offset, errorevery = errorevery - except TypeError: - offset = 0 - - if errorevery < 1 or int(errorevery) != errorevery: - raise ValueError( - 'errorevery must be positive integer or tuple of integers') - if int(offset) != offset: - raise ValueError("errorevery's starting index must be an integer") - self._process_unit_info([("x", x), ("y", y)], kwargs, convert=False) # Make sure all the args are iterable; use lists not arrays to preserve @@ -3179,6 +3168,33 @@ def errorbar(self, x, y, yerr=None, xerr=None, if not np.iterable(yerr): yerr = [yerr] * len(y) + if isinstance(errorevery, Integral): + errorevery = (0, errorevery) + if isinstance(errorevery, tuple): + if (len(errorevery) == 2 and + isinstance(errorevery[0], Integral) and + isinstance(errorevery[1], Integral)): + errorevery = slice(errorevery[0], None, errorevery[1]) + else: + raise ValueError( + f'errorevery={errorevery!r} is a not a tuple of two ' + f'integers') + + elif isinstance(errorevery, slice): + pass + + elif not isinstance(errorevery, str) and np.iterable(errorevery): + # fancy indexing + try: + x[errorevery] + except (ValueError, IndexError) as err: + raise ValueError( + f"errorevery={errorevery!r} is iterable but not a valid " + f"NumPy fancy index to match 'xerr'/'yerr'") from err + else: + raise ValueError( + f"errorevery={errorevery!r} is not a recognized value") + label = kwargs.pop("label", None) kwargs['label'] = '_nolegend_' @@ -3219,6 +3235,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, base_style.pop('markerfacecolor', None) base_style.pop('markeredgewidth', None) base_style.pop('markeredgecolor', None) + base_style.pop('markevery', None) base_style.pop('linestyle', None) # Make the style dict for the line collections (the bars). @@ -3260,7 +3277,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, xuplims = np.broadcast_to(xuplims, len(x)).astype(bool) everymask = np.zeros(len(x), bool) - everymask[offset::errorevery] = True + everymask[errorevery] = True def apply_mask(arrays, mask): # Return, for each array in *arrays*, the elements for which *mask* diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index d30c97932e89..c715cb1ec355 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3389,8 +3389,25 @@ def test_errorbar_with_prop_cycle(fig_test, fig_ref): ax.set_xlim(1, 11) +def test_errorbar_every_invalid(): + x = np.linspace(0, 1, 15) + y = x * (1-x) + yerr = y/6 + + ax = plt.figure().subplots() + + with pytest.raises(ValueError, match='not a tuple of two integers'): + ax.errorbar(x, y, yerr, errorevery=(1, 2, 3)) + with pytest.raises(ValueError, match='not a tuple of two integers'): + ax.errorbar(x, y, yerr, errorevery=(1.3, 3)) + with pytest.raises(ValueError, match='not a valid NumPy fancy index'): + ax.errorbar(x, y, yerr, errorevery=[False, True]) + with pytest.raises(ValueError, match='not a recognized value'): + ax.errorbar(x, y, yerr, errorevery='foobar') + + @check_figures_equal() -def test_errorbar_offsets(fig_test, fig_ref): +def test_errorbar_every(fig_test, fig_ref): x = np.linspace(0, 1, 15) y = x * (1-x) yerr = y/6 @@ -3401,7 +3418,7 @@ def test_errorbar_offsets(fig_test, fig_ref): for color, shift in zip('rgbk', [0, 0, 2, 7]): y += .02 - # Using feature in question + # Check errorevery using an explicit offset and step. ax_test.errorbar(x, y, yerr, errorevery=(shift, 4), capsize=4, c=color) @@ -3411,6 +3428,27 @@ def test_errorbar_offsets(fig_test, fig_ref): ax_ref.errorbar(x[shift::4], y[shift::4], yerr[shift::4], capsize=4, c=color, fmt='none') + # Check that markevery is propagated to line, without affecting errorbars. + ax_test.errorbar(x, y + 0.1, yerr, markevery=(1, 4), capsize=4, fmt='o') + ax_ref.plot(x[1::4], y[1::4] + 0.1, 'o', zorder=2.1) + ax_ref.errorbar(x, y + 0.1, yerr, capsize=4, fmt='none') + + # Check that passing a slice to markevery/errorevery works. + ax_test.errorbar(x, y + 0.2, yerr, errorevery=slice(2, None, 3), + markevery=slice(2, None, 3), + capsize=4, c='C0', fmt='o') + ax_ref.plot(x[2::3], y[2::3] + 0.2, 'o', c='C0', zorder=2.1) + ax_ref.errorbar(x[2::3], y[2::3] + 0.2, yerr[2::3], + capsize=4, c='C0', fmt='none') + + # Check that passing an iterable to markevery/errorevery works. + ax_test.errorbar(x, y + 0.2, yerr, errorevery=[False, True, False] * 5, + markevery=[False, True, False] * 5, + capsize=4, c='C1', fmt='o') + ax_ref.plot(x[1::3], y[1::3] + 0.2, 'o', c='C1', zorder=2.1) + ax_ref.errorbar(x[1::3], y[1::3] + 0.2, yerr[1::3], + capsize=4, c='C1', fmt='none') + @image_comparison(['hist_stacked_stepfilled', 'hist_stacked_stepfilled']) def test_hist_stacked_stepfilled():