From 235674fb773446fbc1aa6d1f38d6d582c8185de3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 21 Jan 2019 20:21:13 +0100 Subject: [PATCH] Interpret unitless bar widths for unitized data as relative to spacing. --- lib/matplotlib/axes/_axes.py | 133 ++++++++++++++++------------------- 1 file changed, 60 insertions(+), 73 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 70472e05758a..76982833092d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -32,6 +32,7 @@ import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms import matplotlib.tri as mtri +import matplotlib.units as munits from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.axes._base import _AxesBase, _process_plot_format @@ -2012,56 +2013,33 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): return self.plot(x, y, *args, data=data, **kwargs) @staticmethod - def _convert_dx(dx, x0, xconv, convert): + def _convert_dz(axis, z, z_conv, dz): """ - Small helper to do logic of width conversion flexibly. - - *dx* and *x0* have units, but *xconv* has already been converted - to unitless (and is an ndarray). This allows the *dx* to have units - that are different from *x0*, but are still accepted by the - ``__add__`` operator of *x0*. + Convert potentially unitized *dz* into unitless values using the + *axis*' converter and *z*/*z_conv* as values (respectively unitized and + unitless) over which *dz* apply. """ - - # x should be an array... - assert type(xconv) is np.ndarray - - if xconv.size == 0: - # xconv has already been converted, but maybe empty... - return convert(dx) - - try: - # attempt to add the width to x0; this works for - # datetime+timedelta, for instance - - # only use the first element of x and x0. This saves - # having to be sure addition works across the whole - # vector. This is particularly an issue if - # x0 and dx are lists so x0 + dx just concatenates the lists. - # We can't just cast x0 and dx to numpy arrays because that - # removes the units from unit packages like `pint` that - # wrap numpy arrays. - try: - x0 = x0[0] - except (TypeError, IndexError, KeyError): - x0 = x0 - - try: - x = xconv[0] - except (TypeError, IndexError, KeyError): - x = xconv - - delist = False - if not np.iterable(dx): - dx = [dx] - delist = True - dx = [convert(x0 + ddx) - x for ddx in dx] - if delist: - dx = dx[0] - except (ValueError, TypeError, AttributeError): - # if the above fails (for any reason) just fallback to what - # we do by default and convert dx by iteslf. - dx = convert(dx) - return dx + scalar_dz = np.ndim(dz) == 0 + if axis.converter is not None: # z was unitized. + if munits.ConversionInterface.is_numlike(dz): # Scalar width. + if np.size(z_conv) == 1: + dz = np.diff(axis.get_view_interval()) * dz + else: + dz = np.array(dz) * np.ptp(z_conv) / (np.size(z_conv) - 1) + else: # Unitized. + # cbook.flatten([]) is similar to atleast_1d here, but returns + # a list and keeps units. + z = [*cbook.flatten([z])] + dz = [*cbook.flatten([dz])] + if len(z) == 1: + z = z * len(dz) + if len(dz) == 1: + dz = dz * len(z) + dz = [axis.convert_units(_z + _dz) - axis.convert_units(_z) + for _z, _dz in zip(z, dz)] + if scalar_dz and np.ndim(dz) > 0: + dz, = dz + return dz @_preprocess_data() @docstring.dedent_interpd @@ -2088,7 +2066,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", The height(s) of the bars. width : scalar or array-like, optional - The width(s) of the bars (default: 0.8). + The width(s) of the bars (default: 0.8, but see note below if *x* + has units). bottom : scalar or array-like, optional The y coordinate(s) of the bars bases (default: 0). @@ -2166,14 +2145,19 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", *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 - :meth:`errorbar`, so they can also have shape 2xN for - independent specification of lower and upper errors. + + If *x* has units, then *width* should normally also be specified with + units (so that they can be meaningfully added). However, if a unitless + *width* is used (in particular, for the default *width*), then it + is interpreted as a fraction of the average inter-bar spacing. For + example, if the bars are equally spaced, then a *width* of 1 makes the + bars contiguous. As a special case, if there is a *single* bar, then + a unitless *width* is interpreted as a fraction of the axis (current) + view span. Other optional kwargs: %(Rectangle)s - """ kwargs = cbook.normalize_kwargs(kwargs, mpatches.Patch._alias_map) color = kwargs.pop('color', None) @@ -2226,18 +2210,16 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", # lets do some conversions now since some types cannot be # subtracted uniformly - if self.xaxis is not None: - x0 = x - x = np.asarray(self.convert_xunits(x)) - width = self._convert_dx(width, x0, x, self.convert_xunits) - if xerr is not None: - xerr = self._convert_dx(xerr, x0, x, self.convert_xunits) - if self.yaxis is not None: - y0 = y - y = np.asarray(self.convert_yunits(y)) - height = self._convert_dx(height, y0, y, self.convert_yunits) - if yerr is not None: - yerr = self._convert_dx(yerr, y0, y, self.convert_yunits) + x0 = x + x = self.convert_xunits(x) + width = self._convert_dz(self.xaxis, x0, x, width) + if xerr is not None: + xerr = self._convert_dz(self.xaxis, x0, x, xerr) + y0 = y + y = self.convert_yunits(y) + height = self._convert_dz(self.yaxis, y0, y, height) + if yerr is not None: + yerr = self._convert_dz(self.yaxis, y0, y, yerr) x, height, width, y, linewidth = np.broadcast_arrays( # Make args iterable too. @@ -2376,8 +2358,9 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", width : scalar or array-like The width(s) of the bars. - height : sequence of scalars, optional, default: 0.8 - The heights of the bars. + height : sequence of scalars, optional + The heights of the bars (default: 0.8, but see note below if *y* + has units). left : sequence of scalars The x coordinates of the left sides of the bars (default: 0). @@ -2452,14 +2435,19 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", *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 - :meth:`errorbar`, so they can also have shape 2xN for - independent specification of lower and upper errors. + + If *y* has units, then *height* should normally also be specified with + units (so that they can be meaningfully added). However, if a unitless + *height* is used (in particular, for the default *height*), then it is + interpreted as a fraction of the average inter-bar spacing. For + example, if the bars are equally spaced, then a *height* of 1 makes the + bars contiguous. As a special case, if there is a *single* bar, then + a unitless *height* is interpreted as a fraction of the axis (current) + view span. Other optional kwargs: %(Rectangle)s - """ kwargs.setdefault('orientation', 'horizontal') patches = self.bar(x=left, height=height, width=width, bottom=y, @@ -2535,9 +2523,8 @@ def broken_barh(self, xranges, yrange, **kwargs): 'with two elements (i.e. an Nx2 array)') # convert the absolute values, not the x and dx... x_conv = np.asarray(self.convert_xunits(xr[0])) - x1 = self._convert_dx(xr[1], xr[0], x_conv, self.convert_xunits) + x1 = self._convert_dz(self.xaxis, xr[0], x_conv, xr[1]) xranges_conv.append((x_conv, x1)) - yrange_conv = self.convert_yunits(yrange) col = mcoll.BrokenBarHCollection(xranges_conv, yrange_conv, **kwargs)