Skip to content

Commit d1607af

Browse files
fill_between extended to 3D
fill_between in plot types Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> fill_between single_polygon flag
1 parent b19794e commit d1607af

File tree

11 files changed

+286
-32
lines changed

11 files changed

+286
-32
lines changed

doc/api/toolkits/mplot3d/axes3d.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Plotting
3030
plot_surface
3131
plot_wireframe
3232
plot_trisurf
33+
fill_between
3334

3435
clabel
3536
contour
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Fill between 3D lines
2+
---------------------
3+
4+
The new method `.Axes3D.fill_between` allows to fill the surface between two
5+
3D lines with polygons.
6+
7+
.. plot::
8+
:include-source:
9+
:alt: Example of 3D fill_between
10+
11+
N = 50
12+
theta = np.linspace(0, 2*np.pi, N)
13+
14+
x1 = np.cos(theta)
15+
y1 = np.sin(theta)
16+
z1 = 0.1 * np.sin(6 * theta)
17+
18+
x2 = 0.6 * np.cos(theta)
19+
y2 = 0.6 * np.sin(theta)
20+
z2 = 2 # Note that scalar values work in addition to length N arrays
21+
22+
fig = plt.figure()
23+
ax = fig.add_subplot(projection='3d')
24+
ax.fill_between(x1, y1, z1, x2, y2, z2,
25+
alpha=0.5, edgecolor='k')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
=====================
3+
Fill between 3D lines
4+
=====================
5+
6+
Demonstrate how to fill the space between 3D lines with surfaces. Here we
7+
create a sort of "lampshade" shape.
8+
"""
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
N = 50
14+
theta = np.linspace(0, 2*np.pi, N)
15+
16+
x1 = np.cos(theta)
17+
y1 = np.sin(theta)
18+
z1 = 0.1 * np.sin(6 * theta)
19+
20+
x2 = 0.6 * np.cos(theta)
21+
y2 = 0.6 * np.sin(theta)
22+
z2 = 2 # Note that scalar values work in addition to length N arrays
23+
24+
fig = plt.figure()
25+
ax = fig.add_subplot(projection='3d')
26+
ax.fill_between(x1, y1, z1, x2, y2, z2, alpha=0.5, edgecolor='k')
27+
28+
plt.show()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
=========================
3+
Fill under 3D line graphs
4+
=========================
5+
6+
Demonstrate how to create polygons which fill the space under a line
7+
graph. In this example polygons are semi-transparent, creating a sort
8+
of 'jagged stained glass' effect.
9+
"""
10+
11+
import math
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
gamma = np.vectorize(math.gamma)
17+
N = 31
18+
x = np.linspace(0., 10., N)
19+
lambdas = range(1, 9)
20+
21+
ax = plt.figure().add_subplot(projection='3d')
22+
23+
facecolors = plt.colormaps['viridis_r'](np.linspace(0, 1, len(lambdas)))
24+
25+
for i, l in enumerate(lambdas):
26+
# Note fill_between can take coordinates as length N vectors, or scalars
27+
# single_polygon=True is set for faster and cleaner rendering for this
28+
# shape on a single plane.
29+
ax.fill_between(x, l, l**x * np.exp(-l) / gamma(x + 1),
30+
x, l, 0,
31+
single_polygon=True,
32+
facecolors=facecolors[i], alpha=.7)
33+
34+
ax.set(xlim=(0, 10), ylim=(1, 9), zlim=(0, 0.35),
35+
xlabel='x', ylabel=r'$\lambda$', zlabel='probability')
36+
37+
plt.show()

galleries/examples/mplot3d/polys3d.py

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,36 @@
11
"""
2-
=============================================
3-
Generate polygons to fill under 3D line graph
4-
=============================================
2+
====================
3+
Generate 3D polygons
4+
====================
55
6-
Demonstrate how to create polygons which fill the space under a line
7-
graph. In this example polygons are semi-transparent, creating a sort
8-
of 'jagged stained glass' effect.
6+
Demonstrate how to create polygons in 3D. Here we stack 3 hexagons.
97
"""
108

11-
import math
12-
139
import matplotlib.pyplot as plt
1410
import numpy as np
1511

16-
from matplotlib.collections import PolyCollection
17-
18-
# Fixing random state for reproducibility
19-
np.random.seed(19680801)
12+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
2013

14+
# Coordinates of a hexagon
15+
angles = np.linspace(0, 2 * np.pi, 6, endpoint=False)
16+
x = np.cos(angles)
17+
y = np.sin(angles)
18+
zs = [-3, -2, -1]
2119

22-
def polygon_under_graph(x, y):
23-
"""
24-
Construct the vertex list which defines the polygon filling the space under
25-
the (x, y) line graph. This assumes x is in ascending order.
26-
"""
27-
return [(x[0], 0.), *zip(x, y), (x[-1], 0.)]
20+
# Close the hexagon by repeating the first vertex
21+
x = np.append(x, x[0])
22+
y = np.append(y, y[0])
2823

24+
verts = []
25+
for z in zs:
26+
verts.append(list(zip(x*z, y*z, np.full_like(x, z))))
27+
verts = np.array(verts)
2928

3029
ax = plt.figure().add_subplot(projection='3d')
3130

32-
x = np.linspace(0., 10., 31)
33-
lambdas = range(1, 9)
34-
35-
# verts[i] is a list of (x, y) pairs defining polygon i.
36-
gamma = np.vectorize(math.gamma)
37-
verts = [polygon_under_graph(x, l**x * np.exp(-l) / gamma(x + 1))
38-
for l in lambdas]
39-
facecolors = plt.colormaps['viridis_r'](np.linspace(0, 1, len(verts)))
40-
41-
poly = PolyCollection(verts, facecolors=facecolors, alpha=.7)
42-
ax.add_collection3d(poly, zs=lambdas, zdir='y')
43-
44-
ax.set(xlim=(0, 10), ylim=(1, 9), zlim=(0, 0.35),
45-
xlabel='x', ylabel=r'$\lambda$', zlabel='probability')
31+
poly = Poly3DCollection(verts, alpha=.7)
32+
ax.add_collection3d(poly)
33+
ax.auto_scale_xyz(verts[:, :, 0], verts[:, :, 1], verts[:, :, 2])
34+
ax.set_aspect('equalxy')
4635

4736
plt.show()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
====================================
3+
fill_between(x1, y1, z1, x2, y2, z2)
4+
====================================
5+
6+
See `~mpl_toolkits.mplot3d.axes3d.Axes3D.fill_between`.
7+
"""
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
plt.style.use('_mpl-gallery')
12+
13+
# Make data
14+
n = 50
15+
theta = np.linspace(0, 2*np.pi, n)
16+
x1 = np.cos(theta)
17+
y1 = np.sin(theta)
18+
z1 = np.linspace(0, 1, n)
19+
x2 = np.cos(theta + np.pi)
20+
y2 = np.sin(theta + np.pi)
21+
z2 = z1
22+
23+
# Plot
24+
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
25+
ax.fill_between(x1, y1, z1, x2, y2, z2, alpha=0.5)
26+
ax.plot(x1, y1, z1, linewidth=2, color='C0')
27+
ax.plot(x2, y2, z2, linewidth=2, color='C0')
28+
29+
ax.set(xticklabels=[],
30+
yticklabels=[],
31+
zticklabels=[])
32+
33+
plt.show()

galleries/users_explain/toolkits/mplot3d.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ See `.Axes3D.contourf` for API documentation.
111111
The feature demoed in the second contourf3d example was enabled as a
112112
result of a bugfix for version 1.1.0.
113113

114+
.. _fillbetween3d:
115+
116+
Fill between 3D lines
117+
=====================
118+
See `.Axes3D.fill_between` for API documentation.
119+
120+
.. figure:: /gallery/mplot3d/images/sphx_glr_fillbetween3d_001.png
121+
:target: /gallery/mplot3d/fillbetween3d.html
122+
:align: center
123+
114124
.. _polygon3d:
115125

116126
Polygon plots

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,99 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs):
19571957

19581958
plot3D = plot
19591959

1960+
def fill_between(self, x1, y1, z1, x2, y2, z2, *,
1961+
where=None, single_polygon=False, **kwargs):
1962+
"""
1963+
Fill the area between two 3D curves.
1964+
1965+
The curves are defined by the points (*x1*, *y1*, *z1*) and
1966+
(*x2*, *y2*, *z2*). This creates one or multiple quadrangle
1967+
polygons that are filled. All points must be the same length N, or a
1968+
single value to be used for all points.
1969+
1970+
Parameters
1971+
----------
1972+
x1, y1, z1 : float or 1D array-like
1973+
x, y, and z coordinates of vertices for 1st line.
1974+
1975+
x2, y2, z2 : float or 1D array-like
1976+
x, y, and z coordinates of vertices for 2nd line.
1977+
1978+
where : array of bool (length N), optional
1979+
Define *where* to exclude some regions from being filled. The
1980+
filled regions are defined by the coordinates ``pts[where]``,
1981+
for all x, y, and z pts. More precisely, fill between ``pts[i]``
1982+
and ``pts[i+1]`` if ``where[i] and where[i+1]``. Note that this
1983+
definition implies that an isolated *True* value between two
1984+
*False* values in *where* will not result in filling. Both sides of
1985+
the *True* position remain unfilled due to the adjacent *False*
1986+
values.
1987+
1988+
single_polygon : bool
1989+
If True, then the 1st and 2nd lines are connected to form a single
1990+
polygon, which may be faster and render more cleanly for simple
1991+
shapes (eg, for filling between two lines that lie on a single
1992+
plane). If False (default), then a separate polygon is created for
1993+
each pair of subsequent points in the 1st and 2nd lines. This is
1994+
more flexible, but may be slower and result in rendering artifacts.
1995+
1996+
**kwargs
1997+
All other keyword arguments are passed on to `.Poly3DCollection`.
1998+
1999+
Returns
2000+
-------
2001+
`.Poly3DCollection`
2002+
A `.Poly3DCollection` containing the plotted polygons.
2003+
2004+
"""
2005+
had_data = self.has_data()
2006+
x1, y1, z1, x2, y2, z2 = cbook._broadcast_with_masks(x1, y1, z1, x2, y2, z2)
2007+
2008+
if where is None:
2009+
where = True
2010+
else:
2011+
where = np.asarray(where, dtype=bool)
2012+
if where.size != x1.size:
2013+
raise ValueError(f"where size ({where.size}) does not match "
2014+
f"size ({x1.size})")
2015+
where = where & ~np.isnan(x1) # NaNs were broadcast in _broadcast_with_masks
2016+
2017+
if single_polygon:
2018+
poly = []
2019+
for i in range(len(x1)):
2020+
if where[i]:
2021+
poly.append((x1[i], y1[i], z1[i]))
2022+
for i in range(len(x2) - 1, -1, -1):
2023+
if where[i]:
2024+
poly.append((x2[i], y2[i], z2[i]))
2025+
polys = [poly]
2026+
2027+
else:
2028+
polys = []
2029+
for idx0, idx1 in cbook.contiguous_regions(where):
2030+
x1slice = x1[idx0:idx1]
2031+
y1slice = y1[idx0:idx1]
2032+
z1slice = z1[idx0:idx1]
2033+
x2slice = x2[idx0:idx1]
2034+
y2slice = y2[idx0:idx1]
2035+
z2slice = z2[idx0:idx1]
2036+
2037+
if not len(x1slice):
2038+
continue
2039+
2040+
for i in range(len(x1slice) - 1):
2041+
poly = [(x1slice[i], y1slice[i], z1slice[i]),
2042+
(x1slice[i+1], y1slice[i+1], z1slice[i+1]),
2043+
(x2slice[i+1], y2slice[i+1], z2slice[i+1]),
2044+
(x2slice[i], y2slice[i], z2slice[i])]
2045+
polys.append(poly)
2046+
2047+
polyc = art3d.Poly3DCollection(polys, **kwargs)
2048+
self.add_collection(polyc)
2049+
2050+
self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data)
2051+
return polyc
2052+
19602053
def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
19612054
vmax=None, lightsource=None, **kwargs):
19622055
"""

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,44 @@ def test_plot_3d_from_2d():
593593
ax.plot(xs, ys, zs=0, zdir='y')
594594

595595

596+
@mpl3d_image_comparison(['fill_between_multi_polygon.png'], style='mpl20')
597+
def test_fill_between_multi_polygon():
598+
fig = plt.figure()
599+
ax = fig.add_subplot(projection='3d')
600+
601+
theta = np.linspace(0, 2*np.pi, 50)
602+
603+
x1 = np.cos(theta)
604+
y1 = np.sin(theta)
605+
z1 = 0.1 * np.sin(6 * theta)
606+
607+
x2 = 0.6 * np.cos(theta)
608+
y2 = 0.6 * np.sin(theta)
609+
z2 = 2
610+
611+
where = (theta < np.pi/2) | (theta > 3*np.pi/2)
612+
613+
ax.fill_between(x1, y1, z1, x2, y2, z2,
614+
where=where, alpha=0.5, edgecolor='k')
615+
616+
617+
@mpl3d_image_comparison(['fill_between_single_polygon.png'], style='mpl20')
618+
def test_fill_between_single_polygon():
619+
fig = plt.figure()
620+
ax = fig.add_subplot(projection='3d')
621+
622+
theta = np.linspace(0, 2*np.pi, 50)
623+
624+
x1 = x2 = theta
625+
y1 = y2 = 0
626+
z1 = np.cos(theta)
627+
z2 = 0
628+
629+
ax.fill_between(x1, y1, z1, x2, y2, z2,
630+
single_polygon=True,
631+
alpha=0.5, edgecolor='k')
632+
633+
596634
@mpl3d_image_comparison(['surface3d.png'], style='mpl20')
597635
def test_surface3d():
598636
# Remove this line when this test image is regenerated.

0 commit comments

Comments
 (0)