diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 44467581f446..f7c6f2f3fa6f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2043,7 +2043,8 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, #### Specialized plotting # @_preprocess_data() # let 'plot' do the unpacking.. - def step(self, x, y, *args, where='pre', data=None, **kwargs): + def step(self, x, y, *args, where='pre', data=None, + bottom=0, orientation='vertical', **kwargs): """ Make a step plot. @@ -2079,7 +2080,7 @@ 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'}, optional, default 'pre' + where : {'pre', 'post', 'mid', 'edges'}, optional, default 'pre' Define where the steps should be placed: - 'pre': The y value is continued constantly to the left from @@ -2089,6 +2090,18 @@ 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 len(x) = len(y) + 1, steps have y[i] value on + the interval ``[x[i], x[i+1])`` + - 'edges': Expects len(x) = len(y) + 1, steps have y[i] value on + the interval ``[x[i], x[i+1]), shape is closed at x[0], x[-1]`` + + orientation : {'vertical', 'horizontal'}, optional, default 'vertical' + If horizontal, switches x and y axes. You may wish to rotate + instead of flipping by parsing y=y[::-1] + + bottom : scalar, optional, default : 0 + If plotting with 'edges', sets low bound. + Returns ------- @@ -2104,7 +2117,37 @@ 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', 'edges', 'between'), + where=where) + cbook._check_in_list(('horizontal', 'vertical'), + orientation=orientation) + if where == 'between' or where == 'edges': + if x.shape[0] != y.shape[0] + 1: + raise ValueError(f"When drawing with 'edges' or 'between', " + f"x must have first dimension greater " + f"then y by exactly 1, but x, y " + f"have shapes {x.shape} and " + f"{y.shape}") + + if orientation == 'horizontal': + y = np.r_[y[0], y] + x, y = y, x + elif orientation == 'vertical': + y = np.r_[y, y[-1]] + + if where == 'edges': + if orientation == 'horizontal': + self.add_line( + mlines.Line2D([bottom, x[0]], [y[0], y[0]])) + self.add_line( + mlines.Line2D([bottom, x[-1]], [y[-1], y[-1]])) + elif orientation == 'vertical': + self.add_line( + mlines.Line2D([x[0], x[0]], [bottom, y[0]])) + self.add_line( + mlines.Line2D([x[-1], x[-1]], [bottom, y[-1]])) + where = 'post' + kwargs['drawstyle'] = 'steps-' + where return self.plot(x, y, *args, data=data, **kwargs) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 64b055479e7c..1699f38d7a60 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2916,10 +2916,13 @@ def stem( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @docstring.copy(Axes.step) -def step(x, y, *args, where='pre', data=None, **kwargs): +def step( + x, y, *args, where='pre', data=None, bottom=0, + orientation='vertical', **kwargs): return gca().step( x, y, *args, where=where, **({"data": data} if data is not - None else {}), **kwargs) + None else {}), bottom=bottom, orientation=orientation, + **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index f7036342d720..efaa132d6540 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3696,6 +3696,60 @@ def test_step_linestyle(): ax.set_ylim([-1, 7]) +@check_figures_equal() +def test_step_histlike(fig_test, fig_ref): + import matplotlib.lines as mlines + y = np.array([6, 14, 32, 37, 48, 32, 21, 4]) # hist + x = np.array([1., 2., 3., 4., 5., 6., 7., 8., 9.]) # bins + bottom = -10 + # Test + fig_test, test_axes = plt.subplots(4, 2) + test_axes = test_axes.flatten() + test_axes[0].step(x, y, where='between') + test_axes[1].step(x, y, where='between', orientation='horizontal') + test_axes[2].step(x, y, where='edges') + test_axes[3].step(x, y, where='edges', orientation='horizontal') + test_axes[4].step(x, y, where='edges', bottom=-10) + test_axes[5].step(x, y, where='edges', orientation='horizontal', + bottom=bottom) + test_axes[6].step(x, y, where='edges', bottom=-10) + test_axes[6].semilogy() + test_axes[7].step(x, y, where='edges', orientation='horizontal', + bottom=bottom) + test_axes[7].semilogy() + # Ref + fig_ref, ref_axes = plt.subplots(4, 2) + ref_axes = ref_axes.flatten() + ref_axes[0].plot(x, np.r_[y, y[-1]], drawstyle='steps-post') + ref_axes[1].plot(np.r_[y[0], y], x, drawstyle='steps-post') + + ref_axes[2].plot(x, np.r_[y, y[-1]], drawstyle='steps-post') + ref_axes[2].add_line(mlines.Line2D([x[0], x[0]], [0, y[0]])) + ref_axes[2].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]])) + + ref_axes[3].plot(np.r_[y[0], y], x, drawstyle='steps-post') + ref_axes[3].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]])) + ref_axes[3].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]])) + + ref_axes[4].plot(x, np.r_[y, y[-1]], drawstyle='steps-post') + ref_axes[4].add_line(mlines.Line2D([x[0], x[0]], [bottom, y[0]])) + ref_axes[4].add_line(mlines.Line2D([x[-1], x[-1]], [bottom, y[-1]])) + + ref_axes[5].plot(np.r_[y[0], y], x, drawstyle='steps-post') + ref_axes[5].add_line(mlines.Line2D([bottom, y[0]], [x[0], x[0]])) + ref_axes[5].add_line(mlines.Line2D([bottom, y[-1]], [x[-1], x[-1]])) + + ref_axes[6].plot(x, np.r_[y, y[-1]], drawstyle='steps-post') + ref_axes[6].add_line(mlines.Line2D([x[0], x[0]], [bottom, y[0]])) + ref_axes[6].add_line(mlines.Line2D([x[-1], x[-1]], [bottom, y[-1]])) + ref_axes[6].semilogy() + + ref_axes[7].plot(np.r_[y[0], y], x, drawstyle='steps-post') + ref_axes[7].add_line(mlines.Line2D([bottom, y[0]], [x[0], x[0]])) + ref_axes[7].add_line(mlines.Line2D([bottom, y[-1]], [x[-1], x[-1]])) + ref_axes[7].semilogx() + + @image_comparison(['mixed_collection'], remove_text=True) def test_mixed_collection(): from matplotlib import patches