Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add new method Colormap.with_alpha() #29525

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

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/users/next_whats_new/colormap_with_alpha
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Tuning transparency of colormaps
--------------------------------
The new method `.Colormap.with_alpha` allows to create a new colormap with the same
color values but a new uniform alpha value. This is handy if you want to modify only
the transparency of mapped colors for an Artist.
19 changes: 19 additions & 0 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,25 @@
self._lut[self._i_over] = self._lut[self.N - 1]
self._lut[self._i_bad] = self._rgba_bad

def with_alpha(self, alpha):
"""
Return a copy of the colormap with a new uniform transparency.

Parameters
----------
alpha : float
The alpha blending value, between 0 (transparent) and 1 (opaque).
"""
if not isinstance(alpha, Real):
raise TypeError(f"'alpha' must be numeric or None, not {type(alpha)}")

Check warning on line 956 in lib/matplotlib/colors.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/colors.py#L956

Added line #L956 was not covered by tests
if not 0 <= alpha <= 1:
Copy link
Member

@jklymak jklymak Jan 28, 2025

Choose a reason for hiding this comment

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

I think this is going to return ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all() is someone inputs an array.

Suggested change
if not 0 <= alpha <= 1:
if (len(alpha) > 1) or (not 0 <= alpha <= 1)):

Copy link
Member Author

@timhoffm timhoffm Jan 28, 2025

Choose a reason for hiding this comment

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

Your observation on the error is correct, and to me this is intended. What's wrong with that? / What would you expect instead? I can't make sense of the suggestion, because it would raise on floats ("object of type 'float' has no len()").

Copy link
Member

@jklymak jklymak Jan 28, 2025

Choose a reason for hiding this comment

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

It is cryptic if someone passes an utterable iterable. You probably want something like:

if (isinstance(x, Iterable) and hasattr(x, "__len__") and len(alpha) > 1):
  raise(ValueError, 'alpha must be a single value, not a list or array')

Copy link
Member Author

Choose a reason for hiding this comment

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

Generally, we don't jump extra hoops to catch types that are not supported and give them a nicer error message.

But granted, Artist.set_alpha does something similar, so I've taken the same approach as there.

ValueError("'alpha' must be between 0 and 1, inclusive")

Check warning on line 958 in lib/matplotlib/colors.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/colors.py#L958

Added line #L958 was not covered by tests
new_cm = self.copy()
if not new_cm._isinit:
new_cm._init()
new_cm._lut[:, 3] = alpha
Copy link
Member

Choose a reason for hiding this comment

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

This has the unintended(?) consequence of allowing an array as well. If that array is the wrong size, folks are going to perhaps get cryptic size mismatch errors? Did we want to give thought to handling that, or just wait and see if it's a problem in real life? I'm actually unsure of how a user would get how big the lookup table is.

Copy link
Member Author

@timhoffm timhoffm Jan 26, 2025

Choose a reason for hiding this comment

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

Yes, I’m aware of this. I’ve intentionally specified this as float in the docstring and type annotation. Every other possibility accepted value is an implementation detail. I didn’t want to explicitly check for array and raise on that.

I’m also unsure how helpful a per-element alpha is. I assume varying alpha in a color map is less used compared to making the whole mapping semi-transparent.

Therefore, I’ve not included official varying alpha support for now. We can always later expand on it and also then decide whether the implementation just stays as is or whether we want additional safety checks and better error handling for arrays.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll chime in here and say that I sometimes do use varying alpha in colormaps. This is useful for hiding/washing-out less important features/regions. Making a linear ramp that goes from 0.2-> 1 or something like that along with the low-high values. You can do that here with: alpha=np.linspace(0.2, 1, cmap.N) (are we getting rid of N though?) You can also imagine wanting to make the center white region of the RdBu transparent instead of just white, so a triangular ramp is helpful on diverging colormaps.

Example I added to mplcairo where the rendering of varying alpha between patches is better than with Agg: https://github.com/matplotlib/mplcairo/blob/247583de20b94d5c1a0d773e720d64e741b122df/examples/quadmesh.py#L22-L26

All this to say, I think the varying alpha is a useful feature, and making it easier would be nice!

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure. I’d like to postpone this to a follow -up. This PR was motivated so that #29044 can be done without the need to regenerate reference images. I’m interested in getting this in quickly, so that #29044 can move forward.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you've already got a nice enough way of doing that here? I guess I'm thinking that it might be helpful to do as Jody mentioned and put some validation on alpha right away. Raise if it is not a float(s) between 0-1 or if it is of a different length than N.

We also have the colorbar set_alpha method, so might be worth also thinking about how this will play with that (I think that colorbar.set_alpha() still wins on the colorbar, but the collections would be mapped with the cmap alpha here)
https://matplotlib.org/stable/api/colorbar_api.html#matplotlib.colorbar.Colorbar.set_alpha

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we please focus on getting this through quickly as a minimal public API, so that #29044 can move forward?

I’m happy to discuss arrays, validation and Colorbar alpha in a follow-up.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure of the urgency here to avoid changing a visual test.

We have a continual problem with the color/alpha/facecolor/edgecolor dance - maybe this is the solution, but it doesn't seem right. OTOH, adding more ways to sculpt colormaps seems like a net positive regardless of the current issue that spurred this.

Copy link
Member Author

Choose a reason for hiding this comment

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

We have a continual problem with the color/alpha/facecolor/edgecolor dance - maybe this is the solution, but it doesn't seem right.

I don't understand the second half. If "this" means this PR, it's not supposed to solve any general problem with color/alpha/facecolor/edgecolor. This PR is a simple and straight forward extension of the Colormap functionality. It was motivated through a specific test in another PR that could better be written using this functionality. Not more not less.


I've added a range check for the float. I don't oversee whether array alpha is a meaningful API, in particular in the context of LinearSegementedColormap, where the lookup-table is basically an implementation detail. Therefore I want to keep things simple here.

return new_cm

def _init(self):
"""Generate the lookup table, ``self._lut``."""
raise NotImplementedError("Abstract class only")
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/colors.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class Colormap:
under: ColorType | None = ...,
over: ColorType | None = ...
) -> Colormap: ...
def with_alpha(self, alpha: float) -> Colormap: ...
def is_gray(self) -> bool: ...
def resampled(self, lutsize: int) -> Colormap: ...
def reversed(self, name: str | None = ...) -> Colormap: ...
Expand Down
13 changes: 12 additions & 1 deletion lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import matplotlib.scale as mscale
from matplotlib.rcsetup import cycler
from matplotlib.testing.decorators import image_comparison, check_figures_equal
from matplotlib.colors import is_color_like, to_rgba_array
from matplotlib.colors import is_color_like, to_rgba_array, ListedColormap


@pytest.mark.parametrize('N, result', [
Expand Down Expand Up @@ -248,6 +248,17 @@ def test_LinearSegmentedColormap_from_list_bad_under_over():
assert mcolors.same_color(cmap.get_over(), "y")


def test_colormap_with_alpha():
cmap = ListedColormap(["red", "green", ("blue", 0.8)])
cmap2 = cmap.with_alpha(0.5)
# color is the same:
vals = [0, 0.5, 1] # numeric positions that map to the listed colors
assert_array_equal(cmap(vals)[:, :3], cmap2(vals)[:, :3])
# alpha of cmap2 is changed:
assert_array_equal(cmap(vals)[:, 3], [1, 1, 0.8])
assert_array_equal(cmap2(vals)[:, 3], [0.5, 0.5, 0.5])


def test_BoundaryNorm():
"""
GitHub issue #1258: interpolation was failing with numpy
Expand Down