Skip to content

Add option to create horizontally-oriented stem plots #18187

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 8 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/users/next_whats_new/stem_orientation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Added *orientation* parameter for stem plots
--------------------------------------------

By default, stem lines are vertical. They can be changed to horizontal using
the *orientation* parameter of `.Axes.stem` or `.pyplot.stem`:

.. plot::

locs = np.linspace(0.1, 2 * np.pi, 25)
heads = np.cos(locs)

fig, ax = plt.subplots()
ax.stem(locs, heads, orientation='horizontal')
85 changes: 63 additions & 22 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2723,27 +2723,32 @@ def broken_barh(self, xranges, yrange, **kwargs):

@_preprocess_data()
def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
label=None, use_line_collection=True):
label=None, use_line_collection=True, orientation='vertical'):
"""
Create a stem plot.

A stem plot plots vertical lines at each *x* location from the baseline
to *y*, and places a marker there.
A stem plot draws lines perpendicular to a baseline at each location
*locs* from the baseline to *heads*, and places a marker there. For
vertical stem plots (the default), the *locs* are *x* positions, and
the *heads* are *y* values. For horizontal stem plots, the *locs* are
*y* positions, and the *heads* are *x* values.

Call signature::

stem([x,] y, linefmt=None, markerfmt=None, basefmt=None)
stem([locs,] heads, linefmt=None, markerfmt=None, basefmt=None)

The x-positions are optional. The formats may be provided either as
positional or as keyword-arguments.
The *locs*-positions are optional. The formats may be provided either
as positional or as keyword-arguments.

Parameters
----------
x : array-like, optional
The x-positions of the stems. Default: (0, 1, ..., len(y) - 1).
locs : array-like, default: (0, 1, ..., len(heads) - 1)
For vertical stem plots, the x-positions of the stems.
For horizontal stem plots, the y-positions of the stems.

y : array-like
The y-values of the stem heads.
heads : array-like
For vertical stem plots, the y-values of the stem heads.
For horizontal stem plots, the x-values of the stem heads.

linefmt : str, optional
A string defining the properties of the vertical lines. Usually,
Expand Down Expand Up @@ -2774,8 +2779,12 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
basefmt : str, default: 'C3-' ('C2-' in classic mode)
A format string defining the properties of the baseline.

orientation : str, default: 'vertical'
If 'vertical', will produce a plot with stems oriented vertically,
otherwise the stems will be oriented horizontally.

bottom : float, default: 0
The y-position of the baseline.
The y/x-position of the baseline (depending on orientation).

label : str, default: None
The label to use for the stems in legends.
Expand Down Expand Up @@ -2803,17 +2812,24 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
if not 1 <= len(args) <= 5:
raise TypeError('stem expected between 1 and 5 positional '
'arguments, got {}'.format(args))
cbook._check_in_list(['horizontal', 'vertical'],
orientation=orientation)

if len(args) == 1:
y, = args
x = np.arange(len(y))
heads, = args
locs = np.arange(len(heads))
args = ()
else:
x, y, *args = args
locs, heads, *args = args

self._process_unit_info(xdata=x, ydata=y)
x = self.convert_xunits(x)
y = self.convert_yunits(y)
if orientation == 'vertical':
self._process_unit_info(xdata=locs, ydata=heads)
locs = self.convert_xunits(locs)
heads = self.convert_yunits(heads)
else:
self._process_unit_info(xdata=heads, ydata=locs)
heads = self.convert_xunits(heads)
locs = self.convert_yunits(locs)

# defaults for formats
if linefmt is None:
Expand Down Expand Up @@ -2864,7 +2880,14 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,

# New behaviour in 3.1 is to use a LineCollection for the stemlines
if use_line_collection:
stemlines = [((xi, bottom), (xi, yi)) for xi, yi in zip(x, y)]
if orientation == 'horizontal':
stemlines = [
((bottom, loc), (head, loc))
for loc, head in zip(locs, heads)]
else:
stemlines = [
((loc, bottom), (loc, head))
for loc, head in zip(locs, heads)]
if linestyle is None:
linestyle = rcParams['lines.linestyle']
stemlines = mcoll.LineCollection(stemlines, linestyles=linestyle,
Expand All @@ -2874,16 +2897,34 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
# Old behaviour is to plot each of the lines individually
else:
stemlines = []
for xi, yi in zip(x, y):
l, = self.plot([xi, xi], [bottom, yi],
for loc, head in zip(locs, heads):
if orientation == 'horizontal':
xs = [bottom, head]
ys = [loc, loc]
else:
xs = [loc, loc]
ys = [bottom, head]
l, = self.plot(xs, ys,
color=linecolor, linestyle=linestyle,
marker=linemarker, label="_nolegend_")
stemlines.append(l)

markerline, = self.plot(x, y, color=markercolor, linestyle=markerstyle,
if orientation == 'horizontal':
marker_x = heads
marker_y = locs
baseline_x = [bottom, bottom]
baseline_y = [np.min(locs), np.max(locs)]
else:
marker_x = locs
marker_y = heads
baseline_x = [np.min(locs), np.max(locs)]
baseline_y = [bottom, bottom]

markerline, = self.plot(marker_x, marker_y,
color=markercolor, linestyle=markerstyle,
marker=markermarker, label="_nolegend_")

baseline, = self.plot([np.min(x), np.max(x)], [bottom, bottom],
baseline, = self.plot(baseline_x, baseline_y,
color=basecolor, linestyle=basestyle,
marker=basemarker, label="_nolegend_")

Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3018,11 +3018,13 @@ def stackplot(
@_copy_docstring_and_deprecators(Axes.stem)
def stem(
*args, linefmt=None, markerfmt=None, basefmt=None, bottom=0,
label=None, use_line_collection=True, data=None):
label=None, use_line_collection=True, orientation='vertical',
data=None):
return gca().stem(
*args, linefmt=linefmt, markerfmt=markerfmt, basefmt=basefmt,
bottom=bottom, label=label,
use_line_collection=use_line_collection,
orientation=orientation,
**({"data": data} if data is not None else {}))


Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 17 additions & 6 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3245,14 +3245,13 @@ def test_hist_stacked_weighted():
@image_comparison(['stem.png'], style='mpl20', remove_text=True)
def test_stem(use_line_collection):
x = np.linspace(0.1, 2 * np.pi, 100)
args = (x, np.cos(x))
# Label is a single space to force a legend to be drawn, but to avoid any
# text being drawn
kwargs = dict(linefmt='C2-.', markerfmt='k+', basefmt='C1-.',
label=' ', use_line_collection=use_line_collection)

fig, ax = plt.subplots()
ax.stem(*args, **kwargs)
# Label is a single space to force a legend to be drawn, but to avoid any
# text being drawn
ax.stem(x, np.cos(x),
linefmt='C2-.', markerfmt='k+', basefmt='C1-.', label=' ',
use_line_collection=use_line_collection)

ax.legend()

Expand All @@ -3279,6 +3278,18 @@ def test_stem_dates():
ax.stem(xs, ys, "*-")


@pytest.mark.parametrize("use_line_collection", [True, False],
ids=['w/ line collection', 'w/o line collection'])
@image_comparison(['stem_orientation.png'], style='mpl20', remove_text=True)
def test_stem_orientation(use_line_collection):
x = np.linspace(0.1, 2*np.pi, 50)

fig, ax = plt.subplots()
ax.stem(x, np.cos(x),
linefmt='C2-.', markerfmt='kx', basefmt='C1-.',
use_line_collection=use_line_collection, orientation='horizontal')


@image_comparison(['hist_stacked_stepfilled_alpha'])
def test_hist_stacked_stepfilled_alpha():
# make some data
Expand Down