Skip to content

feat: StepPatch #18275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 17, 2020
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc/api/artist_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Binned
Axes.hexbin
Axes.hist
Axes.hist2d
Axes.stairs

Contours
--------
Expand Down
1 change: 1 addition & 0 deletions doc/api/patches_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Classes
FancyBboxPatch
Patch
PathPatch
StepPatch
Polygon
Rectangle
RegularPolygon
Expand Down
23 changes: 23 additions & 0 deletions doc/users/next_whats_new/steppatch_and_stairs.rst
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions examples/lines_bars_and_markers/stairs_demo.py
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`~matplotlib.patches.StepPatch` properties
`~.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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just looking from the peanut gallery...

  1. do you actually want a sticky at y=0 if baseline is None?
  2. adding the sticky could be done as part of the StepPatch implementation (you could even try to keep it in sync in set_baseline, although it's not necessarily clear what to do if the user manually changed the value in the meantime, so... perhaps not). I realize other classes don't do that, although for many of them that's because they're too general for the class to really know what stickies to use (e.g. a Rectangle does not know it's being used as part of a histogram).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 is intended. I think the implied consensus was that the naming would be general but some histogram like assumptions could be made in the plotting function. That's why sticky is in stairs and not in StepPatch. I suppose that means the update of StepPatch is not nicely behaved wrt baseline sticky, but then again neither are lims

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,
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(),
Expand Down
45 changes: 45 additions & 0 deletions lib/matplotlib/legend_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading