Skip to content

Commit 44e04d8

Browse files
committed
Place 3D contourfs midway between levels
1 parent 7c2a3c4 commit 44e04d8

File tree

5 files changed

+79
-12
lines changed

5 files changed

+79
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
3D contourf polygons placed between levels
2+
------------------------------------------
3+
The polygons used in a 3D `~mpl_toolkits.mplot3d.Axes3D.contourf` plot are
4+
now placed halfway between the contour levels, as each polygon represents the
5+
location of values that lie between two levels.

lib/matplotlib/contour.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,8 @@ def __init__(self, ax, *args,
813813
kwargs = self._process_args(*args, **kwargs)
814814
self._process_levels()
815815

816+
self._extend_min = self.extend in ['min', 'both']
817+
self._extend_max = self.extend in ['max', 'both']
816818
if self.colors is not None:
817819
ncolors = len(self.levels)
818820
if self.filled:
@@ -821,25 +823,27 @@ def __init__(self, ax, *args,
821823

822824
# Handle the case where colors are given for the extended
823825
# parts of the contour.
824-
extend_min = self.extend in ['min', 'both']
825-
extend_max = self.extend in ['max', 'both']
826+
826827
use_set_under_over = False
827828
# if we are extending the lower end, and we've been given enough
828829
# colors then skip the first color in the resulting cmap. For the
829830
# extend_max case we don't need to worry about passing more colors
830831
# than ncolors as ListedColormap will clip.
831-
total_levels = ncolors + int(extend_min) + int(extend_max)
832-
if len(self.colors) == total_levels and (extend_min or extend_max):
832+
total_levels = (ncolors +
833+
int(self._extend_min) +
834+
int(self._extend_max))
835+
if (len(self.colors) == total_levels and
836+
(self._extend_min or self._extend_max)):
833837
use_set_under_over = True
834-
if extend_min:
838+
if self._extend_min:
835839
i0 = 1
836840

837841
cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors)
838842

839843
if use_set_under_over:
840-
if extend_min:
844+
if self._extend_min:
841845
cmap.set_under(self.colors[0])
842-
if extend_max:
846+
if self._extend_max:
843847
cmap.set_over(self.colors[-1])
844848

845849
self.collections = cbook.silent_list(None)

lib/mpl_toolkits/mplot3d/axes3d.py

+35-5
Original file line numberDiff line numberDiff line change
@@ -2068,12 +2068,32 @@ def add_contour_set(
20682068
art3d.line_collection_2d_to_3d(linec, z, zdir=zdir)
20692069

20702070
def add_contourf_set(self, cset, zdir='z', offset=None):
2071+
self._add_contourf_set(cset, zdir=zdir, offset=offset)
2072+
2073+
def _add_contourf_set(self, cset, zdir='z', offset=None):
2074+
"""
2075+
Returns
2076+
-------
2077+
levels : numpy.ndarray
2078+
Levels at which the filled contours are added.
2079+
"""
20712080
zdir = '-' + zdir
2072-
for z, linec in zip(cset.levels, cset.collections):
2081+
2082+
midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2
2083+
# Linearly interpolate to get levels for any extensions
2084+
if cset._extend_min:
2085+
min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2
2086+
midpoints = np.insert(midpoints, 0, min_level)
2087+
if cset._extend_max:
2088+
max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2
2089+
midpoints = np.append(midpoints, max_level)
2090+
2091+
for z, linec in zip(midpoints, cset.collections):
20732092
if offset is not None:
20742093
z = offset
20752094
art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
20762095
linec.set_sort_zpos(z)
2096+
return midpoints
20772097

20782098
@_preprocess_data()
20792099
def contour(self, X, Y, Z, *args,
@@ -2168,6 +2188,16 @@ def tricontour(self, *args,
21682188
self.auto_scale_xyz(X, Y, Z, had_data)
21692189
return cset
21702190

2191+
def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data):
2192+
# Autoscale in the zdir based on the levels added, which are
2193+
# different from data range if any contour extensions are present
2194+
dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels}
2195+
# Input data and levels have different sizes, but auto_scale_xyz
2196+
# expected same-size input, so manually take min/max limits
2197+
limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim]))
2198+
for dim in ['x', 'y', 'z']]
2199+
self.auto_scale_xyz(*limits, had_data)
2200+
21712201
@_preprocess_data()
21722202
def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
21732203
"""
@@ -2195,9 +2225,9 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
21952225

21962226
jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
21972227
cset = super().contourf(jX, jY, jZ, *args, **kwargs)
2198-
self.add_contourf_set(cset, zdir, offset)
2228+
levels = self._add_contourf_set(cset, zdir, offset)
21992229

2200-
self.auto_scale_xyz(X, Y, Z, had_data)
2230+
self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
22012231
return cset
22022232

22032233
contourf3D = contourf
@@ -2246,9 +2276,9 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
22462276
tri = Triangulation(jX, jY, tri.triangles, tri.mask)
22472277

22482278
cset = super().tricontourf(tri, jZ, *args, **kwargs)
2249-
self.add_contourf_set(cset, zdir, offset)
2279+
levels = self._add_contourf_set(cset, zdir, offset)
22502280

2251-
self.auto_scale_xyz(X, Y, Z, had_data)
2281+
self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
22522282
return cset
22532283

22542284
def add_collection3d(self, col, zs=0, zdir='z'):

lib/mpl_toolkits/tests/test_mplot3d.py

+28
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,34 @@ def test_contourf3d_fill():
155155
ax.set_zlim(-1, 1)
156156

157157

158+
@pytest.mark.parametrize('extend, levels', [['both', [2, 4, 6]],
159+
['min', [2, 4, 6, 8]],
160+
['max', [0, 2, 4, 6]]])
161+
@check_figures_equal(extensions=["png"])
162+
def test_contourf3d_extend(fig_test, fig_ref, extend, levels):
163+
X, Y = np.meshgrid(np.arange(-2, 2, 0.25), np.arange(-2, 2, 0.25))
164+
# Z is in the range [0, 8]
165+
Z = X**2 + Y**2
166+
167+
# Manually set the over/under colors to be the end of the colormap
168+
cmap = plt.get_cmap('viridis').copy()
169+
cmap.set_under(cmap(0))
170+
cmap.set_over(cmap(255))
171+
# Set vmin/max to be the min/max values plotted on the reference image
172+
kwargs = {'vmin': 1, 'vmax': 7, 'cmap': cmap}
173+
174+
ax_ref = fig_ref.add_subplot(projection='3d')
175+
ax_ref.contourf(X, Y, Z, levels=[0, 2, 4, 6, 8], **kwargs)
176+
177+
ax_test = fig_test.add_subplot(projection='3d')
178+
ax_test.contourf(X, Y, Z, levels, extend=extend, **kwargs)
179+
180+
for ax in [ax_ref, ax_test]:
181+
ax.set_xlim(-2, 2)
182+
ax.set_ylim(-2, 2)
183+
ax.set_zlim(-10, 10)
184+
185+
158186
@mpl3d_image_comparison(['tricontour.png'], tol=0.02)
159187
def test_tricontour():
160188
fig = plt.figure()

0 commit comments

Comments
 (0)