diff --git a/doc/users/next_whats_new/option_between_for_step_fill_between.rst b/doc/users/next_whats_new/option_between_for_step_fill_between.rst new file mode 100644 index 000000000000..f305bc987e92 --- /dev/null +++ b/doc/users/next_whats_new/option_between_for_step_fill_between.rst @@ -0,0 +1,22 @@ +New drawstyles for steps - "steps-between", "steps-edges" +------------------------------------------------------------------------ +They are asymmetrical such that abs(len(x) - len(y)) == 1. Typically +to enable plotting histograms with step() and fill_between(). + + .. plot:: + + import numpy as np + import matplotlib.pyplot as plt + + x = np.arange(0,7,1) + y = np.array([2,3,4,5,4,3]) + + fig, ax = plt.subplots(constrained_layout=True) + + ax.plot(x, y + 2, drawstyle='steps-between') + ax.plot(x, y, drawstyle='steps-edges') + + plt.show() + +See :doc:`/gallery/lines_bars_and_markers/step_demo` and +:doc:`/gallery/lines_bars_and_markers/filled_step` for examples. diff --git a/examples/lines_bars_and_markers/filled_step.py b/examples/lines_bars_and_markers/filled_step.py index a156665b0d49..5a378125cc53 100644 --- a/examples/lines_bars_and_markers/filled_step.py +++ b/examples/lines_bars_and_markers/filled_step.py @@ -1,9 +1,9 @@ """ ========================= -Hatch-filled histograms +Filled histograms ========================= -Hatching capabilities for plotting histograms. +Filled histograms and hatching capabilities for plotting histograms. """ import itertools @@ -15,6 +15,30 @@ import matplotlib.ticker as mticker from cycler import cycler +############################################################################### +# Plain filled steps + +# sphinx_gallery_thumbnail_number = 2 + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), tight_layout=True) +ax1.fill_between([0, 1, 2, 3], [1, 2, 3], step='between') +ax2.fill_betweenx([0, 1, 2, 3], [0, 1, 2], step='between') +ax1.set_ylabel('counts') +ax1.set_xlabel('edges') +ax2.set_xlabel('counts') +ax2.set_ylabel('edges') + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), tight_layout=True) +ax1.fill_between([0, 1, 2, 3], [1, 2, 3], [0, 1, 0], step='between') +ax2.fill_betweenx([0, 1, 2, 3], [1, 2, 3], [0, 1, 0], step='between') +ax1.set_ylabel('counts') +ax1.set_xlabel('edges') +ax2.set_xlabel('counts') +ax2.set_ylabel('edges') + + +############################################################################### +# Hatches def filled_hist(ax, edges, values, bottoms=None, orientation='v', **kwargs): diff --git a/examples/lines_bars_and_markers/step_demo.py b/examples/lines_bars_and_markers/step_demo.py index 46b370eefd4d..43b5d59059f6 100644 --- a/examples/lines_bars_and_markers/step_demo.py +++ b/examples/lines_bars_and_markers/step_demo.py @@ -49,6 +49,20 @@ plt.title('plt.plot(drawstyle=...)') plt.show() +# Plotting with where='between'/'edges' +x = np.arange(0, 7, 1) +y = np.array([2, 3, 4, 5, 4, 3]) + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) +ax1.step(x, y + 2, where='between', label='between') +ax1.step(x, y, where='edges', label='edges') +ax1.legend(title='Parameter where:') + +ax2.step(y + 2, x, where='between', label='between') +ax2.step(y, x, where='edges', label='edges') +ax2.legend(title='Parameter where:') + +plt.show() ############################################################################# # # ------------ diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 01ad682c013a..a5ec52d40556 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2126,7 +2126,9 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): An object with labelled data. If given, provide the label names to plot in *x* and *y*. - where : {'pre', 'post', 'mid'}, default: 'pre' + where : {'pre', 'post', 'mid', 'between', 'edges'}, optional + Default 'pre' + Define where the steps should be placed: - 'pre': The y value is continued constantly to the left from @@ -2136,6 +2138,10 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the value ``y[i]``. - 'mid': Steps occur half-way between the *x* positions. + - 'between': Expects abs(len(x)-len(y)) == 1, steps have value y[i] + on the interval ``[x[i], x[i+1])`` + - 'edges': Expects abs(len(x)-len(y)) == 1, steps have value y[i] + on interval ``[x[i], x[i+1]), shape is closed at x[0], x[-1]`` Returns ------- @@ -2151,7 +2157,17 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): ----- .. [notes section required to get data note injection right] """ - cbook._check_in_list(('pre', 'post', 'mid'), where=where) + cbook._check_in_list(('pre', 'post', 'mid', 'between', 'edges'), + where=where) + + if where in ['between', 'edges']: + if len(x) == len(y) or abs(len(x)-len(y)) > 1: + raise ValueError(f"When plotting with 'between' or 'edges'" + f"input sizes have to be have to satisfy " + f"len(x) + 1 == len(y) or " + f"len(x) == len(y) + 1 but x " + f"and y have size {len(x)} and {len(y)}") + kwargs['drawstyle'] = 'steps-' + where return self.plot(x, y, *args, data=data, **kwargs) @@ -5098,7 +5114,7 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, Setting *interpolate* to *True* will calculate the actual intersection point and extend the filled region up to this point. - step : {'pre', 'post', 'mid'}, optional + step : {'pre', 'post', 'mid', 'between'}, optional Define *step* if the filling should be a step function, i.e. constant in between *x*. The value determines where the step will occur: @@ -5110,6 +5126,8 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the value ``y[i]``. - 'mid': Steps occur half-way between the *x* positions. + - 'between': Expects len(x) = len(y) + 1, steps have value y[i] + on the interval ``[x[i], x[i+1])`` Other Parameters ---------------- @@ -5133,6 +5151,10 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, .. [notes section required to get data note injection right] """ + + cbook._check_in_list((None, 'pre', 'post', 'mid', 'between'), + step=step) + if not rcParams['_internal.classic_mode']: kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection) if not any(c in kwargs for c in ('color', 'facecolor')): @@ -5140,7 +5162,8 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, self._get_patches_for_fill.get_next_color() # Handle united data, such as dates - self._process_unit_info(xdata=x, ydata=y1, kwargs=kwargs) + self._process_unit_info(xdata=x, kwargs=kwargs) + self._process_unit_info(ydata=y1) self._process_unit_info(ydata=y2) # Convert the arrays so we can work with them @@ -5153,79 +5176,39 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, raise ValueError('Input passed into argument "%r"' % name + 'is not 1-dimensional.') + pad_size = x.size + if step == 'between': + pad_size -= 1 + if where is None: - where = True + where = np.ones(pad_size).astype(bool) else: where = np.asarray(where, dtype=bool) - if where.size != x.size: + if where.size != x.size and step != 'between': cbook.warn_deprecated( "3.2", message="The parameter where must have the same size as x " "in fill_between(). This will become an error in " "future versions of Matplotlib.") - where = where & ~functools.reduce(np.logical_or, - map(np.ma.getmask, [x, y1, y2])) - - x, y1, y2 = np.broadcast_arrays(np.atleast_1d(x), y1, y2) - - polys = [] - for ind0, ind1 in cbook.contiguous_regions(where): - xslice = x[ind0:ind1] - y1slice = y1[ind0:ind1] - y2slice = y2[ind0:ind1] - if step is not None: - step_func = cbook.STEP_LOOKUP_MAP["steps-" + step] - xslice, y1slice, y2slice = step_func(xslice, y1slice, y2slice) - - if not len(xslice): - continue - - N = len(xslice) - X = np.zeros((2 * N + 2, 2), float) - - if interpolate: - def get_interp_point(ind): - im1 = max(ind - 1, 0) - x_values = x[im1:ind + 1] - diff_values = y1[im1:ind + 1] - y2[im1:ind + 1] - y1_values = y1[im1:ind + 1] - - if len(diff_values) == 2: - if np.ma.is_masked(diff_values[1]): - return x[im1], y1[im1] - elif np.ma.is_masked(diff_values[0]): - return x[ind], y1[ind] - - diff_order = diff_values.argsort() - diff_root_x = np.interp( - 0, diff_values[diff_order], x_values[diff_order]) - x_order = x_values.argsort() - diff_root_y = np.interp(diff_root_x, x_values[x_order], - y1_values[x_order]) - return diff_root_x, diff_root_y - - start = get_interp_point(ind0) - end = get_interp_point(ind1) - else: - # the purpose of the next two lines is for when y2 is a - # scalar like 0 and we want the fill to go all the way - # down to 0 even if none of the y1 sample points do - start = xslice[0], y2slice[0] - end = xslice[-1], y2slice[-1] - X[0] = start - X[N + 1] = end + # Broadcast scalar values + y1 = np.broadcast_to(y1, pad_size, subok=True) + y2 = np.broadcast_to(y2, pad_size, subok=True) + where = np.broadcast_to(where, pad_size, subok=True) - X[1:N + 1, 0] = xslice - X[1:N + 1, 1] = y1slice - X[N + 2:, 0] = xslice[::-1] - X[N + 2:, 1] = y2slice[::-1] - - polys.append(X) + _get_masks = list(map(np.ma.getmask, [y1, y2])) + where = where & ~functools.reduce(np.logical_or, _get_masks) + polys = cbook._get_fillbetween_polys(x, y1, y2, where, step=step, + interpolate=interpolate, dir='y') collection = mcoll.PolyCollection(polys, **kwargs) # now update the datalim and autoscale + # For between pad last value + if step == 'between': + y1 = np.append(y1, y1[-1]) + y2 = np.append(y2, y2[-1]) + where = np.append(where, True) XY1 = np.array([x[where], y1[where]]).T XY2 = np.array([x[where], y2[where]]).T self.dataLim.update_from_data_xy(XY1, self.ignore_existing_data_limits, @@ -5286,7 +5269,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, Setting *interpolate* to *True* will calculate the actual intersection point and extend the filled region up to this point. - step : {'pre', 'post', 'mid'}, optional + step : {'pre', 'post', 'mid', 'between'}, optional Define *step* if the filling should be a step function, i.e. constant in between *y*. The value determines where the step will occur: @@ -5298,6 +5281,8 @@ def fill_betweenx(self, y, x1, x2=0, where=None, every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the value ``y[i]``. - 'mid': Steps occur half-way between the *x* positions. + - 'between': Expects len(y) = len(x) + 1, steps have value x[i] + on the interval ``[y[i], y[i+1])`` Other Parameters ---------------- @@ -5321,6 +5306,10 @@ def fill_betweenx(self, y, x1, x2=0, where=None, .. [notes section required to get data note injection right] """ + + cbook._check_in_list((None, 'pre', 'post', 'mid', 'between'), + step=step) + if not rcParams['_internal.classic_mode']: kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection) if not any(c in kwargs for c in ('color', 'facecolor')): @@ -5328,7 +5317,8 @@ def fill_betweenx(self, y, x1, x2=0, where=None, self._get_patches_for_fill.get_next_color() # Handle united data, such as dates - self._process_unit_info(ydata=y, xdata=x1, kwargs=kwargs) + self._process_unit_info(ydata=y, kwargs=kwargs) + self._process_unit_info(xdata=x1) self._process_unit_info(xdata=x2) # Convert the arrays so we can work with them @@ -5341,78 +5331,39 @@ def fill_betweenx(self, y, x1, x2=0, where=None, raise ValueError('Input passed into argument "%r"' % name + 'is not 1-dimensional.') + pad_size = y.size + if step == 'between': + pad_size -= 1 + if where is None: - where = True + where = np.ones(pad_size).astype(bool) else: where = np.asarray(where, dtype=bool) - if where.size != y.size: + if where.size != y.size and step != 'between': cbook.warn_deprecated( "3.2", message="The parameter where must have the same size as y " "in fill_between(). This will become an error in " "future versions of Matplotlib.") - where = where & ~functools.reduce(np.logical_or, - map(np.ma.getmask, [y, x1, x2])) - - y, x1, x2 = np.broadcast_arrays(np.atleast_1d(y), x1, x2) - - polys = [] - for ind0, ind1 in cbook.contiguous_regions(where): - yslice = y[ind0:ind1] - x1slice = x1[ind0:ind1] - x2slice = x2[ind0:ind1] - if step is not None: - step_func = cbook.STEP_LOOKUP_MAP["steps-" + step] - yslice, x1slice, x2slice = step_func(yslice, x1slice, x2slice) - - if not len(yslice): - continue - - N = len(yslice) - Y = np.zeros((2 * N + 2, 2), float) - if interpolate: - def get_interp_point(ind): - im1 = max(ind - 1, 0) - y_values = y[im1:ind + 1] - diff_values = x1[im1:ind + 1] - x2[im1:ind + 1] - x1_values = x1[im1:ind + 1] - - if len(diff_values) == 2: - if np.ma.is_masked(diff_values[1]): - return x1[im1], y[im1] - elif np.ma.is_masked(diff_values[0]): - return x1[ind], y[ind] - - diff_order = diff_values.argsort() - diff_root_y = np.interp( - 0, diff_values[diff_order], y_values[diff_order]) - y_order = y_values.argsort() - diff_root_x = np.interp(diff_root_y, y_values[y_order], - x1_values[y_order]) - return diff_root_x, diff_root_y - - start = get_interp_point(ind0) - end = get_interp_point(ind1) - else: - # the purpose of the next two lines is for when x2 is a - # scalar like 0 and we want the fill to go all the way - # down to 0 even if none of the x1 sample points do - start = x2slice[0], yslice[0] - end = x2slice[-1], yslice[-1] - - Y[0] = start - Y[N + 1] = end - Y[1:N + 1, 0] = x1slice - Y[1:N + 1, 1] = yslice - Y[N + 2:, 0] = x2slice[::-1] - Y[N + 2:, 1] = yslice[::-1] + # Broadcast scalar values + x1 = np.broadcast_to(x1, pad_size, subok=True) + x2 = np.broadcast_to(x2, pad_size, subok=True) + where = np.broadcast_to(where, pad_size, subok=True) - polys.append(Y) + _get_masks = list(map(np.ma.getmask, [x1, x2])) + where = where & ~functools.reduce(np.logical_or, _get_masks) + polys = cbook._get_fillbetween_polys(y, x1, x2, where, step=step, + interpolate=interpolate, dir='x') collection = mcoll.PolyCollection(polys, **kwargs) # now update the datalim and autoscale + # For between pad last value + if step == 'between': + x1 = np.append(x1, x1[-1]) + x2 = np.append(x2, x2[-1]) + where = np.append(where, True) X1Y = np.array([x1[where], y[where]]).T X2Y = np.array([x2[where], y[where]]).T self.dataLim.update_from_data_xy(X1Y, self.ignore_existing_data_limits, diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index db47c347b9bf..0940590a1505 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -337,9 +337,10 @@ def _plot_args(self, tup, kwargs): if self.axes.yaxis is not None: self.axes.yaxis.update_units(y) - if x.shape[0] != y.shape[0]: - raise ValueError(f"x and y must have same first dimension, but " - f"have shapes {x.shape} and {y.shape}") + if not kwargs.get('drawstyle') in ['steps-between', 'steps-edges']: + if x.shape[0] != y.shape[0]: + raise ValueError(f"x and y must have same first dimension, but" + f" have shapes {x.shape} and {y.shape}") if x.ndim > 2 or y.ndim > 2: raise ValueError(f"x and y can be no greater than 2-D, but have " f"shapes {x.shape} and {y.shape}") diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 5c9a419ed353..534a189ed653 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1533,6 +1533,43 @@ def violin_stats(X, method, points=100, quantiles=None): return vpstats +def _pad_arrays(*v, padval=np.nan): + """ + Pad list of arrays of varying lengths to the same size with specified + value. Useful for padding asymetrical arrays + + Parameters + ---------- + v : iterable + List of arrays to be padded to the largest len. All elements must + support iteration + + padval : scalar, bool or NaN, defaul NaN + value to pad missing values with + + Returns + ------- + out : array + Array of input arrays padded to the same len by specified padval + + Examples + -------- + >>> a, b, c = _pad_arrays(np.array([1,2,3,4]), np.array([1,2,3]), + np.array([1])) + >>> a + np.array([1, 2, 3, 4]) + >>> b + np.array([1, 2, 3, np.nan] + >>> c + np.array([1, np.nan, np.nan, np.nan]) + """ + + if len(set([k.shape[0] for k in v])) == 1: + return v + padded_v = list(itertools.zip_longest(*v, fillvalue=padval)) + return np.array(padded_v, dtype=float).T + + def pts_to_prestep(x, *args): """ Convert continuous line to pre-steps. @@ -1571,6 +1608,219 @@ def pts_to_prestep(x, *args): return steps +def _get_fillbetween_polys(x, y1, y2, where, step=None, + interpolate=False, dir='x'): + """ + A helper function for fill_between and fill_betweenx. + Converts x, y1, y2 or y, x1, x2 arrays into vertices to fill + polycollection. + + Parameters + ---------- + x (y): array + Base array for fill_between (x-like) + + y1 (x1): array + Upper edge boundary for fill_between plot (y1-like) + + y2 (x2): array + Lower edge boundary for fill_between plot (y2-like) + + where: array + Bool array. See `~.axes.Axes.fill_between` + + step : {'pre', 'post', 'mid', 'between'}, optional + See `~.axes.Axes.fill_between` + + interpolate : bool, default: False + See `~.axes.Axes.fill_between` + + dir : {'x', 'y}, optional, default: 'x' + Return vertex collection for `~.axes.Axes.fill_between` or + `~.axes.Axes.fill_betweenx` + + Returns + ------- + out : array + ``Nx2`` array of verstices + """ + polys = [] + for ind0, ind1 in contiguous_regions(where): + if step == 'between': + xslice = x[ind0:ind1+1] + else: + xslice = x[ind0:ind1] + y1slice = y1[ind0:ind1] + y2slice = y2[ind0:ind1] + if step is not None: + step_func = STEP_LOOKUP_MAP["steps-" + step] + xslice, y1slice, y2slice = step_func(xslice, y1slice, y2slice) + + if not len(xslice): + continue + + N = len(xslice) + X = np.zeros((2 * N + 2, 2), float) + + if interpolate: + def get_interp_point(ind): + im1 = max(ind - 1, 0) + x_values = x[im1:ind + 1] + diff_values = y1[im1:ind + 1] - y2[im1:ind + 1] + y1_values = y1[im1:ind + 1] + + if len(diff_values) == 2: + if np.ma.is_masked(diff_values[1]): + return x[im1], y1[im1] + elif np.ma.is_masked(diff_values[0]): + return x[ind], y1[ind] + + diff_order = diff_values.argsort() + diff_root_x = np.interp( + 0, diff_values[diff_order], x_values[diff_order]) + x_order = x_values.argsort() + diff_root_y = np.interp(diff_root_x, x_values[x_order], + y1_values[x_order]) + return diff_root_x, diff_root_y + + start = get_interp_point(ind0) + end = get_interp_point(ind1) + else: + # the purpose of the next two lines is for when y2 is a + # scalar like 0 and we want the fill to go all the way + # down to 0 even if none of the y1 sample points do + start = xslice[0], y2slice[0] + end = xslice[-1], y2slice[-1] + + X[0] = start + X[N + 1] = end + + X[1:N + 1, 0] = xslice + X[1:N + 1, 1] = y1slice + X[N + 2:, 0] = xslice[::-1] + X[N + 2:, 1] = y2slice[::-1] + + if dir == 'y': + polys.append(X) + elif dir == 'x': + polys.append(X.T[::-1].T) + return polys + + +def _pts_to_betweenstep(x, *args): + """ + Convert continuous line to between-steps. + + Given a set of ``N`` edges and ``N - 1`` values padded with Nan to size N, + converts to ``2N - 2`` points, which when connected linearly give + a step function connecting step edges at a specified value + + Parameters + ---------- + x : array + The x location of the step edges. May be empty. + + y1, ..., yp : array + y arrays to be turned into steps; must have length as ``x`` +/- 1. + Returns + ------- + out : array + The x and y values converted to steps in the same order as the input; + can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is + length ``N``, each of these arrays will be length ``2N - 2``. For + ``N=0``, the length will be 0. + + """ + args = np.array(args) + step_length = max(2 * max(len(x), len(args[0])) - 2, 0) + steps = np.zeros((1 + len(args), step_length)) + + xlike = len(x) == len(args[0]) + 1 + ylike = len(x) + 1 == len(args[0]) + + def __between__(steps, sl0, sl1, _x, _args): + # Be agnostic whether xlike or ylike + if _x.flatten().shape != x.shape: + if _x.flatten().shape[-1] == _x.shape[-1]: + _x = _x[0] + steps[sl0, ::steps.shape[-1]-1] = _x[::_x.shape[-1]-1] + steps[sl0, 2::2] = _x[1:-1] + steps[sl0, 1:-1:2] = _x[1:-1] + steps[sl1, 0::2] = _args + steps[sl1, 1::2] = _args + return steps + + if xlike: + steps = __between__(steps, 0, slice(1, None), + x, args) + elif ylike: + steps = __between__(steps, slice(1, None), 0, + args, x) + else: + # Fall back to steps-post (e.g. legend drawing) + steps = pts_to_poststep(x, *args) + + return steps + + +def _pts_to_betweenstep_edges(x, *args): + """ + Convert continuous line to between-steps, adding edges + + Given a set of ``N`` edges and ``N - 1`` values padded with Nan to size N, + converts to ``2N`` points, which when connected linearly give + a step function connecting step edges at a specified value, with the first + and last edge going to 0 + + Parameters + ---------- + x : array + The x location of the step edges. May be empty. + + y1, ..., yp : array + y arrays to be turned into steps; must have length as ``x`` +/- 1. + Returns + ------- + out : array + The x and y values converted to steps in the same order as the input; + can be unpacked as ``x_out, y1_out, ..., yp_out``. If the input is + length ``N``, each of these arrays will be length ``2N``. For + ``N=0``, the length will be 0. + + """ + xlike = len(x) == len(args[0]) + 1 + ylike = len(x) + 1 == len(args[0]) + if not (xlike or ylike): + # Fall back to steps-post (e.g. legend drawing) + return pts_to_poststep(x, *args) + + steps = _pts_to_betweenstep(x, *args) + # Extra steps to plot edges where values are missing (Nan). + nan_cols = np.nonzero(np.isnan(np.sum(steps, axis=0)))[0] + nan_cols[1::2] = nan_cols[1::2]+1 + pad_steps = [] + + for part in np.split(steps, nan_cols, axis=1): + if not np.isnan(np.sum(part)): + pad_part = np.zeros((2, part.shape[-1]+2)) + pad_part[:, 1:-1] = part + if xlike: + pad_part[:, 0] = part[:, 0] + pad_part[1, 0] = 0 + pad_part[:, -1] = part[:, -1] + pad_part[1, -1] = 0 + else: + pad_part[:, 0] = 0 + pad_part[1, 0] = part[1, 0] + pad_part[:, -1] = 0 + pad_part[1, -1] = part[1, -1] + pad_steps.append(pad_part) + else: + pad_steps.append(part) + + return np.hstack(pad_steps) + + def pts_to_poststep(x, *args): """ Convert continuous line to post-steps. @@ -1649,7 +1899,9 @@ def pts_to_midstep(x, *args): 'steps': pts_to_prestep, 'steps-pre': pts_to_prestep, 'steps-post': pts_to_poststep, - 'steps-mid': pts_to_midstep} + 'steps-mid': pts_to_midstep, + 'steps-between': _pts_to_betweenstep, + 'steps-edges': _pts_to_betweenstep_edges} def index_of(y): diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 76c256dfa185..3832f0c5773a 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -231,6 +231,8 @@ class Line2D(Artist): 'steps-mid': '_draw_steps_mid', 'steps-pre': '_draw_steps_pre', 'steps-post': '_draw_steps_post', + 'steps-between': '_draw_steps_between', + 'steps-edges': '_draw_steps_edges', } _drawStyles_s = { @@ -613,8 +615,9 @@ def set_picker(self, p): def get_window_extent(self, renderer): bbox = Bbox([[0, 0], [0, 0]]) trans_data_to_xy = self.get_transform().transform - bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()), - ignore=True) + padded_xy = np.column_stack( + cbook._pad_arrays(*self.get_xydata())).astype(float) + bbox.update_from_data_xy(trans_data_to_xy(padded_xy), ignore=True) # correct for marker size, if any if self._marker: ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5 @@ -665,8 +668,13 @@ def recache(self, always=False): else: y = self._y - self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float) - self._x, self._y = self._xy.T # views + if self._drawstyle in ["steps-between", "steps-edges"]: + # Account for varying x, y length + self._x, self._y = x, y + self._xy = np.array([self._x, self._y]) + else: + self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float) + self._x, self._y = self._xy.T # views self._subslice = False if (self.axes and len(x) > 1000 and self._is_sorted(x) and @@ -688,7 +696,11 @@ def recache(self, always=False): interpolation_steps = self._path._interpolation_steps else: interpolation_steps = 1 - xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy.T) + + if self._drawstyle in ["steps-between", "steps-edges"]: + xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy) + else: + xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy.T) self._path = Path(np.asarray(xy).T, _interpolation_steps=interpolation_steps) self._transformed_path = None @@ -703,7 +715,17 @@ def _transform_path(self, subslice=None): """ # Masked arrays are now handled by the Path class itself if subslice is not None: - xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy[subslice, :].T) + if self._drawstyle in ["steps-between", "steps-edges"]: + xy_asym_sliced = [row[subslice] for row in self._xy] + # If not asym after slice, crop as original + if len(set(map(len, xy_asym_sliced))) != 2: + if len(self._xy[0]) < len(self._xy[1]): + xy_asym_sliced[1] = xy_asym_sliced[1][:-1] + else: + xy_asym_sliced[0] = xy_asym_sliced[0][:-1] + xy = STEP_LOOKUP_MAP[self._drawstyle](*xy_asym_sliced) + else: + xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy[subslice, :].T) _path = Path(np.asarray(xy).T, _interpolation_steps=self._path._interpolation_steps) else: @@ -1056,7 +1078,7 @@ def set_drawstyle(self, drawstyle): Parameters ---------- drawstyle : {'default', 'steps', 'steps-pre', 'steps-mid', \ -'steps-post'}, default: 'default' +'steps-post', 'steps-between', 'steps-edges'}, default: 'default' For 'default', the points are connected with straight lines. The steps variants connect the points with step-like lines, @@ -1068,6 +1090,10 @@ def set_drawstyle(self, drawstyle): - 'steps-mid': The step is halfway between the points. - 'steps-post: The step is at the end of the line segment, i.e. the line will be at the y-value of the point to the left. + - 'between': Expects abs(len(x)-len(y)) == 1, steps have value y[i] + on the interval ``[x[i], x[i+1])`` + - 'edges': Expects abs(len(x)-len(y)) == 1, steps have value y[i] + on interval ``[x[i], x[i+1]), shape is closed at x[0], x[-1]`` - 'steps' is equal to 'steps-pre' and is maintained for backward-compatibility. diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf index eeb8969fa702..98c3e8cde324 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png index 59c32a9084d7..db3f7efd011a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png and b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg index 35a003c97c21..5d02afc717dd 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg @@ -1,7 +1,7 @@ - + - + +" id="mc63e59a608" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + @@ -572,92 +572,92 @@ L 0 4 +" id="m556f96d829" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m27e32ca04a" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -673,7 +673,7 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - + - + @@ -1153,72 +1153,72 @@ L 518.4 231.709091 - + - + - + - + - + - + - + - + - + - + - + - + @@ -1226,15 +1226,15 @@ L 518.4 231.709091 - + - + - +