From cb8042decde5aa679f8a80af02af8e799a717c73 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Fri, 22 May 2026 23:06:40 +0530 Subject: [PATCH 1/7] Fix 3D surface corruption with non-linear scales by masking invalid values --- lib/mpl_toolkits/mplot3d/axes3d.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 45f9319355e0..47a42ce3c943 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2452,6 +2452,12 @@ 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) + + # Mask out values that are invalid for the current scale + X = np.where(self.xaxis._scale.val_in_range(X), X, np.nan) + Y = np.where(self.yaxis._scale.val_in_range(Y), Y, np.nan) + Z = np.where(self.zaxis._scale.val_in_range(Z), Z, np.nan) + rows, cols = Z.shape has_stride = 'rstride' in kwargs or 'cstride' in kwargs From e175f8c0b8b1e21bf773fe7d5004dd691e069648 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Fri, 22 May 2026 23:31:41 +0530 Subject: [PATCH 2/7] Fix TypeError by vectorizing val_in_range array checks --- lib/mpl_toolkits/mplot3d/axes3d.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 47a42ce3c943..1f089598e35c 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2454,9 +2454,13 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, X, Y, Z = np.broadcast_arrays(X, Y, Z) # Mask out values that are invalid for the current scale - X = np.where(self.xaxis._scale.val_in_range(X), X, np.nan) - Y = np.where(self.yaxis._scale.val_in_range(Y), Y, np.nan) - Z = np.where(self.zaxis._scale.val_in_range(Z), Z, np.nan) + 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) rows, cols = Z.shape From a4f7f338376b0ba5515fdf7de617d7bc4347b8d1 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Fri, 22 May 2026 23:37:01 +0530 Subject: [PATCH 3/7] Fix W293 trailing whitespace on blank line --- lib/mpl_toolkits/mplot3d/axes3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 1f089598e35c..5a0b998ef19e 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2457,7 +2457,7 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, 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) From 6503bdc00beb19eecc5ede8b61a52fecf3775f80 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Sat, 23 May 2026 01:31:04 +0530 Subject: [PATCH 4/7] Refactor scale masking into helper function and add tests --- lib/mpl_toolkits/mplot3d/axes3d.py | 33 ++++++++++++------- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 21 ++++++++++++ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5a0b998ef19e..e5642a196041 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): """ @@ -2453,14 +2464,7 @@ 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) - # 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) + X, Y, Z = self._mask_invalid_scale_values(X, Y, Z) rows, cols = Z.shape @@ -2628,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 @@ -2780,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: @@ -2802,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..94aea45d960c 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -3189,3 +3189,24 @@ 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(): + import matplotlib.pyplot as plt + import numpy as np + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + # Create mock arrays with zeroes and negative values + X, Y = np.meshgrid(np.linspace(-1, 1, 5), np.linspace(-1, 1, 5)) + Z = X + Y + + ax.plot_surface(X, Y, Z) + + # Apply non-linear scales + ax.set_xscale('log') + ax.set_zscale('log') + + # Drawing forces evaluation and confirms the absence of crashes + fig.canvas.draw() From 3aab8480f08ea2389da1937a79705d10434e3134 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Sat, 23 May 2026 01:36:43 +0530 Subject: [PATCH 5/7] Fix W293 trailing whitespace across refactored files --- lib/mpl_toolkits/mplot3d/axes3d.py | 6 +++--- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index e5642a196041..c19ac101deec 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2375,7 +2375,7 @@ def _mask_invalid_scale_values(self, X, Y, Z): 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) @@ -2632,9 +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 diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 94aea45d960c..5358c53e38b7 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -3197,16 +3197,16 @@ def test_3d_log_scale_negative_masking(): fig = plt.figure() ax = fig.add_subplot(111, projection='3d') - + # Create mock arrays with zeroes and negative values X, Y = np.meshgrid(np.linspace(-1, 1, 5), np.linspace(-1, 1, 5)) Z = X + Y - + ax.plot_surface(X, Y, Z) - + # Apply non-linear scales ax.set_xscale('log') ax.set_zscale('log') - + # Drawing forces evaluation and confirms the absence of crashes fig.canvas.draw() From 3cab3324246b2adac95e1fd6b6c818af948ecbf4 Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Sat, 23 May 2026 01:48:51 +0530 Subject: [PATCH 6/7] Update 3D log scale test to assert dropped polygons --- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 5358c53e38b7..506ff2de0672 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -3192,21 +3192,20 @@ def test_scale3d_calc_coord(): def test_3d_log_scale_negative_masking(): - import matplotlib.pyplot as plt - import numpy as np - fig = plt.figure() ax = fig.add_subplot(111, projection='3d') - - # Create mock arrays with zeroes and negative values - X, Y = np.meshgrid(np.linspace(-1, 1, 5), np.linspace(-1, 1, 5)) - Z = X + Y - - ax.plot_surface(X, Y, Z) - - # Apply non-linear scales + + # Set scales BEFORE plotting so the masking logic is triggered ax.set_xscale('log') ax.set_zscale('log') - - # Drawing forces evaluation and confirms the absence of crashes - fig.canvas.draw() + + # 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 From acc5892b4e36739f85b5f13e6456b8f73341bcce Mon Sep 17 00:00:00 2001 From: Rahul Rathnavel K Date: Sat, 23 May 2026 01:51:46 +0530 Subject: [PATCH 7/7] Fix W293 trailing whitespace in 3D log scale test --- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 506ff2de0672..410e0c0fc22d 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -3194,17 +3194,17 @@ def test_scale3d_calc_coord(): 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.