8000 fill_between extended to 3D · matplotlib/matplotlib@3ef2340 · GitHub
[go: up one dir, main page]

Skip to content

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3ef2340

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 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)
0