diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 45f9319355e0..c19ac101deec 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -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) + return X, Y, Z + def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, vmax=None, lightsource=None, axlim_clip=False, **kwargs): """ @@ -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 @@ -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 @@ -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: @@ -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 diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 2a5593a641c9..410e0c0fc22d 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -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