diff --git a/doc/users/next_whats_new/errorbar_offsets.rst b/doc/users/next_whats_new/errorbar_offsets.rst new file mode 100644 index 000000000000..b2ca9f7a23c5 --- /dev/null +++ b/doc/users/next_whats_new/errorbar_offsets.rst @@ -0,0 +1,10 @@ +Errorbar plots can shift which points have error bars +----------------------------------------------------- + +Previously, `plt.errorbar()` accepted a kwarg `errorevery` such that the +command `plt.errorbar(x, y, yerr, errorevery=6)` would add error bars to +datapoints `x[::6], y[::6]`. + +`errorbar()` now also accepts a tuple for `errorevery` such that +`plt.errorbar(x, y, yerr, errorevery=(start, N))` adds error bars to points +`x[start::N], y[start::N]`. diff --git a/examples/lines_bars_and_markers/errorbar_subsample.py b/examples/lines_bars_and_markers/errorbar_subsample.py index 8b4d11087ac7..a54c66cf5655 100644 --- a/examples/lines_bars_and_markers/errorbar_subsample.py +++ b/examples/lines_bars_and_markers/errorbar_subsample.py @@ -12,23 +12,29 @@ # example data x = np.arange(0.1, 4, 0.1) -y = np.exp(-x) +y1 = np.exp(-1.0 * x) +y2 = np.exp(-0.5 * x) # example variable error bar values -yerr = 0.1 + 0.1 * np.sqrt(x) +y1err = 0.1 + 0.1 * np.sqrt(x) +y2err = 0.1 + 0.1 * np.sqrt(x/2) # Now switch to a more OO interface to exercise more features. -fig, axs = plt.subplots(nrows=1, ncols=2, sharex=True) -ax = axs[0] -ax.errorbar(x, y, yerr=yerr) -ax.set_title('all errorbars') +fig, (ax_l, ax_c, ax_r) = plt.subplots(nrows=1, ncols=3, + sharex=True, figsize=(12, 6)) -ax = axs[1] -ax.errorbar(x, y, yerr=yerr, errorevery=5) -ax.set_title('only every 5th errorbar') +ax_l.set_title('all errorbars') +ax_l.errorbar(x, y1, yerr=y1err) +ax_l.errorbar(x, y2, yerr=y2err) +ax_c.set_title('only every 6th errorbar') +ax_c.errorbar(x, y1, yerr=y1err, errorevery=6) +ax_c.errorbar(x, y2, yerr=y2err, errorevery=6) -fig.suptitle('Errorbar subsampling for better appearance') +ax_r.set_title('second series shifted by 3') +ax_r.errorbar(x, y1, yerr=y1err, errorevery=(0, 6)) +ax_r.errorbar(x, y2, yerr=y2err, errorevery=(3, 6)) +fig.suptitle('Errorbar subsampling for better appearance') plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c7ad1a61fc8d..8257bb4dcd70 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3143,10 +3143,14 @@ def errorbar(self, x, y, yerr=None, xerr=None, and *yerr*. To use limits with inverted axes, :meth:`set_xlim` or :meth:`set_ylim` must be called before :meth:`errorbar`. - errorevery : positive integer, optional, default: 1 - Subsamples the errorbars. e.g., if errorevery=5, errorbars for - every 5-th datapoint will be plotted. The data plot itself still - shows all data points. + errorevery : int or (int, int), optional, default: 1 + draws error bars on a subset of the data. *errorevery* =N draws + error bars on the points (x[::N], y[::N]). + *errorevery* =(start, N) draws error bars on the points + (x[start::N], y[start::N]). e.g. errorevery=(6, 3) + adds error bars to the data at (x[6], x[9], x[12], x[15], ...). + Used to avoid overlapping error bars when two series share x-axis + values. Returns ------- @@ -3191,9 +3195,17 @@ 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) - if errorevery < 1: + 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 has to be a strictly positive integer') + 'errorevery\'s starting index must be an integer') self._process_unit_info(xdata=x, ydata=y, kwargs=kwargs) @@ -3302,7 +3314,8 @@ def errorbar(self, x, y, yerr=None, xerr=None, xlolims = np.broadcast_to(xlolims, len(x)).astype(bool) xuplims = np.broadcast_to(xuplims, len(x)).astype(bool) - everymask = np.arange(len(x)) % errorevery == 0 + everymask = np.zeros(len(x), bool) + everymask[offset::errorevery] = True def xywhere(xs, ys, mask): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 9f8e2ce60e24..ea45561d6599 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2945,6 +2945,29 @@ def test_errorbar_with_prop_cycle(): ax.errorbar(x=[2, 4, 10], y=[6, 4, 2], yerr=0.5) +@check_figures_equal() +def test_errorbar_offsets(fig_test, fig_ref): + x = np.linspace(0, 1, 15) + y = x * (1-x) + yerr = y/6 + + ax_ref = fig_ref.subplots() + ax_test = fig_test.subplots() + + for color, shift in zip('rgbk', [0, 0, 2, 7]): + y += .02 + + # Using feature in question + ax_test.errorbar(x, y, yerr, errorevery=(shift, 4), + capsize=4, c=color) + + # Using manual errorbars + # n.b. errorbar draws the main plot at z=2.1 by default + ax_ref.plot(x, y, c=color, zorder=2.1) + ax_ref.errorbar(x[shift::4], y[shift::4], yerr[shift::4], + capsize=4, c=color, fmt='none') + + @image_comparison(baseline_images=['hist_stacked_stepfilled', 'hist_stacked_stepfilled']) def test_hist_stacked_stepfilled():