diff --git a/.flake8 b/.flake8 index 20f404db4104..111f14d349e8 100644 --- a/.flake8 +++ b/.flake8 @@ -162,6 +162,7 @@ per-file-ignores = examples/lines_bars_and_markers/fill.py: E402 examples/lines_bars_and_markers/fill_between_demo.py: E402 examples/lines_bars_and_markers/filled_step.py: E402 + examples/lines_bars_and_markers/stairs_demo.py: E402 examples/lines_bars_and_markers/horizontal_barchart_distribution.py: E402 examples/lines_bars_and_markers/joinstyle.py: E402 examples/lines_bars_and_markers/scatter_hist.py: E402 diff --git a/doc/api/artist_api.rst b/doc/api/artist_api.rst index 184a16d4be61..d77f27f0960f 100644 --- a/doc/api/artist_api.rst +++ b/doc/api/artist_api.rst @@ -4,7 +4,7 @@ ``matplotlib.artist`` ********************* -.. inheritance-diagram:: matplotlib.axes._axes.Axes matplotlib.axes._base._AxesBase matplotlib.axis.Axis matplotlib.axis.Tick matplotlib.axis.XAxis matplotlib.axis.XTick matplotlib.axis.YAxis matplotlib.axis.YTick matplotlib.collections.AsteriskPolygonCollection matplotlib.collections.BrokenBarHCollection matplotlib.collections.CircleCollection matplotlib.collections.Collection matplotlib.collections.EllipseCollection matplotlib.collections.EventCollection matplotlib.collections.LineCollection matplotlib.collections.PatchCollection matplotlib.collections.PathCollection matplotlib.collections.PolyCollection matplotlib.collections.QuadMesh matplotlib.collections.RegularPolyCollection matplotlib.collections.StarPolygonCollection matplotlib.collections.TriMesh matplotlib.collections._CollectionWithSizes matplotlib.contour.ClabelText matplotlib.figure.Figure matplotlib.image.AxesImage matplotlib.image.BboxImage matplotlib.image.FigureImage matplotlib.image.NonUniformImage matplotlib.image.PcolorImage matplotlib.image._ImageBase matplotlib.legend.Legend matplotlib.lines.Line2D matplotlib.offsetbox.AnchoredOffsetbox matplotlib.offsetbox.AnchoredText matplotlib.offsetbox.AnnotationBbox matplotlib.offsetbox.AuxTransformBox matplotlib.offsetbox.DrawingArea matplotlib.offsetbox.HPacker matplotlib.offsetbox.OffsetBox matplotlib.offsetbox.OffsetImage matplotlib.offsetbox.PackerBase matplotlib.offsetbox.PaddedBox matplotlib.offsetbox.TextArea matplotlib.offsetbox.VPacker matplotlib.patches.Arc matplotlib.patches.Arrow matplotlib.patches.Circle matplotlib.patches.CirclePolygon matplotlib.patches.ConnectionPatch matplotlib.patches.Ellipse matplotlib.patches.FancyArrow matplotlib.patches.FancyArrowPatch matplotlib.patches.FancyBboxPatch matplotlib.patches.Patch matplotlib.patches.PathPatch matplotlib.patches.Polygon matplotlib.patches.Rectangle matplotlib.patches.RegularPolygon matplotlib.patches.Shadow matplotlib.patches.Wedge matplotlib.projections.geo.AitoffAxes matplotlib.projections.geo.GeoAxes matplotlib.projections.geo.HammerAxes matplotlib.projections.geo.LambertAxes matplotlib.projections.geo.MollweideAxes matplotlib.projections.polar.PolarAxes matplotlib.quiver.Barbs matplotlib.quiver.Quiver matplotlib.quiver.QuiverKey matplotlib.spines.Spine matplotlib.table.Cell matplotlib.table.CustomCell matplotlib.table.Table matplotlib.text.Annotation matplotlib.text.Text +.. inheritance-diagram:: matplotlib.axes._axes.Axes matplotlib.axes._base._AxesBase matplotlib.axis.Axis matplotlib.axis.Tick matplotlib.axis.XAxis matplotlib.axis.XTick matplotlib.axis.YAxis matplotlib.axis.YTick matplotlib.collections.AsteriskPolygonCollection matplotlib.collections.BrokenBarHCollection matplotlib.collections.CircleCollection matplotlib.collections.Collection matplotlib.collections.EllipseCollection matplotlib.collections.EventCollection matplotlib.collections.LineCollection matplotlib.collections.PatchCollection matplotlib.collections.PathCollection matplotlib.collections.PolyCollection matplotlib.collections.QuadMesh matplotlib.collections.RegularPolyCollection matplotlib.collections.StarPolygonCollection matplotlib.collections.TriMesh matplotlib.collections._CollectionWithSizes matplotlib.contour.ClabelText matplotlib.figure.Figure matplotlib.image.AxesImage matplotlib.image.BboxImage matplotlib.image.FigureImage matplotlib.image.NonUniformImage matplotlib.image.PcolorImage matplotlib.image._ImageBase matplotlib.legend.Legend matplotlib.lines.Line2D matplotlib.offsetbox.AnchoredOffsetbox matplotlib.offsetbox.AnchoredText matplotlib.offsetbox.AnnotationBbox matplotlib.offsetbox.AuxTransformBox matplotlib.offsetbox.DrawingArea matplotlib.offsetbox.HPacker matplotlib.offsetbox.OffsetBox matplotlib.offsetbox.OffsetImage matplotlib.offsetbox.PackerBase matplotlib.offsetbox.PaddedBox matplotlib.offsetbox.TextArea matplotlib.offsetbox.VPacker matplotlib.patches.Arc matplotlib.patches.Arrow matplotlib.patches.Circle matplotlib.patches.CirclePolygon matplotlib.patches.ConnectionPatch matplotlib.patches.Ellipse matplotlib.patches.FancyArrow matplotlib.patches.FancyArrowPatch matplotlib.patches.FancyBboxPatch matplotlib.patches.Patch matplotlib.patches.PathPatch matplotlib.patches.StepPatch matplotlib.patches.Polygon matplotlib.patches.Rectangle matplotlib.patches.RegularPolygon matplotlib.patches.Shadow matplotlib.patches.Wedge matplotlib.projections.geo.AitoffAxes matplotlib.projections.geo.GeoAxes matplotlib.projections.geo.HammerAxes matplotlib.projections.geo.LambertAxes matplotlib.projections.geo.MollweideAxes matplotlib.projections.polar.PolarAxes matplotlib.quiver.Barbs matplotlib.quiver.Quiver matplotlib.quiver.QuiverKey matplotlib.spines.Spine matplotlib.table.Cell matplotlib.table.CustomCell matplotlib.table.Table matplotlib.text.Annotation matplotlib.text.Text :parts: 1 :private-bases: diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index aa10872ddd35..cf0f0fdac8d7 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -137,6 +137,7 @@ Binned Axes.hexbin Axes.hist Axes.hist2d + Axes.stairs Contours -------- diff --git a/doc/api/patches_api.rst b/doc/api/patches_api.rst index e183fde84c12..ac21a644ccab 100644 --- a/doc/api/patches_api.rst +++ b/doc/api/patches_api.rst @@ -29,6 +29,7 @@ Classes FancyBboxPatch Patch PathPatch + StepPatch Polygon Rectangle RegularPolygon diff --git a/doc/users/next_whats_new/steppatch_and_stairs.rst b/doc/users/next_whats_new/steppatch_and_stairs.rst new file mode 100644 index 000000000000..268130ff9d1e --- /dev/null +++ b/doc/users/next_whats_new/steppatch_and_stairs.rst @@ -0,0 +1,23 @@ +New StepPatch artist and a stairs method +---------------------------------------- +New `~.matplotlib.patches.StepPatch` artist and `.pyplot.stairs` method. +For both the artist and the function, the x-like edges input is one +longer than the y-like values input + + .. plot:: + + import numpy as np + import matplotlib.pyplot as plt + + np.random.seed(0) + h, bins = np.histogram(np.random.normal(5, 2, 5000), + bins=np.linspace(0,10,20)) + + fig, ax = plt.subplots(constrained_layout=True) + + ax.stairs(h, bins) + + plt.show() + +See :doc:`/gallery/lines_bars_and_markers/stairs_demo` +for examples. \ No newline at end of file diff --git a/examples/lines_bars_and_markers/stairs_demo.py b/examples/lines_bars_and_markers/stairs_demo.py new file mode 100644 index 000000000000..d9f8cce98326 --- /dev/null +++ b/examples/lines_bars_and_markers/stairs_demo.py @@ -0,0 +1,81 @@ +""" +=========== +Stairs Demo +=========== + +This example demonstrates the use of `~.matplotlib.pyplot.stairs` +for histogram and histogram-like data visualization and an associated +underlying `.StepPatch` artist, which is a specialized version of +`.PathPatch` specified by its bins and edges. + +The primary difference to `~.matplotlib.pyplot.step` is that ``stairs`` +x-like edges input is one longer than its y-like values input. +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import StepPatch + +np.random.seed(0) +h, bins = np.histogram(np.random.normal(5, 3, 5000), + bins=np.linspace(0, 10, 20)) + +fig, axs = plt.subplots(3, 1, figsize=(7, 15)) +axs[0].stairs(h, bins, label='Simple histogram') +axs[0].stairs(h, bins+5, baseline=50, label='Modified baseline') +axs[0].stairs(h, bins+10, baseline=None, label='No edges') +axs[0].set_title("Step Histograms") + +axs[1].stairs(np.arange(1, 6, 1), fill=True, + label='Filled histogram\nw/ automatic edges') +axs[1].stairs(np.arange(1, 6, 1)*0.3, np.arange(2, 8, 1), + orientation='horizontal', hatch='//', + label='Hatched histogram\nw/ horizontal orientation') +axs[1].set_title("Filled histogram") + +patch = StepPatch(values=[1, 2, 3, 2, 1], + edges=range(1, 7), + label=('Patch derived underlying object\n' + 'with default edge/facecolor behaviour')) +axs[2].add_patch(patch) +axs[2].set_xlim(0, 7) +axs[2].set_ylim(-1, 5) +axs[2].set_title("StepPatch artist") + +for ax in axs: + ax.legend() +plt.show() + +############################################################################# +# Comparison of `.pyplot.step` and `.pyplot.stairs`. + +bins = np.arange(14) +centers = bins[:-1] + np.diff(bins)/2 +y = np.sin(centers / 2) + +plt.step(bins[:-1], y, where='post', label='Step(where="post")') +plt.plot(bins[:-1], y, 'o--', color='grey', alpha=0.3) + +plt.stairs(y - 1, bins, baseline=None, label='Stairs') +plt.plot(centers, y - 1, 'o--', color='grey', alpha=0.3) +plt.plot(np.repeat(bins, 2), np.hstack([y[0], np.repeat(y, 2), y[-1]]) - 1, + 'o', color='red', alpha=0.2) + +plt.legend() +plt.title('plt.step vs plt.stairs') +plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions, methods, classes and modules is shown +# in this example: + +import matplotlib +matplotlib.axes.Axes.stairs +matplotlib.pyplot.stairs +matplotlib.patches.StepPatch diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 500ef4365092..206d63f7dfc8 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6871,6 +6871,73 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, else "List[Polygon]") return tops, bins, cbook.silent_list(patch_type, patches) + @_preprocess_data() + def stairs(self, values, edges=None, *, + orientation='vertical', baseline=0, fill=False, **kwargs): + """ + A histogram-like line or filled plot. + + Parameters + ---------- + values : array-like + An array of y-values. + + edges : array-like, default: ``range(len(vals)+1)`` + A array of x-values, with ``len(edges) == len(vals) + 1``, + between which the curve takes on vals values. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + + baseline : float or None, default: 0 + Determines starting value of the bounding edges or when + ``fill=True``, position of lower edge. + + fill : bool, default: False + + Returns + ------- + StepPatch : `matplotlib.patches.StepPatch` + + Other Parameters + ---------------- + **kwargs + `~matplotlib.patches.StepPatch` properties + + """ + + if 'color' in kwargs: + _color = kwargs.pop('color') + else: + _color = self._get_lines.get_next_color() + if fill: + kwargs.setdefault('edgecolor', 'none') + kwargs.setdefault('facecolor', _color) + else: + kwargs.setdefault('edgecolor', _color) + + if edges is None: + edges = np.arange(len(values) + 1) + + self._process_unit_info(xdata=edges, ydata=values, kwargs=kwargs) + edges = self.convert_xunits(edges) + values = self.convert_yunits(values) + + patch = mpatches.StepPatch(values, + edges, + baseline=baseline, + orientation=orientation, + fill=fill, + **kwargs) + self.add_patch(patch) + if baseline is None: + baseline = 0 + if orientation == 'vertical': + patch.sticky_edges.y.append(baseline) + else: + patch.sticky_edges.x.append(baseline) + self._request_autoscale_view() + return patch + @_preprocess_data(replace_names=["x", "y", "weights"]) @docstring.dedent_interpd def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 25f790893961..e30d8056eb08 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -33,7 +33,8 @@ from matplotlib.cbook import silent_list from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D -from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch +from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, + StepPatch) from matplotlib.collections import (LineCollection, RegularPolyCollection, CircleCollection, PathCollection, PolyCollection) @@ -623,6 +624,7 @@ def draw(self, renderer): ErrorbarContainer: legend_handler.HandlerErrorbar(), Line2D: legend_handler.HandlerLine2D(), Patch: legend_handler.HandlerPatch(), + StepPatch: legend_handler.HandlerStepPatch(), LineCollection: legend_handler.HandlerLineCollection(), RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(), CircleCollection: legend_handler.HandlerCircleCollection(), diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 4b96dd4289c5..45fb759d7b23 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -302,6 +302,51 @@ def create_artists(self, legend, orig_handle, return [p] +class HandlerStepPatch(HandlerBase): + """ + Handler for `~.matplotlib.patches.StepPatch` instances. + """ + def __init__(self, **kw): + """ + Any other keyword arguments are given to `HandlerBase`. + """ + super().__init__(**kw) + + def _create_patch(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize): + p = Rectangle(xy=(-xdescent, -ydescent), + color=orig_handle.get_facecolor(), + width=width, height=height) + return p + + # Unfilled StepPatch should show as a line + def _create_line(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize): + + # Overwrite manually because patch and line properties don't mix + legline = Line2D([0, width], [height/2, height/2], + color=orig_handle.get_edgecolor(), + linestyle=orig_handle.get_linestyle(), + linewidth=orig_handle.get_linewidth(), + ) + + legline.set_drawstyle('default') + legline.set_marker("") + return legline + + def create_artists(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize, trans): + if orig_handle.get_fill() or (orig_handle.get_hatch() is not None): + p = self._create_patch(legend, orig_handle, + xdescent, ydescent, width, height, fontsize) + self.update_prop(p, orig_handle, legend) + else: + p = self._create_line(legend, orig_handle, + xdescent, ydescent, width, height, fontsize) + p.set_transform(trans) + return [p] + + class HandlerLineCollection(HandlerLine2D): """ Handler for `.LineCollection` instances. diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 859d495111cb..a616e3d77281 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -989,6 +989,121 @@ def set_path(self, path): self._path = path +class StepPatch(PathPatch): + """An unclosed step path patch.""" + + @docstring.dedent_interpd + def __init__(self, values, edges, *, + orientation='vertical', baseline=0, **kwargs): + """ + Parameters + ---------- + values : array-like + An array of y-values. + + edges : array-like + A array of x-value edges, with ``len(edges) == len(vals) + 1``, + between which the curve takes on vals values. + + orientation : {'vertical', 'horizontal'}, default: 'vertical' + + baseline : float or None, default: 0 + Determines starting value of the bounding edges or when + ``fill=True``, position of lower edge. + + Other valid keyword arguments are: + + %(Patch)s + """ + self.baseline = baseline + self.orientation = orientation + self._edges = np.asarray(edges) + self._values = np.asarray(values) + self._update_path() + super().__init__(self._path, **kwargs) + + def _update_path(self): + if np.isnan(np.sum(self._edges)): + raise ValueError('Nan values in "edges" are disallowed') + if self._edges.size - 1 != self._values.size: + raise ValueError('Size mismatch between "values" and "edges". ' + "Expected `len(values) + 1 == len(edges)`, but " + f"`len(values) = {self._values.size}` and " + f"`len(edges) = {self._edges.size}`.") + verts, codes = [], [] + for idx0, idx1 in cbook.contiguous_regions(~np.isnan(self._values)): + x = np.repeat(self._edges[idx0:idx1+1], 2) + y = np.repeat(self._values[idx0:idx1], 2) + if self.baseline is not None: + y = np.hstack((self.baseline, y, self.baseline)) + else: + y = np.hstack((y[0], y, y[-1])) + if self.orientation == 'vertical': + xy = np.column_stack([x, y]) + else: + xy = np.column_stack([y, x]) + verts.append(xy) + codes.append(np.array([Path.MOVETO] + [Path.LINETO]*(len(xy)-1))) + self._path = Path(np.vstack(verts), np.hstack(codes)) + + def get_data(self): + """Get `.StepPatch` values and edges.""" + return self._values, self._edges + + def set_data(self, values, edges=None): + """ + Set `.StepPatch` values and optionally edges. + + Parameters + ---------- + values : 1D array-like or None + Will not update values, if passing None + edges : 1D array-like, optional + """ + if values is not None: + self._values = np.asarray(values) + if edges is not None: + self._edges = np.asarray(edges) + self._update_path() + self.stale = True + + def set_values(self, values): + """ + Set `.StepPatch` values. + + Parameters + ---------- + values : 1D array-like + """ + self.set_data(values, edges=None) + + def set_edges(self, edges): + """ + Set `.StepPatch` edges. + + Parameters + ---------- + edges : 1D array-like + """ + self.set_data(None, edges=edges) + + def get_baseline(self): + """Get `.StepPatch` baseline value.""" + return self.baseline + + def set_baseline(self, baseline): + """ + Set `.StepPatch` baseline value. + + Parameters + ---------- + baseline : float or None + """ + self.baseline = baseline + self._update_path() + self.stale = True + + class Polygon(Patch): """A general polygon patch.""" diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index ba82e3433716..c5c047471aa8 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2777,6 +2777,17 @@ def hist( **({"data": data} if data is not None else {}), **kwargs) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Axes.stairs) +def stairs( + values, edges=None, *, orientation='vertical', baseline=0, + fill=False, data=None, **kwargs): + return gca().stairs( + values, edges=edges, orientation=orientation, + baseline=baseline, fill=fill, + **({"data": data} if data is not None else {}), **kwargs) + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.hist2d) def hist2d( diff --git a/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_datetime.png b/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_datetime.png new file mode 100644 index 000000000000..fa499047b0f8 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_datetime.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_options.png b/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_options.png new file mode 100644 index 000000000000..0f750bc421d9 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_options.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a35f55a55db9..801294158551 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1744,6 +1744,148 @@ def test_hist_zorder(histtype, zorder): assert patch.get_zorder() == zorder +@check_figures_equal() +def test_stairs(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 + + fig_test, test_axes = plt.subplots(3, 2) + test_axes = test_axes.flatten() + test_axes[0].stairs(y, x, baseline=None) + test_axes[1].stairs(y, x, baseline=None, orientation='horizontal') + test_axes[2].stairs(y, x) + test_axes[3].stairs(y, x, orientation='horizontal') + test_axes[4].stairs(y, x) + test_axes[4].semilogy() + test_axes[5].stairs(y, x, orientation='horizontal') + test_axes[5].semilogy() + + fig_ref, ref_axes = plt.subplots(3, 2) + ref_axes = ref_axes.flatten() + ref_axes[0].plot(x, np.append(y, y[-1]), drawstyle='steps-post') + ref_axes[1].plot(np.append(y[0], y), x, drawstyle='steps-post') + + ref_axes[2].plot(x, np.append(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[2].set_ylim(0, None) + + ref_axes[3].plot(np.append(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[3].set_xlim(0, None) + + ref_axes[4].plot(x, np.append(y, y[-1]), drawstyle='steps-post') + ref_axes[4].add_line(mlines.Line2D([x[0], x[0]], [0, y[0]])) + ref_axes[4].add_line(mlines.Line2D([x[-1], x[-1]], [0, y[-1]])) + ref_axes[4].semilogy() + + ref_axes[5].plot(np.append(y[0], y), x, drawstyle='steps-post') + ref_axes[5].add_line(mlines.Line2D([0, y[0]], [x[0], x[0]])) + ref_axes[5].add_line(mlines.Line2D([0, y[-1]], [x[-1], x[-1]])) + ref_axes[5].semilogx() + + +@check_figures_equal() +def test_stairs_fill(fig_test, fig_ref): + h, bins = [1, 2, 3, 4, 2], [0, 1, 2, 3, 4, 5] + bs = -2 + # Test + fig_test, test_axes = plt.subplots(2, 2) + test_axes = test_axes.flatten() + test_axes[0].stairs(h, bins, fill=True) + test_axes[1].stairs(h, bins, orientation='horizontal', fill=True) + test_axes[2].stairs(h, bins, baseline=bs, fill=True) + test_axes[3].stairs(h, bins, baseline=bs, orientation='horizontal', + fill=True) + + # # Ref + fig_ref, ref_axes = plt.subplots(2, 2) + ref_axes = ref_axes.flatten() + ref_axes[0].fill_between(bins, np.append(h, h[-1]), step='post') + ref_axes[0].set_ylim(0, None) + ref_axes[1].fill_betweenx(bins, np.append(h, h[-1]), step='post') + ref_axes[1].set_xlim(0, None) + ref_axes[2].fill_between(bins, np.append(h, h[-1]), + np.ones(len(h)+1)*bs, step='post') + ref_axes[2].set_ylim(bs, None) + ref_axes[3].fill_betweenx(bins, np.append(h, h[-1]), + np.ones(len(h)+1)*bs, step='post') + ref_axes[3].set_xlim(bs, None) + + +@check_figures_equal() +def test_stairs_update(fig_test, fig_ref): + # Test + fig_test, test_ax = plt.subplots() + h = test_ax.stairs([1, 2, 3]) + h.set_values([3, 2, 1]) + h.set_edges(np.arange(4)+2) + h.set_data([1, 2, 1], np.arange(4)/2) + h.set_data([1, 2, 3]) + h.set_data(None, np.arange(4)) + assert np.allclose(h.get_data()[0], np.arange(1, 4)) + assert np.allclose(h.get_data()[1], np.arange(4)) + h.set_baseline(-2) + assert h.get_baseline() == -2 + + # # Ref + fig_ref, ref_ax = plt.subplots() + h = ref_ax.stairs([1, 2, 3], baseline=-2) + + +@pytest.mark.xfail +def test_stairs_invalid_nan(): + plt.stairs([1, 2], [0, np.nan, 1]) + + +@pytest.mark.xfail +def test_stairs_invalid_mismatch(): + plt.stairs([1, 2], [0, 1]) + + +@pytest.mark.xfail +def test_stairs_invalid_update(): + h = plt.stairs([1, 2], [0, 1, 2]) + h.set_edges([1, np.nan, 2]) + + +@pytest.mark.xfail +def test_stairs_invalid_update2(): + h = plt.stairs([1, 2], [0, 1, 2]) + h.set_edges(np.arange(5)) + + +@image_comparison(['test_stairs_options.png'], remove_text=True) +def test_stairs_options(): + x, y = np.array([1, 2, 3, 4, 5]), np.array([1, 2, 3, 4]).astype(float) + yn = y.copy() + yn[1] = np.nan + + fig, ax = plt.subplots() + ax.stairs(y*3, x, color='green', fill=True, label="A") + ax.stairs(y, x*3-3, color='red', fill=True, + orientation='horizontal', label="B") + ax.stairs(yn, x, color='orange', ls='--', lw=2, label="C") + ax.stairs(yn/3, x*3-2, ls='--', lw=2, baseline=0.5, + orientation='horizontal', label="D") + ax.stairs(y[::-1]*3+12, x, color='red', ls='--', lw=2, baseline=None, + label="E") + ax.stairs(y[:-1][::-1]*2+11, x[:-1]+0.5, color='black', ls='--', lw=2, + baseline=12, hatch='//', label="F") + ax.legend(loc=0) + + +@image_comparison(['test_stairs_datetime.png']) +def test_stairs_datetime(): + f, ax = plt.subplots(constrained_layout=True) + ax.stairs(np.arange(36), + np.arange(np.datetime64('2001-12-27'), + np.datetime64('2002-02-02'))) + plt.xticks(rotation=30) + + def contour_dat(): x = np.linspace(-3, 5, 150) y = np.linspace(-3, 5, 120) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 98840d1f09f2..5035db1c3db2 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -215,6 +215,7 @@ def boilerplate_gen(): 'grid', 'hexbin', 'hist', + 'stairs', 'hist2d', 'hlines', 'imshow',