Skip to content

Commit 3ef2340

Browse files
fill_between extended to 3D
fill_between in plot types Apply suggestions from code review Co-authored-by: Tim Hoffmann <[email protected]> fill_between single_polygon flag 3D fill_between auto mode maps to polygon when all points lie on a x, y, or z plane 3D fill_between auto mode maps to polygon when all points lie on a x, y, or z plane Code review comments fill_between 3d shading fill_between 3d shading
1 parent 5d6acdf commit 3ef2340

File tree

11 files changed

+317
-32
lines changed

11 files changed

+317
-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: 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/axes3d.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,129 @@ 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 lines are in a plane parallel to a coordinate axis
1998+
(one of *x*, *y*, *z* are constant and equal for both lines),
1999+
'polygon' is used. Otherwise, 'quad' is used.
2000+
2001+
facecolors : list of :mpltype:`color`, default: None
2002+
Colors of each individual patch, or a single color to be used for
2003+
all patches.
2004+
2005+
shade : bool, default: None
2006+
Whether to shade the facecolors. If *None*, then defaults to *True*
2007+
for 'quad' mode and *False* for 'polygon' mode.
2008+
2009+
**kwargs
2010+
All other keyword arguments are passed on to `.Poly3DCollection`.
2011+
2012+
Returns
2013+
-------
2014+
`.Poly3DCollection`
2015+
A `.Poly3DCollection` containing the plotted polygons.
2016+
2017+
"""
2018+
_api.check_in_list(['auto', 'quad', 'polygon'], mode=mode)
2019+
2020+
had_data = self.has_data()
2021+
x1, y1, z1, x2, y2, z2 = cbook._broadcast_with_masks(x1, y1, z1, x2, y2, z2)
2022+
if mode == 'auto':
2023+
if ((np.all(x1 == x1[0]) and np.all(x2 == x1[0]))
2024+
or (np.all(y1 == y1[0]) and np.all(y2 == y1[0]))
2025+
or (np.all(z1 == z1[0]) and np.all(z2 == z1[0]))):
2026+
mode = 'polygon'
2027+
else:
2028+
mode = 'quad'
2029+
2030+
if shade is None:
2031+
if mode == 'quad':
2032+
shade = True
2033+
else:
2034+
shade = False
2035+
2036+
if facecolors is None:
2037+
facecolors = [self._get_patches_for_fill.get_next_color()]
2038+
facecolors = list(mcolors.to_rgba_array(facecolors))
2039+
2040+
if where is None:
2041+
where = True
2042+
else:
2043+
where = np.asarray(where, dtype=bool)
2044+
if where.size != x1.size:
2045+
raise ValueError(f"where size ({where.size}) does not match "
2046+
f"size ({x1.size})")
2047+
where = where & ~np.isnan(x1) # NaNs were broadcast in _broadcast_with_masks
2048+
2049+
polys = []
2050+
for idx0, idx1 in cbook.contiguous_regions(where):
2051+
x1i = x1[idx0:idx1]
2052+
y1i = y1[idx0:idx1]
2053+
z1i = z1[idx0:idx1]
2054+
x2i = x2[idx0:idx1]
2055+
y2i = y2[idx0:idx1]
2056+
z2i = z2[idx0:idx1]
2057+
2058+
if not len(x1i):
2059+
continue
2060+
2061+
if mode == 'quad':
2062+
# Preallocate the array for the region's vertices, and fill it in
2063+
n_polys_i = len(x1i) - 1
2064+
polys_i = np.empty((n_polys_i, 4, 3))
2065+
polys_i[:, 0, :] = np.column_stack((x1i[:-1], y1i[:-1], z1i[:-1]))
2066+
polys_i[:, 1, :] = np.column_stack((x1i[1:], y1i[1:], z1i[1:]))
2067+
polys_i[:, 2, :] = np.column_stack((x2i[1:], y2i[1:], z2i[1:]))
2068+
polys_i[:, 3, :] = np.column_stack((x2i[:-1], y2i[:-1], z2i[:-1]))
2069+
polys = polys + [*polys_i]
2070+
elif mode == 'polygon':
2071+
line1 = np.column_stack((x1i, y1i, z1i))
2072+
line2 = np.column_stack((x2i[::-1], y2i[::-1], z2i[::-1]))
2073+
poly = np.concatenate((line1, line2), axis=0)
2074+
polys.append(poly)
2075+
2076+
polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade,
2077+
**kwargs)
2078+
self.add_collection(polyc)
2079+
2080+
self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data)
2081+
return polyc
2082+
19602083
def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
19612084
vmax=None, lightsource=None, **kwargs):
19622085
"""
Loading
Loading

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

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

595595

596+
@mpl3d_image_comparison(['fill_between_quad.png'], style='mpl20')
597+
def test_fill_between_quad():
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+
# Since none of x1 == x2, y1 == y2, or z1 == z2 is True, the fill_between
614+
# mode will map to 'quad'
615+
ax.fill_between(x1, y1, z1, x2, y2, z2,
616+
where=where, mode='auto', alpha=0.5, edgecolor='k')
617+
618+
619+
@mpl3d_image_comparison(['fill_between_polygon.png'], style='mpl20')
620+
def test_fill_between_polygon():
621+
fig = plt.figure()
622+
ax = fig.add_subplot(projection='3d')
623+
624+
theta = np.linspace(0, 2*np.pi, 50)
625+
626+
x1 = x2 = theta
627+
y1 = y2 = 0
628+
z1 = np.cos(theta)
629+
z2 = z1 + 1
630+
631+
where = (theta < np.pi/2) | (theta > 3*np.pi/2)
632+
633+
# Since x1 == x2 and y1 == y2, the fill_between mode will be 'polygon'
634+
ax.fill_between(x1, y1, z1, x2, y2, z2,
635+
where=where, mode='auto', edgecolor='k')
636+
637+
596638
@mpl3d_image_comparison(['surface3d.png'], style='mpl20')
597639
def test_surface3d():
598640
# Remove this line when this test image is regenerated.

0 commit comments

Comments
 (0)