Skip to content

Commit c49978d

Browse files
authored
Merge pull request matplotlib#18310 from QuLogic/stem3d
Add 3D stem plot
2 parents 86f2d7b + 823e605 commit c49978d

File tree

5 files changed

+219
-0
lines changed

5 files changed

+219
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Stem plots in 3D Axes
2+
---------------------
3+
4+
Stem plots are now supported on 3D Axes. Much like 2D stems,
5+
`~.axes3d.Axes3D.stem3D` supports plotting the stems in various orientations:
6+
7+
.. plot::
8+
9+
theta = np.linspace(0, 2*np.pi)
10+
x = np.cos(theta - np.pi/2)
11+
y = np.sin(theta - np.pi/2)
12+
z = theta
13+
directions = ['z', 'x', 'y']
14+
names = [r'$\theta$', r'$\cos\theta$', r'$\sin\theta$']
15+
16+
fig, axs = plt.subplots(1, 3, figsize=(8, 4),
17+
constrained_layout=True,
18+
subplot_kw={'projection': '3d'})
19+
for ax, zdir, name in zip(axs, directions, names):
20+
ax.stem(x, y, z, orientation=zdir)
21+
ax.set_title(name)
22+
fig.suptitle(r'A parametric circle: $(x, y) = (\cos\theta, \sin\theta)$')
23+
24+
See also the :doc:`/gallery/mplot3d/stem3d_demo` demo.

examples/mplot3d/stem3d_demo.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
=======
3+
3D stem
4+
=======
5+
6+
Demonstration of a stem plot in 3D, which plots vertical lines from a baseline
7+
to the *z*-coordinate and places a marker at the tip.
8+
"""
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
theta = np.linspace(0, 2*np.pi)
14+
x = np.cos(theta - np.pi/2)
15+
y = np.sin(theta - np.pi/2)
16+
z = theta
17+
18+
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
19+
ax.stem(x, y, z)
20+
21+
plt.show()
22+
23+
#############################################################################
24+
#
25+
# The position of the baseline can be adapted using *bottom*. The parameters
26+
# *linefmt*, *markerfmt*, and *basefmt* control basic format properties of the
27+
# plot. However, in contrast to `~.axes3d.Axes3D.plot` not all properties are
28+
# configurable via keyword arguments. For more advanced control adapt the line
29+
# objects returned by `~.stem3D`.
30+
31+
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
32+
markerline, stemlines, baseline = ax.stem(
33+
x, y, z, linefmt='grey', markerfmt='D', bottom=np.pi)
34+
markerline.set_markerfacecolor('none')
35+
36+
plt.show()
37+
38+
#############################################################################
39+
#
40+
# The orientation of the stems and baseline can be changed using *orientation*.
41+
# This determines in which direction the stems are projected from the head
42+
# points, towards the *bottom* baseline.
43+
#
44+
# For examples, by setting ``orientation='x'``, the stems are projected along
45+
# the *x*-direction, and the baseline is in the *yz*-plane.
46+
47+
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
48+
markerline, stemlines, baseline = ax.stem(x, y, z, bottom=-1, orientation='x')
49+
ax.set(xlabel='x', ylabel='y', zlabel='z')
50+
51+
plt.show()

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,6 +3351,121 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
33513351
batch.append(axis_bb)
33523352
return mtransforms.Bbox.union(batch)
33533353

3354+
def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-',
3355+
bottom=0, label=None, orientation='z'):
3356+
"""
3357+
Create a 3D stem plot.
3358+
3359+
A stem plot draws lines perpendicular to a baseline, and places markers
3360+
at the heads. By default, the baseline is defined by *x* and *y*, and
3361+
stems are drawn vertically from *bottom* to *z*.
3362+
3363+
Parameters
3364+
----------
3365+
x, y, z : array-like
3366+
The positions of the heads of the stems. The stems are drawn along
3367+
the *orientation*-direction from the baseline at *bottom* (in the
3368+
*orientation*-coordinate) to the heads. By default, the *x* and *y*
3369+
positions are used for the baseline and *z* for the head position,
3370+
but this can be changed by *orientation*.
3371+
3372+
linefmt : str, default: 'C0-'
3373+
A string defining the properties of the vertical lines. Usually,
3374+
this will be a color or a color and a linestyle:
3375+
3376+
========= =============
3377+
Character Line Style
3378+
========= =============
3379+
``'-'`` solid line
3380+
``'--'`` dashed line
3381+
``'-.'`` dash-dot line
3382+
``':'`` dotted line
3383+
========= =============
3384+
3385+
Note: While it is technically possible to specify valid formats
3386+
other than color or color and linestyle (e.g. 'rx' or '-.'), this
3387+
is beyond the intention of the method and will most likely not
3388+
result in a reasonable plot.
3389+
3390+
markerfmt : str, default: 'C0o'
3391+
A string defining the properties of the markers at the stem heads.
3392+
3393+
basefmt : str, default: 'C3-'
3394+
A format string defining the properties of the baseline.
3395+
3396+
bottom : float, default: 0
3397+
The position of the baseline, in *orientation*-coordinates.
3398+
3399+
label : str, default: None
3400+
The label to use for the stems in legends.
3401+
3402+
orientation : {'x', 'y', 'z'}, default: 'z'
3403+
The direction along which stems are drawn.
3404+
3405+
Returns
3406+
-------
3407+
`.StemContainer`
3408+
The container may be treated like a tuple
3409+
(*markerline*, *stemlines*, *baseline*)
3410+
3411+
Examples
3412+
--------
3413+
.. plot:: gallery/mplot3d/stem3d_demo.py
3414+
"""
3415+
3416+
from matplotlib.container import StemContainer
3417+
3418+
had_data = self.has_data()
3419+
3420+
_api.check_in_list(['x', 'y', 'z'], orientation=orientation)
3421+
3422+
xlim = (np.min(x), np.max(x))
3423+
ylim = (np.min(y), np.max(y))
3424+
zlim = (np.min(z), np.max(z))
3425+
3426+
# Determine the appropriate plane for the baseline and the direction of
3427+
# stemlines based on the value of orientation.
3428+
if orientation == 'x':
3429+
basex, basexlim = y, ylim
3430+
basey, baseylim = z, zlim
3431+
lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)]
3432+
for thisx, thisy, thisz in zip(x, y, z)]
3433+
elif orientation == 'y':
3434+
basex, basexlim = x, xlim
3435+
basey, baseylim = z, zlim
3436+
lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)]
3437+
for thisx, thisy, thisz in zip(x, y, z)]
3438+
else:
3439+
basex, basexlim = x, xlim
3440+
basey, baseylim = y, ylim
3441+
lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)]
3442+
for thisx, thisy, thisz in zip(x, y, z)]
3443+
3444+
# Determine style for stem lines.
3445+
linestyle, linemarker, linecolor = _process_plot_format(linefmt)
3446+
if linestyle is None:
3447+
linestyle = rcParams['lines.linestyle']
3448+
3449+
# Plot everything in required order.
3450+
baseline, = self.plot(basex, basey, basefmt, zs=bottom,
3451+
zdir=orientation, label='_nolegend_')
3452+
stemlines = art3d.Line3DCollection(
3453+
lines, linestyles=linestyle, colors=linecolor, label='_nolegend_')
3454+
self.add_collection(stemlines)
3455+
markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_')
3456+
3457+
stem_container = StemContainer((markerline, stemlines, baseline),
3458+
label=label)
3459+
self.add_container(stem_container)
3460+
3461+
jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom],
3462+
orientation)
3463+
self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data)
3464+
3465+
return stem_container
3466+
3467+
stem3D = stem
3468+
33543469
docstring.interpd.update(Axes3D_kwdoc=artist.kwdoc(Axes3D))
33553470
docstring.dedent_interpd(Axes3D.__init__)
33563471

231 KB
Loading

lib/mpl_toolkits/tests/test_mplot3d.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,35 @@ def test_errorbar3d():
12251225
ax.legend()
12261226

12271227

1228+
@image_comparison(['stem3d.png'], style='mpl20')
1229+
def test_stem3d():
1230+
fig, axs = plt.subplots(2, 3, figsize=(8, 6),
1231+
constrained_layout=True,
1232+
subplot_kw={'projection': '3d'})
1233+
1234+
theta = np.linspace(0, 2*np.pi)
1235+
x = np.cos(theta - np.pi/2)
1236+
y = np.sin(theta - np.pi/2)
1237+
z = theta
1238+
1239+
for ax, zdir in zip(axs[0], ['x', 'y', 'z']):
1240+
ax.stem(x, y, z, orientation=zdir)
1241+
ax.set_title(f'orientation={zdir}')
1242+
1243+
x = np.linspace(-np.pi/2, np.pi/2, 20)
1244+
y = np.ones_like(x)
1245+
z = np.cos(x)
1246+
1247+
for ax, zdir in zip(axs[1], ['x', 'y', 'z']):
1248+
markerline, stemlines, baseline = ax.stem(
1249+
x, y, z,
1250+
linefmt='C4-.', markerfmt='C1D', basefmt='C2',
1251+
orientation=zdir)
1252+
ax.set_title(f'orientation={zdir}')
1253+
markerline.set(markerfacecolor='none', markeredgewidth=2)
1254+
baseline.set_linewidth(3)
1255+
1256+
12281257
@image_comparison(["equal_box_aspect.png"], style="mpl20")
12291258
def test_equal_box_aspect():
12301259
from itertools import product, combinations

0 commit comments

Comments
 (0)