diff --git a/doc/users/next_whats_new/new_color_spec_tuple.rst b/doc/users/next_whats_new/new_color_spec_tuple.rst new file mode 100644 index 000000000000..9f8d0ecabc3e --- /dev/null +++ b/doc/users/next_whats_new/new_color_spec_tuple.rst @@ -0,0 +1,21 @@ +Add a new valid color format ``(matplotlib_color, alpha)`` +---------------------------------------------------------- + + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib.patches import Rectangle + + fig, ax = plt.subplots() + + rectangle = Rectangle((.2, .2), .6, .6, + facecolor=('blue', 0.2), + edgecolor=('green', 0.5)) + ax.add_patch(rectangle) + + +Users can define a color using the new color specification, *(matplotlib_color, alpha)*. +Note that an explicit alpha keyword argument will override an alpha value from +*(matplotlib_color, alpha)*. diff --git a/galleries/examples/color/set_alpha.py b/galleries/examples/color/set_alpha.py new file mode 100644 index 000000000000..4130fe1109ef --- /dev/null +++ b/galleries/examples/color/set_alpha.py @@ -0,0 +1,53 @@ +""" +================================= +Ways to set a color's alpha value +================================= + +Compare setting alpha by the *alpha* keyword argument and by one of the Matplotlib color +formats. Often, the *alpha* keyword is the only tool needed to add transparency to a +color. In some cases, the *(matplotlib_color, alpha)* color format provides an easy way +to fine-tune the appearance of a Figure. + +""" + +import matplotlib.pyplot as plt +import numpy as np + +# Fixing random state for reproducibility. +np.random.seed(19680801) + +fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4)) + +x_values = [n for n in range(20)] +y_values = np.random.randn(20) + +facecolors = ['green' if y > 0 else 'red' for y in y_values] +edgecolors = facecolors + +ax1.bar(x_values, y_values, color=facecolors, edgecolor=edgecolors, alpha=0.5) +ax1.set_title("Explicit 'alpha' keyword value\nshared by all bars and edges") + + +# Normalize y values to get distinct face alpha values. +abs_y = [abs(y) for y in y_values] +face_alphas = [n / max(abs_y) for n in abs_y] +edge_alphas = [1 - alpha for alpha in face_alphas] + +colors_with_alphas = list(zip(facecolors, face_alphas)) +edgecolors_with_alphas = list(zip(edgecolors, edge_alphas)) + +ax2.bar(x_values, y_values, color=colors_with_alphas, + edgecolor=edgecolors_with_alphas) +ax2.set_title('Normalized alphas for\neach bar and each edge') + +plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.axes.Axes.bar` +# - `matplotlib.pyplot.subplots` diff --git a/galleries/users_explain/colors/colors.py b/galleries/users_explain/colors/colors.py index bd8cdb299a85..d308f0fb1859 100644 --- a/galleries/users_explain/colors/colors.py +++ b/galleries/users_explain/colors/colors.py @@ -68,6 +68,9 @@ | to black if cycle does not | | | include color. | | +--------------------------------------+--------------------------------------+ +| Tuple of one of the above color | - ``('green', 0.3)`` | +| formats and an alpha float. | - ``('#f00', 0.9)`` | ++--------------------------------------+--------------------------------------+ .. _xkcd color survey: https://xkcd.com/color/rgb/ diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 304eccca1bd7..d9a60afd0f3f 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -315,6 +315,13 @@ def _to_rgba_no_colorcycle(c, alpha=None): *alpha* is ignored for the color value ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``. """ + if isinstance(c, tuple) and len(c) == 2: + if alpha is None: + c, alpha = c + else: + c = c[0] + if alpha is not None and not 0 <= alpha <= 1: + raise ValueError("'alpha' must be between 0 and 1, inclusive") orig_c = c if c is np.ma.masked: return (0., 0., 0., 0.) @@ -425,6 +432,11 @@ def to_rgba_array(c, alpha=None): (n, 4) array of RGBA colors, where each channel (red, green, blue, alpha) can assume values between 0 and 1. """ + if isinstance(c, tuple) and len(c) == 2: + if alpha is None: + c, alpha = c + else: + c = c[0] # Special-case inputs that are already arrays, for performance. (If the # array has the wrong kind or shape, raise the error during one-at-a-time # conversion.) @@ -464,9 +476,12 @@ def to_rgba_array(c, alpha=None): return np.array([to_rgba(c, a) for a in alpha], float) else: return np.array([to_rgba(c, alpha)], float) - except (ValueError, TypeError): + except TypeError: pass - + except ValueError as e: + if e.args == ("'alpha' must be between 0 and 1, inclusive", ): + # ValueError is from _to_rgba_no_colorcycle(). + raise e if isinstance(c, str): raise ValueError(f"{c!r} is not a valid color value.") diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 14dd4ced77db..21e76bcd18b4 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1307,6 +1307,51 @@ def test_to_rgba_array_alpha_array(): assert_array_equal(c[:, 3], alpha) +def test_to_rgba_array_accepts_color_alpha_tuple(): + assert_array_equal( + mcolors.to_rgba_array(('black', 0.9)), + [[0, 0, 0, 0.9]]) + + +def test_to_rgba_array_explicit_alpha_overrides_tuple_alpha(): + assert_array_equal( + mcolors.to_rgba_array(('black', 0.9), alpha=0.5), + [[0, 0, 0, 0.5]]) + + +def test_to_rgba_array_accepts_color_alpha_tuple_with_multiple_colors(): + color_array = np.array([[1., 1., 1., 1.], [0., 0., 1., 0.]]) + assert_array_equal( + mcolors.to_rgba_array((color_array, 0.2)), + [[1., 1., 1., 0.2], [0., 0., 1., 0.2]]) + + color_sequence = [[1., 1., 1., 1.], [0., 0., 1., 0.]] + assert_array_equal( + mcolors.to_rgba_array((color_sequence, 0.4)), + [[1., 1., 1., 0.4], [0., 0., 1., 0.4]]) + + +def test_to_rgba_array_error_with_color_invalid_alpha_tuple(): + with pytest.raises(ValueError, match="'alpha' must be between 0 and 1,"): + mcolors.to_rgba_array(('black', 2.0)) + + +@pytest.mark.parametrize('rgba_alpha', + [('white', 0.5), ('#ffffff', 0.5), ('#ffffff00', 0.5), + ((1.0, 1.0, 1.0, 1.0), 0.5)]) +def test_to_rgba_accepts_color_alpha_tuple(rgba_alpha): + assert mcolors.to_rgba(rgba_alpha) == (1, 1, 1, 0.5) + + +def test_to_rgba_explicit_alpha_overrides_tuple_alpha(): + assert mcolors.to_rgba(('red', 0.1), alpha=0.9) == (1, 0, 0, 0.9) + + +def test_to_rgba_error_with_color_invalid_alpha_tuple(): + with pytest.raises(ValueError, match="'alpha' must be between 0 and 1"): + mcolors.to_rgba(('blue', 2.0)) + + def test_failed_conversions(): with pytest.raises(ValueError): mcolors.to_rgba('5')