Skip to content
Closed
27 changes: 23 additions & 4 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,17 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *,
self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data)
return polyc

def _mask_invalid_scale_values(self, X, Y, Z):
"""Mask out values that are invalid for the current scale."""
val_in_range_X = np.vectorize(self.xaxis._scale.val_in_range, otypes=[bool])
val_in_range_Y = np.vectorize(self.yaxis._scale.val_in_range, otypes=[bool])
val_in_range_Z = np.vectorize(self.zaxis._scale.val_in_range, otypes=[bool])

X = np.where(val_in_range_X(X), X, np.nan)
Y = np.where(val_in_range_Y(Y), Y, np.nan)
Z = np.where(val_in_range_Z(Z), Z, np.nan)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now since we're working a bug fix and want to limit changes. Being able to pass numpy arrays into val_in_range will be more efficient in the future.

return X, Y, Z

def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
vmax=None, lightsource=None, axlim_clip=False, **kwargs):
"""
Expand Down Expand Up @@ -2452,6 +2463,9 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,

Z = cbook._to_unmasked_float_array(Z)
X, Y, Z = np.broadcast_arrays(X, Y, Z)

X, Y, Z = self._mask_invalid_scale_values(X, Y, Z)

rows, cols = Z.shape

has_stride = 'rstride' in kwargs or 'cstride' in kwargs
Expand Down Expand Up @@ -2618,6 +2632,9 @@ def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs):
raise ValueError("Argument Z must be 2-dimensional.")
# FIXME: Support masked arrays
X, Y, Z = np.broadcast_arrays(X, Y, Z)

X, Y, Z = self._mask_invalid_scale_values(X, Y, Z)

rows, cols = Z.shape

has_stride = 'rstride' in kwargs or 'cstride' in kwargs
Expand Down Expand Up @@ -2770,10 +2787,12 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
z, *args = args
z = np.asarray(z)

tx, ty, tz = self._mask_invalid_scale_values(tri.x, tri.y, z)

triangles = tri.get_masked_triangles()
xt = tri.x[triangles]
yt = tri.y[triangles]
zt = z[triangles]
xt = tx[triangles]
yt = ty[triangles]
zt = tz[triangles]
verts = np.stack((xt, yt, zt), axis=-1)

if cmap:
Expand All @@ -2792,7 +2811,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
facecolors=color, axlim_clip=axlim_clip, **kwargs)

self.add_collection(polyc, autolim="_datalim_only")
self.auto_scale_xyz(tri.x, tri.y, z, had_data)
self.auto_scale_xyz(tx, ty, tz, had_data)

return polyc

Expand Down
20 changes: 20 additions & 0 deletions lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -3189,3 +3189,23 @@ def test_scale3d_calc_coord():
# Pane coordinate should match axis limit (y-pane at max)
assert pane_idx == 1
assert point[pane_idx] == pytest.approx(ax.get_ylim()[1])


def test_3d_log_scale_negative_masking():
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

# Set scales BEFORE plotting so the masking logic is triggered
ax.set_xscale('log')
ax.set_zscale('log')

# A 3x3 grid normally yields 4 polygons total
X, Y = np.meshgrid(np.linspace(-1, 1, 3), np.linspace(-1, 1, 3))
Z = X + Y

surf = ax.plot_surface(X, Y, Z)

# On main (broken), no values are masked, so all 4 polygons are generated.
# On our branch (fixed), the negative/zero values are masked to NaN,
# and plot_surface gracefully drops the invalid polygons.
assert len(surf.get_paths()) < 4
Loading