8000 Merge pull request #28225 from scottshambaugh/3d_fill_between · matplotlib/matplotlib@167a26e · GitHub
[go: up one dir, main page]

Skip to content

Commit 167a26e

Browse files
authored
Merge pull request #28225 from scottshambaugh/3d_fill_between
[ENH]: fill_between extended to 3D
2 parents b3d29fb + dd05f32 commit 167a26e

File tree

13 files changed

+391
-33
lines changed

13 files changed

+391
-33
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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
ax.fill_between(x, l, l**x * np.exp(-l) / gamma(x + 1),
28+
x, l, 0,
29+
facecolors=facecolors[i], alpha=.7)
30+
31+
ax.set(xlim=(0, 10), ylim=(1, 9), zlim=(0, 0.35),
32+
xlabel='x', ylabel=r'$\lambda$', zlabel='probability')
33+
34+
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 for a double helix
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/art3d.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,47 @@ def _zalpha(colors, zs):
11851185
return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
11861186

11871187

1188+
def _all_points_on_plane(xs, ys, zs, atol=1e-8):
1189+
"""
1190+
Check if all points are on the same plane. Note that NaN values are
1191+
ignored.
1192+
1193+
Parameters
1194+
----------
1195+
xs, ys, zs : array-like
1196+
The x, y, and z coordinates of the points.
1197+
atol : float, default: 1e-8
1198+
The tolerance for the equality check.
1199+
"""
1200+
xs, ys, zs = np.asarray(xs), np.asarray(ys), np.asarray(zs)
1201+
points = np.column_stack([xs, ys, zs])
1202+
points = points[~np.isnan(points).any(axis=1)]
1203+
# Check for the case where we have less than 3 unique points
1204+
points = np.unique(points, axis=0)
1205+
if len(points) <= 3:
1206+
return True
1207+
# Calculate the vectors from the first point to all other points
1208+
vs = (points - points[0])[1:]
1209+
vs = vs / np.linalg.norm(vs, axis=1)[:, np.newaxis]
1210+
# Filter out parallel vectors
1211+
vs = np.unique(vs, axis=0)
1212+
if len(vs) <= 2:
1213+
return True
1214+
# Filter out parallel and antiparallel vectors to the first vector
1215+
cross_norms = np.linalg.norm(np.cross(vs[0], vs[1:]), axis=1)
1216+
zero_cross_norms = np.where(np.isclose(cross_norms, 0, atol=atol))[0] + 1
1217+
vs = np.delete(vs, zero_cross_norms, axis=0)
1218+
if len(vs) <= 2:
1219+
return True
1220+
# Calculate the normal vector from the first three points
1221+
n = np.cross(vs[0], vs[1])
1222+
n = n / np.linalg.norm(n)
1223+
# If the dot product of the normal vector and all other vectors is zero,
1224+
# all points are on the same plane
1225+
dots = np.dot(n, vs.transpose())
1226+
return np.allclose(dots, 0, atol=atol)
1227+
1228+
11881229
def _generate_normals(polygons):
11891230
"""
11901231
Compute the normals of a list of polygons, one normal per polygon.

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,130 @@ 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, mode='auto', facecolors=None, shade=None,
1962+
**kwargs):
1963+
"""
1964+
Fill the area between two 3D curves.
1965+
1966+
The curves are defined by the points (*x1*, *y1*, *z1*) and
1967+
(*x2*, *y2*, *z2*). This creates one or multiple quadrangle
1968+
polygons that are filled. All points must be the same length N, or a
1969+
single value to be used for all points.
1970+
1971+
Parameters
1972+
----------
1973+
x1, y1, z1 : float or 1D array-like
1974+
x, y, and z coordinates of vertices for 1st line.
1975+
1976+
x2, y2, z2 : float or 1D array-like
1977+
x, y, and z coordinates of vertices for 2nd line.
1978+
1979+
where : array of bool (length N), optional
1980+
Define *where* to exclude some regions from being filled. The
1981+
filled regions are defined by the coordinates ``pts[where]``,
1982+
for all x, y, and z pts. More precisely, fill between ``pts[i]``
1983+
and ``pts[i+1]`` if ``where[i] and where[i+1]``. Note that this
1984+
definition implies that an isolated *True* value between two
1985+
*False* values in *where* will not result in filling. Both sides of
1986+
the *True* position remain unfilled due to the adjacent *False*
1987+
values.
1988+
1989+
mode : {'quad', 'polygon', 'auto'}, default: 'auto'
1990+
The fill mode. One of:
1991+
1992+
- 'quad': A separate quadrilateral polygon is created for each
1993+
pair of subsequent points in the two lines.
1994+
- 'polygon': The two lines are connected to form a single polygon.
1995+
This is faster and can render more cleanly for simple shapes
1996+
(e.g. for filling between two lines that lie within a plane).
1997+
- 'auto': If the points all lie on the same 3D plane, 'polygon' is
1998+
used. Otherwise, 'quad' is used.
1999+
2000+
facecolors : list of :mpltype:`color`, default: None
2001+
Colors of each individual patch, or a single color to be used for
2002+
all patches.
2003+
2004+
shade : bool, default: None
2005+
Whether to shade the facecolors. If *None*, then defaults to *True*
2006+
for 'quad' mode and *False* for 'polygon' mode.
2007+
2008+
**kwargs
2009+
All other keyword arguments are passed on to `.Poly3DCollection`.
2010+
2011+
Returns
2012+
-------
2013+
`.Poly3DCollection`
2014+
A `.Poly3DCollection` containing the plotted polygons.
2015+
2016+
"""
2017+
_api.check_in_list(['auto', 'quad', 'polygon'], mode=mode)
2018+
2019+
had_data = self.has_data()
2020+
x1, y1, z1, x2, y2, z2 = cbook._broadcast_with_masks(x1, y1, z1, x2, y2, z2)
2021+
2022+
if facecolors is None:
2023+
facecolors = [self._get_patches_for_fill.get_next_color()]
2024+
facecolors = list(mcolors.to_rgba_array(facecolors))
2025+
2026+
if where is None:
2027+
where = True
2028+
else:
2029+
where = np.asarray(where, dtype=bool)
2030+
if where.size != x1.size:
2031+
raise ValueError(f"where size ({where.size}) does not match "
2032+
f"size ({x1.size})")
2033+
where = where & ~np.isnan(x1) # NaNs were broadcast in _broadcast_with_masks
2034+
2035+
if mode == 'auto':
2036+
if art3d._all_points_on_plane(np.concatenate((x1[where], x2[where])),
2037+
np.concatenate((y1[where], y2[where])),
2038+
np.concatenate((z1[where], z2[where])),
2039+
atol=1e-12):
2040+
mode = 'polygon'
2041+
else:
2042+
mode = 'quad'
2043+
2044+
if shade is None:
2045+
if mode == 'quad':
2046+
shade = True
2047+
else:
2048+
shade = False
2049+
2050+
polys = []
2051+
for idx0, idx1 in cbook.contiguous_regions(where):
2052+
x1i = x1[idx0:idx1]
2053+
y1i = y1[idx0:idx1]
2054+
z1i = z1[idx0:idx1]
2055+
x2i = x2[idx0:idx1]
2056+
y2i = y2[idx0:idx1]
2057+
z2i = z2[idx0:idx1]
2058+
2059+
if not len(x1i):
2060+
continue
2061+
2062+
if mode == 'quad':
2063+
# Preallocate the array for the region's vertices, and fill it in
2064+
n_polys_i = len(x1i) - 1
2065+
polys_i = np.empty((n_polys_i, 4, 3))
2066+
polys_i[:, 0, :] = np.column_stack((x1i[:-1], y1i[:-1], z1i[:-1]))
2067+
polys_i[:, 1, :] = np.column_stack((x1i[1:], y1i[1:], z1i[1:]))
2068+
polys_i[:, 2, :] = np.column_stack((x2i[1:], y2i[1:], z2i[1:]))
2069+
polys_i[:, 3, :] = np.column_stack((x2i[:-1], y2i[:-1], z2i[:-1]))
2070+
polys = polys + [*polys_i]
2071+
elif mode == 'polygon':
2072+
line1 = np.column_stack((x1i, y1i, z1i))
2073+
line2 = np.column_stack((x2i[::-1], y2i[::-1], z2i[::-1]))
2074+
poly = np.concatenate((line1, line2), axis=0)
2075+
polys.append(poly)
2076+
2077+
polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade,
2078+
**kwargs)
2079+
self.add_collection(polyc)
2080+
2081+
self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data)
2082+
return polyc
2083+
19602084
def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
19612085
vmax=None, lightsource=None, **kwargs):
19622086
"""
Loading
Loading

0 commit comments

Comments
 (0)
0