diff --git a/doc/api/api_changes/2017-08-27-TAC.rst b/doc/api/api_changes/2017-08-27-TAC.rst new file mode 100644 index 000000000000..82f709169eb8 --- /dev/null +++ b/doc/api/api_changes/2017-08-27-TAC.rst @@ -0,0 +1,22 @@ +Change to signatures of :meth:`~matplotlib.axes.Axes.bar` & :meth:`~matplotlib.axes.Axes.barh` +---------------------------------------------------------------------------------------------- + +For 2.0 the :ref:`default value of *align* ` changed to +``'center'``. However this caused the signature of +:meth:`~matplotlib.axes.Axes.bar` and +:meth:`~matplotlib.axes.Axes.barh` to be misleading as the first parameters were +still *left* and *bottom* respectively:: + + bar(left, height, *, align='center', **kwargs) + barh(bottom, width, *, align='center', **kwargs) + +despite behaving as the center in both cases. The methods now take ``*args, **kwargs`` +is input and are documented to have the primary signatures of:: + + bar(x, height, *, align='center', **kwargs) + barh(y, width, *, align='center', **kwargs) + +Passing *left* and *bottom* as keyword arguments to +:meth:`~matplotlib.axes.Axes.bar` and +:meth:`~matplotlib.axes.Axes.barh` respectively will warn. +Support will be removed in Matplotlib 3.0. diff --git a/doc/users/dflt_style_changes.rst b/doc/users/dflt_style_changes.rst index 2035b71773a5..06984253e72b 100644 --- a/doc/users/dflt_style_changes.rst +++ b/doc/users/dflt_style_changes.rst @@ -599,6 +599,8 @@ The default value of the ``linecolor`` kwarg for `~matplotlib.Axes.hexbin` has changed from ``'none'`` to ``'face'``. If 'none' is now supplied, no line edges are drawn around the hexagons. +.. _barbarh_align: + ``bar`` and ``barh`` -------------------- diff --git a/examples/pie_and_polar_charts/nested_pie.py b/examples/pie_and_polar_charts/nested_pie.py index 5355bd615611..7dd77a1f531d 100644 --- a/examples/pie_and_polar_charts/nested_pie.py +++ b/examples/pie_and_polar_charts/nested_pie.py @@ -44,17 +44,17 @@ left_middle = np.arange(0.0, 2 * np.pi, 2 * np.pi / 12) left_outer = np.arange(0.0, 2 * np.pi, 2 * np.pi / 9) -ax.bar(left=left_inner, +ax.bar(x=left_inner, width=2 * np.pi / 6, bottom=0, color='C0', linewidth=2, edgecolor='w', height=np.zeros_like(left_inner) + 5) -ax.bar(left=left_middle, +ax.bar(x=left_middle, width=2 * np.pi / 12, bottom=5, color='C1', linewidth=2, edgecolor='w', height=np.zeros_like(left_middle) + 2) -ax.bar(left=left_outer, +ax.bar(x=left_outer, width=2 * np.pi / 9, bottom=7, color='C2', linewidth=2, edgecolor='w', height=np.zeros_like(left_outer) + 3) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 1b8870f3a573..00488a134097 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1463,7 +1463,7 @@ def _replacer(data, key): def _preprocess_data(replace_names=None, replace_all_args=False, - label_namer=None, positional_parameter_names=None): + label_namer=None, positional_parameter_names=None): """ A decorator to add a 'data' kwarg to any a function. The signature of the input function must include the ax argument at the first position :: @@ -1720,7 +1720,7 @@ def inner(ax, *args, **kwargs): if len(replace_names) != 0: _repl = "* All arguments with the following names: '{names}'." if replace_all_args: - _repl += "\n* All positional arguments." + _repl += "\n * All positional arguments." _repl = _repl.format(names="', '".join(sorted(replace_names))) inner.__doc__ = (pre_doc + _DATA_DOC_APPENDIX.format(replaced=_repl)) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 15ba96030a89..402382587ccf 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1868,25 +1868,49 @@ def step(self, x, y, *args, **kwargs): return self.plot(x, y, *args, **kwargs) - @_preprocess_data(replace_names=["left", "height", "width", "bottom", + @_preprocess_data(replace_names=["x", "left", + "height", "width", + "y", "bottom", "color", "edgecolor", "linewidth", "tick_label", "xerr", "yerr", "ecolor"], - label_namer=None) + label_namer=None, + replace_all_args=True + ) @docstring.dedent_interpd - def bar(self, left, height, width=0.8, bottom=None, **kwargs): + def bar(self, *args, **kwargs): """ Make a bar plot. - Make a bar plot with rectangles bounded by: + Call signatures:: + + bar(x, height, *, align='center', **kwargs) + bar(x, height, width, *, align='center', **kwargs) + bar(x, height, width, bottom, *, align='center', **kwargs) + + Make a bar plot with rectangles bounded by + + .. math:: + + (x - width/2, x + width/2, bottom, bottom + height) + + (left, right, bottom and top edges) by default. *x*, + *height*, *width*, and *bottom* can be either scalars or + sequences. + + The *align* and *orientation* kwargs control the interpretation of *x* + and *bottom* - `left`, `left` + `width`, `bottom`, `bottom` + `height` - (left, right, bottom and top edges) + The *align* keyword-only argument controls if *x* is interpreted + as the center or the left edge of the rectangle. Parameters ---------- - left : sequence of scalars - the x coordinates of the left sides of the bars + x : sequence of scalars + the x coordinates of the bars. + + *align* controls if *x* is the bar center (default) or + left edge. height : scalar or sequence of scalars the height(s) of the bars @@ -1899,6 +1923,21 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): the y coordinate(s) of the bars default: None + align : {'center', 'edge'}, optional, default: 'center' + If 'center', interpret the *x* argument as the coordinates + of the centers of the bars. If 'edge', aligns bars by + their left edges + + To align the bars on the right edge pass a negative + *width* and ``align='edge'`` + + Returns + ------- + bars : matplotlib.container.BarContainer + Container with all of the bars + errorbars + + Other Parameters + ---------------- color : scalar or array-like, optional the colors of the bar faces @@ -1935,32 +1974,28 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): dictionary of kwargs to be passed to errorbar method. *ecolor* and *capsize* may be specified here rather than as independent kwargs. - align : {'center', 'edge'}, optional, default: 'center' - If 'edge', aligns bars by their left edges (for vertical bars) and - by their bottom edges (for horizontal bars). If 'center', interpret - the `left` argument as the coordinates of the centers of the bars. - To align on the align bars on the right edge pass a negative - `width`. - - orientation : {'vertical', 'horizontal'}, optional - The orientation of the bars. - log : boolean, optional If true, sets the axis to be log scale. default: False - Returns - ------- - bars : matplotlib.container.BarContainer - Container with all of the bars + errorbars + orientation : {'vertical', 'horizontal'}, optional + + This is for internal use, please do not directly use this, + call `barh` instead. + + The orientation of the bars. + + See also + -------- + barh: Plot a horizontal bar plot. Notes ----- - The optional arguments `color`, `edgecolor`, `linewidth`, - `xerr`, and `yerr` can be either scalars or sequences of + The optional arguments *color*, *edgecolor*, *linewidth*, + *xerr*, and *yerr* can be either scalars or sequences of length equal to the number of bars. This enables you to use bar as the basis for stacked bar charts, or candlestick plots. - Detail: `xerr` and `yerr` are passed directly to + Detail: *xerr* and *yerr* are passed directly to :meth:`errorbar`, so they can also have shape 2xN for independent specification of lower and upper errors. @@ -1968,11 +2003,35 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): %(Rectangle)s - See also - -------- - barh: Plot a horizontal bar plot. """ kwargs = cbook.normalize_kwargs(kwargs, mpatches._patch_alias_map) + # this is using the lambdas to do the arg/kwarg unpacking rather + # than trying to re-implement all of that logic our selves. + matchers = [ + (lambda x, height, width=0.8, bottom=None, **kwargs: + (False, x, height, width, bottom, kwargs)), + (lambda left, height, width=0.8, bottom=None, **kwargs: + (True, left, height, width, bottom, kwargs)), + ] + exps = [] + for matcher in matchers: + try: + dp, x, height, width, y, kwargs = matcher(*args, **kwargs) + except TypeError as e: + # This can only come from a no-match as there is + # no other logic in the matchers. + exps.append(e) + else: + break + else: + raise exps[0] + # if we matched the second-case, then the user passed in + # left=val as a kwarg which we want to deprecate + if dp: + warnings.warn( + "The *left* kwarg to `bar` is deprecated use *x* instead. " + "Support for *left* will be removed in Matplotlib 3.0", + mplDeprecation, stacklevel=2) if not self._hold: self.cla() color = kwargs.pop('color', None) @@ -2002,38 +2061,39 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): label = kwargs.pop('label', '') tick_labels = kwargs.pop('tick_label', None) - _left = left - _bottom = bottom - left, height, width, bottom = np.broadcast_arrays( - # Make args iterable too. - np.atleast_1d(left), height, width, bottom) - adjust_ylim = False adjust_xlim = False + if orientation == 'vertical': - self._process_unit_info(xdata=left, ydata=height, kwargs=kwargs) - if log: - self.set_yscale('log', nonposy='clip') - # size width and bottom according to length of left - if _bottom is None: + if y is None: if self.get_yscale() == 'log': adjust_ylim = True - bottom = np.zeros_like(bottom) + y = 0 + + elif orientation == 'horizontal': + if x is None: + if self.get_xscale() == 'log': + adjust_xlim = True + x = 0 + + x, height, width, y, linewidth = np.broadcast_arrays( + # Make args iterable too. + np.atleast_1d(x), height, width, y, linewidth) + + if orientation == 'vertical': + self._process_unit_info(xdata=x, ydata=height, kwargs=kwargs) + if log: + self.set_yscale('log', nonposy='clip') tick_label_axis = self.xaxis - tick_label_position = left + tick_label_position = x elif orientation == 'horizontal': - self._process_unit_info(xdata=width, ydata=bottom, kwargs=kwargs) + self._process_unit_info(xdata=width, ydata=y, kwargs=kwargs) if log: self.set_xscale('log', nonposx='clip') - # size left and height according to length of bottom - if _left is None: - if self.get_xscale() == 'log': - adjust_xlim = True - left = np.zeros_like(left) tick_label_axis = self.yaxis - tick_label_position = bottom + tick_label_position = y else: raise ValueError('invalid orientation: %s' % orientation) @@ -2051,24 +2111,30 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): # lets do some conversions now since some types cannot be # subtracted uniformly if self.xaxis is not None: - left = self.convert_xunits(left) + x = self.convert_xunits(x) width = self.convert_xunits(width) if xerr is not None: xerr = self.convert_xunits(xerr) if self.yaxis is not None: - bottom = self.convert_yunits(bottom) + y = self.convert_yunits(y) height = self.convert_yunits(height) if yerr is not None: yerr = self.convert_yunits(yerr) + # We will now resolve the alignment and really have + # left, bottom, width, height vectors if align == 'center': if orientation == 'vertical': - left = left - width / 2 + left = x - width / 2 + bottom = y elif orientation == 'horizontal': - bottom = bottom - height / 2 - - elif align != 'edge': + bottom = y - height / 2 + left = x + elif align == 'edge': + left = x + bottom = y + else: raise ValueError('invalid alignment: %s' % align) patches = [] @@ -2096,18 +2162,17 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): if xerr is not None or yerr is not None: if orientation == 'vertical': # using list comps rather than arrays to preserve unit info - x = [l + 0.5 * w for l, w in zip(left, width)] - y = [b + h for b, h in zip(bottom, height)] + ex = [l + 0.5 * w for l, w in zip(left, width)] + ey = [b + h for b, h in zip(bottom, height)] elif orientation == 'horizontal': # using list comps rather than arrays to preserve unit info - x = [l + w for l, w in zip(left, width)] - y = [b + 0.5 * h for b, h in zip(bottom, height)] + ex = [l + w for l, w in zip(left, width)] + ey = [b + 0.5 * h for b, h in zip(bottom, height)] - if "label" not in error_kw: - error_kw["label"] = '_nolegend_' + error_kw.setdefault("label", '_nolegend_') - errorbar = self.errorbar(x, y, + errorbar = self.errorbar(ex, ey, yerr=yerr, xerr=xerr, fmt='none', **error_kw) else: @@ -2143,23 +2208,37 @@ def bar(self, left, height, width=0.8, bottom=None, **kwargs): return bar_container @docstring.dedent_interpd - def barh(self, bottom, width, height=0.8, left=None, **kwargs): + def barh(self, *args, **kwargs): """ Make a horizontal bar plot. - Make a horizontal bar plot with rectangles bounded by: + Call signatures:: - `left`, `left` + `width`, `bottom`, `bottom` + `height` - (left, right, bottom and top edges) + bar(y, width, *, align='center', **kwargs) + bar(y, width, height, *, align='center', **kwargs) + bar(y, width, height, left, *, align='center', **kwargs) + + Make a horizontal bar plot with rectangles by default bounded by + + .. math:: + + (left, left + width, y - height/2, y + height/2) + + (left, right, bottom and top edges) by default. *y*, *width*, + *height*, and *left* can be either scalars or sequences. + + The *align* keyword-only argument controls if *y* is interpreted + as the center or the bottom edge of the rectangle. - `bottom`, `width`, `height`, and `left` can be either scalars - or sequences Parameters ---------- - bottom : scalar or array-like + y : scalar or array-like the y coordinate(s) of the bars + *align* controls if *y* is the bar center (default) + or bottom edge. + width : scalar or array-like the width(s) of the bars @@ -2169,6 +2248,14 @@ def barh(self, bottom, width, height=0.8, left=None, **kwargs): left : sequence of scalars the x coordinates of the left sides of the bars + align : {'center', 'edge'}, optional, default: 'center' + If 'center', interpret the *y* argument as the coordinates + of the centers of the bars. If 'edge', aligns bars by + their bottom edges + + To align the bars on the top edge pass a negative + *height* and ``align='edge'`` + Returns ------- `matplotlib.patches.Rectangle` instances. @@ -2206,23 +2293,20 @@ def barh(self, bottom, width, height=0.8, left=None, **kwargs): dictionary of kwargs to be passed to errorbar method. `ecolor` and `capsize` may be specified here rather than as independent kwargs. - align : {'center', 'edge'}, optional, default: 'center' - If 'edge', aligns bars by their left edges (for vertical - bars) and by their bottom edges (for horizontal bars). If - 'center', interpret the `bottom` argument as the - coordinates of the centers of the bars. To align on the - align bars on the top edge pass a negative 'height'. - log : boolean, optional, default: False If true, sets the axis to be log scale + See also + -------- + bar: Plot a vertical bar plot. + Notes ----- - The optional arguments `color`, `edgecolor`, `linewidth`, - `xerr`, and `yerr` can be either scalars or sequences of + The optional arguments *color*, *edgecolor*, *linewidth*, + *xerr*, and *yerr* can be either scalars or sequences of length equal to the number of bars. This enables you to use bar as the basis for stacked bar charts, or candlestick plots. - Detail: `xerr` and `yerr` are passed directly to + Detail: *xerr* and *yerr* are passed directly to :meth:`errorbar`, so they can also have shape 2xN for independent specification of lower and upper errors. @@ -2230,13 +2314,36 @@ def barh(self, bottom, width, height=0.8, left=None, **kwargs): %(Rectangle)s - See also - -------- - bar: Plot a vertical bar plot. """ - - patches = self.bar(left=left, height=height, width=width, - bottom=bottom, orientation='horizontal', **kwargs) + # this is using the lambdas to do the arg/kwarg unpacking rather + # than trying to re-implement all of that logic our selves. + matchers = [ + (lambda y, width, height=0.8, left=None, **kwargs: + (False, y, width, height, left, kwargs)), + (lambda bottom, width, height=0.8, left=None, **kwargs: + (True, bottom, width, height, left, kwargs)), + ] + excs = [] + for matcher in matchers: + try: + dp, y, width, height, left, kwargs = matcher(*args, **kwargs) + except TypeError as e: + # This can only come from a no-match as there is + # no other logic in the matchers. + excs.append(e) + else: + break + else: + raise excs[0] + + if dp: + warnings.warn( + "The *bottom* kwarg to `barh` is deprecated use *y* instead. " + "Support for *bottom* will be removed in Matplotlib 3.0", + mplDeprecation, stacklevel=2) + kwargs.setdefault('orientation', 'horizontal') + patches = self.bar(x=left, height=height, width=width, + bottom=y, **kwargs) return patches @_preprocess_data(label_namer=None) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 85b5ea47b657..7b46dc3179d4 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2622,20 +2622,19 @@ def axvspan(xmin, xmax, ymin=0, ymax=1, hold=None, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.bar) -def bar(left, height, width=0.8, bottom=None, hold=None, data=None, **kwargs): +def bar(*args, **kwargs): ax = gca() # Deprecated: allow callers to override the hold state # by passing hold=True|False washold = ax._hold - + hold = kwargs.pop('hold', None) if hold is not None: ax._hold = hold from matplotlib.cbook import mplDeprecation warnings.warn("The 'hold' keyword argument is deprecated since 2.0.", mplDeprecation) try: - ret = ax.bar(left, height, width=width, bottom=bottom, data=data, - **kwargs) + ret = ax.bar(*args, **kwargs) finally: ax._hold = washold @@ -2644,19 +2643,19 @@ def bar(left, height, width=0.8, bottom=None, hold=None, data=None, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.barh) -def barh(bottom, width, height=0.8, left=None, hold=None, **kwargs): +def barh(*args, **kwargs): ax = gca() # Deprecated: allow callers to override the hold state # by passing hold=True|False washold = ax._hold - + hold = kwargs.pop('hold', None) if hold is not None: ax._hold = hold from matplotlib.cbook import mplDeprecation warnings.warn("The 'hold' keyword argument is deprecated since 2.0.", mplDeprecation) try: - ret = ax.barh(bottom, width, height=height, left=left, **kwargs) + ret = ax.barh(*args, **kwargs) finally: ax._hold = washold @@ -2993,10 +2992,10 @@ def hexbin(x, y, C=None, gridsize=100, bins=None, xscale='linear', # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.hist) -def hist(x, bins=None, range=None, normed=False, weights=None, cumulative=False, +def hist(x, bins=None, range=None, density=None, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, - hold=None, data=None, **kwargs): + normed=None, hold=None, data=None, **kwargs): ax = gca() # Deprecated: allow callers to override the hold state # by passing hold=True|False @@ -3008,11 +3007,11 @@ def hist(x, bins=None, range=None, normed=False, weights=None, cumulative=False, warnings.warn("The 'hold' keyword argument is deprecated since 2.0.", mplDeprecation) try: - ret = ax.hist(x, bins=bins, range=range, normed=normed, + ret = ax.hist(x, bins=bins, range=range, density=density, weights=weights, cumulative=cumulative, bottom=bottom, histtype=histtype, align=align, orientation=orientation, rwidth=rwidth, log=log, color=color, label=label, - stacked=stacked, data=data, **kwargs) + stacked=stacked, normed=normed, data=data, **kwargs) finally: ax._hold = washold diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 972df062075e..aca4572b2e5b 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -43,8 +43,8 @@ def _determinism_save(objects='mhi', format="pdf", usetex=False): if 'h' in objects: # also use different hatch patterns ax2 = fig.add_subplot(1, 6, 2) - bars = ax2.bar(range(1, 5), range(1, 5)) + \ - ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)) + bars = (ax2.bar(range(1, 5), range(1, 5)) + + ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5))) ax2.set_xticks([1.5, 2.5, 3.5, 4.5]) patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.') diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 7a0e8f326fc2..1d0aeee0a8e5 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5266,6 +5266,38 @@ def test_twinx_knows_limits(): assert((xtwin.viewLim.intervalx == ax2.viewLim.intervalx).all()) +@pytest.mark.style('mpl20') +@pytest.mark.parametrize('args, kwargs, warning_count', + [((1, 1), {'width': 1, 'bottom': 1}, 0), + ((1, ), {'height': 1, 'bottom': 1}, 0), + ((), {'x': 1, 'height': 1}, 0), + ((), {'left': 1, 'height': 1}, 1)]) +def test_bar_signature(args, kwargs, warning_count): + fig, ax = plt.subplots() + with warnings.catch_warnings(record=True) as w: + r, = ax.bar(*args, **kwargs) + + assert r.get_width() == kwargs.get('width', 0.8) + assert r.get_y() == kwargs.get('bottom', 0) + assert len(w) == warning_count + + +@pytest.mark.style('mpl20') +@pytest.mark.parametrize('args, kwargs, warning_count', + [((1, 1), {'height': 1, 'left': 1}, 0), + ((1, ), {'width': 1, 'left': 1}, 0), + ((), {'y': 1, 'width': 1}, 0), + ((), {'bottom': 1, 'width': 1}, 1)]) +def test_barh_signature(args, kwargs, warning_count): + fig, ax = plt.subplots() + with warnings.catch_warnings(record=True) as w: + r, = ax.barh(*args, **kwargs) + + assert r.get_height() == kwargs.get('height', 0.8) + assert r.get_x() == kwargs.get('left', 0) + assert len(w) == warning_count + + def test_zero_linewidth(): # Check that setting a zero linewidth doesn't error plt.plot([0, 1], [0, 1], ls='--', lw=0) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index de3b30cb5966..6d8d8f0f73ee 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -31,7 +31,7 @@ def test_simple(): # pickle.dump(ax, BytesIO(), pickle.HIGHEST_PROTOCOL) plt.figure() - plt.bar(left=np.arange(10), height=np.arange(10)) + plt.bar(x=np.arange(10), height=np.arange(10)) pickle.dump(plt.gca(), BytesIO(), pickle.HIGHEST_PROTOCOL) fig = plt.figure()