Repository: matplotlib/matplotlib
Pull request: feat: StepPatch
## PR Summary

A new `StepPatch` artists based of `PathPatch` facilitation plotting histogram-like data and an associated `pyplot.stairs()` plotting function.

Implementation based on @timhoffm and @jklymak suggestions, after drafts and discussions in #15065 and #15019 and the discussion below.

Closes #6669 and possibly #5855.

```
y, x = np.histogram(np.random.normal(5, 2, 1000), bins=np.linspace(0, 10, 40))
y = y.astype(float)
y[10:12] = np.nan
y[17] = np.nan
y[-2:] = np.nan

f, ax = plt.subplots()
ax.stairs(y+3, baseline=2, label="X", fill=True, ls='-')
ax.stairs(y*2, baseline=None, label="X", fill=False, ls='--', lw=2)
``` 

![image](https://user-images.githubusercontent.com/13226500/90406856-04ec2800-e0a6-11ea-9616-29cea5766300.png)

It is also considerably faster than `ax.hist` for a filled plot, since it draws only one* patch.

![image](https://user-images.githubusercontent.com/13226500/90437748-27e10100-e0d3-11ea-93b6-5bbb962833b4.png)


## PR Checklist

- [x] Has Pytest style unit tests
- [x] Code is [Flake 8](http://flake8.pycqa.org/en/latest/) compliant
- [x] New features are documented, with examples if plot related
- [x] Documentation is sphinx and numpydoc compliant
- [x] Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
- [x] Documented in doc/api/next_api_changes/* if API changed in a backward-incompatible way

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at
  https://matplotlib.org/devel/gitwash/development_workflow.html.

- Do not create the PR out of master, but out of a separate branch.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[Feature request] Functions for "manually" plotting histograms
matplotlib's `pyplot.hist` and `pyplot.hist2d` functions fill the supplied data into histogram bins and plot them, but very often users will have already done the first step (sorting data into bins) in their own code and are merely interested in presenting their results in a "style" that is evocative of histograms (most commonly: bar chart with no separation for 1D data, colored grid for 2D data).

Right now they can do this by using the `pyplot.bar` and `pyplot.imshow` functions for 1D and 2D data, respectively, but they have to fill in a bunch of arguments to make it look like something produced by `pyplot.hist` or `pyplot.hist2d` and they have to find out that those two are the right functions for the job in the first place (not very intuitive).

Hence I think it would be good if there were separate functions or maybe just special arguments to `pyplot.hist/hist2d` that allow you to plot bin _values_ (as opposed to data that is yet to be binned) as histograms that look exactly as if they had been produced by `pyplot.hist/hist2d`.

Hints from issue discussion:
How is what you're asking for different from passing in the already supported bin kwarg?

```
hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, **kwargs)

hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, cmax=None, **kwargs)
```

bins can be an array or list of bins, not just an integer.
- [hist documentation](http://matplotlib.org/api/axes_api.html?highlight=hist#matplotlib.axes.Axes.hist)
- [hist2d documentation](http://matplotlib.org/api/axes_api.html?highlight=hist#matplotlib.axes.Axes.hist)

It is (slightly) different because the OP isn't asking for the ability to
pass in a list of bins at which to operate at, but rather to pass in the
resulting histogram along with the bins.

We probably should look into refactoring hist/hist2d a bit so that those
functions are composed of a data-prep step and then a data display step.
Those steps could probably become user-facing functions in their own right
(in their own module... not in axes or pyplot) which advanced users could
access themselves if they want to bypass one step or the other.

On Thu, Jun 30, 2016 at 3:30 PM, hannah notifications@github.com wrote:

> How is what you're asking for different from passing in the already
> supported bin kwarg?
> 
> hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, **kwargs)
> 
> hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, cmax=None, **kwargs)
> 
> bins can be an array or list of bins, not just an integer.
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> https://github.com/matplotlib/matplotlib/issues/6669#issuecomment-229763951,
> or mute the thread
> https://github.com/notifications/unsubscribe/AARy-AI2JA2qvd4HQctKiiqjAy3IJbIlks5qRBlKgaJpZM4JCCfT
> .

Thanks for clarifying. 

Had a look at hist and it seems [really entangled](https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_axes.py#L5800) and I wonder if part of the refactoring should be pitching the display to bar & line2d and others with the customization needed for hist. 

Correct me if I'm wrong, but to me it seems like [this line](https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_axes.py#L6153) pretty much marks the border between data preparation and display logic. `n` contains the histogram-processed data ("bin height") to be plotted, and that is what I would like the user to be able to supply directly (in addition to `bins`, of course).

Kind of. There's some stuff early on about [plot type](https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_axes.py#L6044) and other display logic early on and there's stuff like [conversion logic](https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_axes.py#L6060) that's important for both, as are some of the cbook calls. 

That being said, would you be motivated to factor out the computation logic? 

I'll try, unless someone who is more qualified "feels motivated" :)

You're perfectly qualified to do it! You're motivated, you grok the problem & the solution, and you mostly know where to start. If you sort it out in a less than matplotlibish way, the devs will tell you on the PR. (And you can always ask [on the gitter](https://gitter.im/matplotlib/matplotlib))

:+1: This is something that needs to be done

Hi, what's the status on this? Maybe it is possible to just improve the docs? Ideally, one should be able to pass the output of `numpy.histogram` to this `hist`, because it is a standard in the python world and other histogramming libraries usually support it as well.
Possible implementation: if the x argument of `hist` is getting a tuple with two sequences N+1 and N, then interpret this as the numpy output.

Commit message: feat: add datetime test for StepPatch

Patch to review:
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..e196bb6cf66a
--- /dev/null
+++ b/doc/users/next_whats_new/steppatch_and_stairs.rst
@@ -0,0 +1,22 @@
+New `~.matplotlib.patches.StepPatch` artist and a `.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..707cd37c1c06 100644
--- a/lib/matplotlib/patches.py
+++ b/lib/matplotlib/patches.py
@@ -989,6 +989,122 @@ 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 "
+                             "they are or lengths {} and {}".format(
+                                 self._edges.size, self._values.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..437f99f56ab3 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])
+    h.set_edges(np.arange(5))
+
+
+@pytest.mark.xfail
+def test_stairs_invalid_update2():
+    h = plt.stairs([1, 2], [0, 1])
+    h.set_edges([1, np.nan, 2])
+
+
+@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',


Code review comments:
diff_hunk: @@ -989,6 +989,122 @@ 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 "
+                             "they are or lengths {} and {}".format(
+                                 self._edges.size, self._values.size)
path: lib/matplotlib/patches.py
text: @user1:
Suggestion: replace 1031 and 1032 with the following, which clarifies which size is which.
```python
f"`len(values) = {self._values.size}` and "
f"`len(edges) = {self._edges.size}`."
```

diff_hunk: @@ -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])
+    h.set_edges(np.arange(5))
path: lib/matplotlib/tests/test_axes.py
text: @user1:
What are you trying to do w/ these tests?  They are failing before the set_edges gets run...  I've not seen us use `xfail` tests very often in the test base.  

@author:
Good catch, but yeah the idea is just to make sure stairs fail when the values are invalid.