-
-
Notifications
You must be signed in to change notification settings - Fork 8k
feat(3d): improve plot_surface shading logic #30424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2198,9 +2198,12 @@ | |
vmin, vmax : float, optional | ||
Bounds for the normalization. | ||
|
||
shade : bool, default: True | ||
Whether to shade the facecolors. Shading is always disabled when | ||
*cmap* is specified. | ||
shade : bool or "auto", default: "auto" | ||
Whether to shade the facecolors. "auto" will shade only if the facecolor is uniform, | ||
Check warning on line 2202 in lib/mpl_toolkits/mplot3d/axes3d.py
|
||
i.e. neither *cmap* nor *facecolors* is given. | ||
|
||
Furthermore, shading is generally not compatible with colormapping and | ||
``shade=True, cmap=...`` will raise an error. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is not an error being raised in this case AFAICT, so that should be added or this part of the docstring removed. My personal take is that we should not error, and instead warn that colors might not be as expected and still make the plot. @timhoffm thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggested this behavior in #30290 (comment). My argument is that that combination is fundamentally not reasonable because it distorts the mapped colors and thus breaks the purpose of colormapping. If this is our point of view, erroring out is appropriate - prohibit unreasonable things and not just warn that they are unreasonable. One can take another point of view that the depth-shading is important to get the 3d impression and exact coloring is less important (likely also depends on the colormap used - colormaps that mainly change hue should work better here). If we see this as a valid use case, I would not warn at all at runtime, because the user has to explicitly opt into the behavior by passing both arguments. We then only should add a note to the docstring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer the latter - there are use cases where colormapping is a visual aid rather than the only display of information, and the shading helps 3D interpretability. The one that I'm thinking about is coloring by Z value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds reasonable. |
||
|
||
lightsource : `~matplotlib.colors.LightSource`, optional | ||
The lightsource to use when *shade* is True. | ||
|
@@ -2251,8 +2254,10 @@ | |
fcolors = kwargs.pop('facecolors', None) | ||
|
||
cmap = kwargs.get('cmap', None) | ||
shade = kwargs.pop('shade', cmap is None) | ||
if shade is None: | ||
shade = kwargs.pop('shade', "auto") | ||
MengAiDev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if shade == "auto": | ||
shade = cmap is None and fcolors is None | ||
elif shade is None: | ||
raise ValueError("shade cannot be None.") | ||
scottshambaugh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
colset = [] # the sampled facecolor | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
""" | ||
Tests for plot_surface shade parameter behavior. | ||
""" | ||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
from matplotlib import cm | ||
from matplotlib.colors import Normalize | ||
|
||
|
||
def test_plot_surface_auto_shade_with_facecolors(): | ||
"""Test that plot_surface with facecolors uses shade=False by default.""" | ||
X = np.linspace(0, 1, 10) | ||
Y = np.linspace(0, 1, 10) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.cos((1-X_mesh) * np.pi) * np.cos((1-Y_mesh) * np.pi) * 1e+14 + 1.4e+15 | ||
Z_colors = np.cos(X_mesh * np.pi) | ||
|
||
norm = Normalize(vmin=np.min(Z_colors), vmax=np.max(Z_colors)) | ||
colors = cm.viridis(norm(Z_colors))[:-1, :-1] | ||
|
||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# Test that when facecolors is provided, shade defaults to False | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z, facecolors=colors, edgecolor='none') | ||
|
||
# We can't directly check shade attribute, but we can verify the plot works | ||
# and doesn't crash, which indicates our logic is working | ||
assert surf is not None | ||
scottshambaugh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
plt.close(fig) | ||
|
||
|
||
def test_plot_surface_auto_shade_without_facecolors(): | ||
"""Test that plot_surface without facecolors uses shade=True by default.""" | ||
X = np.linspace(0, 1, 10) | ||
Y = np.linspace(0, 1, 10) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.cos((1-X_mesh) * np.pi) * np.cos((1-Y_mesh) * np.pi) * 1e+14 + 1.4e+15 | ||
|
||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# Test that when no facecolors or cmap is provided, shade defaults to True | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z) | ||
|
||
assert surf is not None | ||
plt.close(fig) | ||
|
||
|
||
def test_plot_surface_auto_shade_with_cmap(): | ||
"""Test that plot_surface with cmap uses shade=False by default.""" | ||
X = np.linspace(0, 1, 10) | ||
Y = np.linspace(0, 1, 10) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.cos((1-X_mesh) * np.pi) * np.cos((1-Y_mesh) * np.pi) * 1e+14 + 1.4e+15 | ||
|
||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# Test that when cmap is provided, shade defaults to False | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z, cmap=cm.viridis) | ||
|
||
assert surf is not None | ||
plt.close(fig) | ||
|
||
|
||
def test_plot_surface_explicit_shade_with_facecolors(): | ||
"""Test that explicit shade parameter overrides auto behavior with facecolors.""" | ||
X = np.linspace(0, 1, 10) | ||
Y = np.linspace(0, 1, 10) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.cos((1-X_mesh) * np.pi) * np.cos((1-Y_mesh) * np.pi) * 1e+14 + 1.4e+15 | ||
Z_colors = np.cos(X_mesh * np.pi) | ||
|
||
norm = Normalize(vmin=np.min(Z_colors), vmax=np.max(Z_colors)) | ||
colors = cm.viridis(norm(Z_colors))[:-1, :-1] | ||
|
||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# Test that explicit shade=True works with facecolors | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z, facecolors=colors, shade=True) | ||
|
||
assert surf is not None | ||
plt.close(fig) | ||
|
||
|
||
def test_plot_surface_explicit_shade_false_without_facecolors(): | ||
"""Test that explicit shade=False overrides auto behavior without facecolors.""" | ||
X = np.linspace(0, 1, 10) | ||
Y = np.linspace(0, 1, 10) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.cos((1-X_mesh) * np.pi) * np.cos((1-Y_mesh) * np.pi) * 1e+14 + 1.4e+15 | ||
|
||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# Test that explicit shade=False works without facecolors | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z, shade=False) | ||
|
||
assert surf is not None | ||
plt.close(fig) | ||
|
||
|
||
def test_plot_surface_shade_auto_behavior_comprehensive(): | ||
"""Test the auto behavior logic comprehensively.""" | ||
X = np.linspace(0, 1, 5) | ||
Y = np.linspace(0, 1, 5) | ||
X_mesh, Y_mesh = np.meshgrid(X, Y) | ||
Z = np.ones_like(X_mesh) | ||
Z_colors = np.ones_like(X_mesh) | ||
colors = cm.viridis(Z_colors)[:-1, :-1] | ||
|
||
test_cases = [ | ||
# (kwargs, description) | ||
({}, "no facecolors, no cmap -> shade=True"), | ||
({'facecolors': colors}, "facecolors provided -> shade=False"), | ||
({'cmap': cm.viridis}, "cmap provided -> shade=False"), | ||
({'facecolors': colors, 'cmap': cm.viridis}, "both facecolors and cmap -> shade=False"), | ||
Check warning on line 119 in lib/mpl_toolkits/mplot3d/tests/test_plot_surface_shade.py
|
||
({'facecolors': colors, 'shade': True}, "explicit shade=True overrides auto"), | ||
({'facecolors': colors, 'shade': False}, "explicit shade=False overrides auto"), | ||
({}, "no parameters -> shade=True"), | ||
] | ||
|
||
for kwargs, description in test_cases: | ||
fig = plt.figure() | ||
ax = fig.add_subplot(111, projection='3d') | ||
|
||
# All these should work without crashing | ||
surf = ax.plot_surface(X_mesh, Y_mesh, Z, **kwargs) | ||
assert surf is not None, f"Failed: {description}" | ||
plt.close(fig) |
Uh oh!
There was an error while loading. Please reload this page.