diff --git a/doc/api/next_api_changes/behavior/17930-ES.rst b/doc/api/next_api_changes/behavior/17930-ES.rst new file mode 100644 index 000000000000..c42b95b6f52f --- /dev/null +++ b/doc/api/next_api_changes/behavior/17930-ES.rst @@ -0,0 +1,8 @@ +``Axes.errorbar`` cycles non-color properties correctly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Formerly, `.Axes.errorbar` incorrectly skipped the Axes property cycle if a +color was explicitly specified, even if the property cycler was for other +properties (such as line style). Now, `.Axes.errorbar` will advance the Axes +property cycle as done for `.Axes.plot`, i.e., as long as all properties in the +cycler are not explicitly passed. diff --git a/doc/users/next_whats_new/2020-07-15-errorbar-cycling.rst b/doc/users/next_whats_new/2020-07-15-errorbar-cycling.rst new file mode 100644 index 000000000000..23d3e327da0a --- /dev/null +++ b/doc/users/next_whats_new/2020-07-15-errorbar-cycling.rst @@ -0,0 +1,23 @@ +``Axes.errorbar`` cycles non-color properties correctly +------------------------------------------------------- + +Formerly, `.Axes.errorbar` incorrectly skipped the Axes property cycle if a +color was explicitly specified, even if the property cycler was for other +properties (such as line style). Now, `.Axes.errorbar` will advance the Axes +property cycle as done for `.Axes.plot`, i.e., as long as all properties in the +cycler are not explicitly passed. + +For example, the following will cycle through the line styles: + +.. plot:: + :include-source: True + + x = np.arange(0.1, 4, 0.5) + y = np.exp(-x) + offsets = [0, 1] + + plt.rcParams['axes.prop_cycle'] = plt.cycler('linestyle', ['-', '--']) + + fig, ax = plt.subplots() + for offset in offsets: + ax.errorbar(x, y + offset, xerr=0.1, yerr=0.3, fmt='tab:blue') diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index af3f5f738995..dfe7db4ea318 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3315,35 +3315,8 @@ def errorbar(self, x, y, yerr=None, xerr=None, self._process_unit_info(xdata=x, ydata=y, kwargs=kwargs) - plot_line = (fmt.lower() != 'none') - label = kwargs.pop("label", None) - - if fmt == '': - fmt_style_kwargs = {} - else: - fmt_style_kwargs = {k: v for k, v in - zip(('linestyle', 'marker', 'color'), - _process_plot_format(fmt)) - if v is not None} - if fmt == 'none': - # Remove alpha=0 color that _process_plot_format returns - fmt_style_kwargs.pop('color') - - if ('color' in kwargs or 'color' in fmt_style_kwargs): - base_style = {} - if 'color' in kwargs: - base_style['color'] = kwargs.pop('color') - else: - base_style = next(self._get_lines.prop_cycler) - - base_style['label'] = '_nolegend_' - base_style.update(fmt_style_kwargs) - if 'color' not in base_style: - base_style['color'] = 'C0' - if ecolor is None: - ecolor = base_style['color'] - # make sure all the args are iterable; use lists not arrays to - # preserve units + # Make sure all the args are iterable; use lists not arrays to preserve + # units. if not np.iterable(x): x = [x] @@ -3361,19 +3334,50 @@ def errorbar(self, x, y, yerr=None, xerr=None, if not np.iterable(yerr): yerr = [yerr] * len(y) - # make the style dict for the 'normal' plot line - plot_line_style = { - **base_style, - **kwargs, - 'zorder': (kwargs['zorder'] - .1 if barsabove else - kwargs['zorder'] + .1), - } + label = kwargs.pop("label", None) + kwargs['label'] = '_nolegend_' + + # Create the main line and determine overall kwargs for child artists. + # We avoid calling self.plot() directly, or self._get_lines(), because + # that would call self._process_unit_info again, and do other indirect + # data processing. + (data_line, base_style), = self._get_lines._plot_args( + (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) + + # Do this after creating `data_line` to avoid modifying `base_style`. + if barsabove: + data_line.set_zorder(kwargs['zorder'] - .1) + else: + data_line.set_zorder(kwargs['zorder'] + .1) + + # Add line to plot, or throw it away and use it to determine kwargs. + if fmt.lower() != 'none': + self.add_line(data_line) + else: + data_line = None + # Remove alpha=0 color that _get_lines._plot_args returns for + # 'none' format, and replace it with user-specified color, if + # supplied. + base_style.pop('color') + if 'color' in kwargs: + base_style['color'] = kwargs.pop('color') + + if 'color' not in base_style: + base_style['color'] = 'C0' + if ecolor is None: + ecolor = base_style['color'] - # make the style dict for the line collections (the bars) - eb_lines_style = dict(base_style) - eb_lines_style.pop('marker', None) - eb_lines_style.pop('linestyle', None) - eb_lines_style['color'] = ecolor + # Eject any marker information from line format string, as it's not + # needed for bars or caps. + base_style.pop('marker', None) + base_style.pop('markersize', None) + base_style.pop('markerfacecolor', None) + base_style.pop('markeredgewidth', None) + base_style.pop('markeredgecolor', None) + base_style.pop('linestyle', None) + + # Make the style dict for the line collections (the bars). + eb_lines_style = {**base_style, 'color': ecolor} if elinewidth: eb_lines_style['linewidth'] = elinewidth @@ -3384,15 +3388,8 @@ def errorbar(self, x, y, yerr=None, xerr=None, if key in kwargs: eb_lines_style[key] = kwargs[key] - # set up cap style dictionary - eb_cap_style = dict(base_style) - # eject any marker information from format string - eb_cap_style.pop('marker', None) - eb_lines_style.pop('markerfacecolor', None) - eb_lines_style.pop('markeredgewidth', None) - eb_lines_style.pop('markeredgecolor', None) - eb_cap_style.pop('ls', None) - eb_cap_style['linestyle'] = 'none' + # Make the style dict for the caps. + eb_cap_style = {**base_style, 'linestyle': 'none'} if capsize is None: capsize = rcParams["errorbar.capsize"] if capsize > 0: @@ -3408,11 +3405,6 @@ def errorbar(self, x, y, yerr=None, xerr=None, eb_cap_style[key] = kwargs[key] eb_cap_style['color'] = ecolor - data_line = None - if plot_line: - data_line = mlines.Line2D(x, y, **plot_line_style) - self.add_line(data_line) - barcols = [] caplines = [] diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 282517cba817..cd4ff5edd27b 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -310,7 +310,7 @@ def _makeline(self, x, y, kw, kwargs): default_dict = self._getdefaults(set(), kw) self._setdefaults(default_dict, kw) seg = mlines.Line2D(x, y, **kw) - return seg + return seg, kw def _makefill(self, x, y, kw, kwargs): # Polygon doesn't directly support unitized inputs. @@ -362,9 +362,9 @@ def _makefill(self, x, y, kw, kwargs): fill=kwargs.get('fill', True), closed=kw['closed']) seg.set(**kwargs) - return seg + return seg, kwargs - def _plot_args(self, tup, kwargs): + def _plot_args(self, tup, kwargs, return_kwargs=False): if len(tup) > 1 and isinstance(tup[-1], str): linestyle, marker, color = _process_plot_format(tup[-1]) tup = tup[:-1] @@ -415,8 +415,12 @@ def _plot_args(self, tup, kwargs): ncx, ncy = x.shape[1], y.shape[1] if ncx > 1 and ncy > 1 and ncx != ncy: raise ValueError(f"x has {ncx} columns but y has {ncy} columns") - return [func(x[:, j % ncx], y[:, j % ncy], kw, kwargs) - for j in range(max(ncx, ncy))] + result = (func(x[:, j % ncx], y[:, j % ncy], kw, kwargs) + for j in range(max(ncx, ncy))) + if return_kwargs: + return list(result) + else: + return [l[0] for l in result] @cbook._define_aliases({"facecolor": ["fc"]}) diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_with_prop_cycle.png b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_with_prop_cycle.png deleted file mode 100644 index c6852c150ebe..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_with_prop_cycle.png and /dev/null differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 45fdcf5d6182..898539929f12 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3147,14 +3147,25 @@ def test_errobar_nonefmt(): assert np.all(errbar.get_color() == mcolors.to_rgba('C0')) -@image_comparison(['errorbar_with_prop_cycle.png'], - style='mpl20', remove_text=True) -def test_errorbar_with_prop_cycle(): - _cycle = cycler(ls=['--', ':'], marker=['s', 's'], mfc=['k', 'w']) +@check_figures_equal(extensions=['png']) +def test_errorbar_with_prop_cycle(fig_test, fig_ref): + ax = fig_ref.subplots() + ax.errorbar(x=[2, 4, 10], y=[0, 1, 2], yerr=0.5, + ls='--', marker='s', mfc='k') + ax.errorbar(x=[2, 4, 10], y=[2, 3, 4], yerr=0.5, color='tab:green', + ls=':', marker='s', mfc='y') + ax.errorbar(x=[2, 4, 10], y=[4, 5, 6], yerr=0.5, fmt='tab:blue', + ls='-.', marker='o', mfc='c') + ax.set_xlim(1, 11) + + _cycle = cycler(ls=['--', ':', '-.'], marker=['s', 's', 'o'], + mfc=['k', 'y', 'c'], color=['b', 'g', 'r']) plt.rc("axes", prop_cycle=_cycle) - fig, ax = plt.subplots() - ax.errorbar(x=[2, 4, 10], y=[3, 2, 4], yerr=0.5) - ax.errorbar(x=[2, 4, 10], y=[6, 4, 2], yerr=0.5) + ax = fig_test.subplots() + ax.errorbar(x=[2, 4, 10], y=[0, 1, 2], yerr=0.5) + ax.errorbar(x=[2, 4, 10], y=[2, 3, 4], yerr=0.5, color='tab:green') + ax.errorbar(x=[2, 4, 10], y=[4, 5, 6], yerr=0.5, fmt='tab:blue') + ax.set_xlim(1, 11) @check_figures_equal()