diff --git a/doc/api/next_api_changes/behavior/18668-LL.rst b/doc/api/next_api_changes/behavior/18668-LL.rst new file mode 100644 index 000000000000..fa9bd076e126 --- /dev/null +++ b/doc/api/next_api_changes/behavior/18668-LL.rst @@ -0,0 +1,8 @@ +Shadow parameter for Legend can be colorlike or bool +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *shadow* parameter can now be any colorlike in addition to +a bool. If it is neither, a ValueError is thrown. + +An accessory validation function `~.rcsetup.validate_color_or_bool` +has also been added, which preferentially returns a color. \ No newline at end of file diff --git a/doc/users/next_whats_new/explicit_shadow_color.rst b/doc/users/next_whats_new/explicit_shadow_color.rst new file mode 100644 index 000000000000..79cd53ad586e --- /dev/null +++ b/doc/users/next_whats_new/explicit_shadow_color.rst @@ -0,0 +1,12 @@ +Legend shadow colors can be explicitly defined +---------------------------------------------- + +The *shadow* parameter for the Legend constructor now +accepts both color and bool. If it is a bool, the +behavior is exactly the same as before. +Any colorlike value sets the shadow to that color, +subject to the same transparency effect that the default +facecolor undergoes. + +An accessory validation function `~.rcsetup.validate_color_or_bool` +has also been added, which preferentially returns a color. \ No newline at end of file diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index ff557816a865..efa7b241deec 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -31,6 +31,7 @@ from matplotlib import _api, docstring, colors from matplotlib.artist import Artist, allow_rasterization from matplotlib.cbook import silent_list +from matplotlib.colors import is_color_like from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, @@ -208,8 +209,9 @@ def _update_bbox_to_anchor(self, loc_in_canvas): Whether round edges should be enabled around the `~.FancyBboxPatch` which makes up the legend's background. -shadow : bool, default: :rc:`legend.shadow` +shadow : bool or color, default: :rc:`legend.shadow` Whether to draw a shadow behind the legend. + If value is a color, a shadow of that color will be applied. framealpha : float, default: :rc:`legend.framealpha` The alpha transparency of the legend's background. @@ -514,7 +516,6 @@ def __init__(self, parent, handles, labels, self._draggable = None # set the text color - color_getters = { # getter function depends on line or patch 'linecolor': ['get_color', 'get_facecolor'], 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'], @@ -607,8 +608,14 @@ def draw(self, renderer): self.legendPatch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height) self.legendPatch.set_mutation_scale(fontsize) - if self.shadow: + if is_color_like(self.shadow): + Shadow(self.legendPatch, 2, -2, color=self.shadow).draw(renderer) + elif self.shadow is True: Shadow(self.legendPatch, 2, -2).draw(renderer) + elif self.shadow is False: + pass + else: + raise ValueError('Shadow must be a valid color or bool.') self.legendPatch.draw(renderer) self._legend_box.draw(renderer) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e53dc2c27eb1..9e46b8a8df15 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -146,6 +146,18 @@ def validate_bool(b): raise ValueError('Could not convert "%s" to bool' % b) +def validate_color_or_bool(c): + """Convert c to color (preferentially) or bool""" + try: + return validate_color(c) + except ValueError: + pass + try: + return validate_bool(c) + except ValueError: + raise ValueError(f'Could not convert {c} to color or bool.') + + @_api.deprecated("3.3") def validate_bool_maybe_none(b): """Convert b to ``bool`` or raise, passing through *None*.""" @@ -1229,7 +1241,7 @@ def _convert_validator_spec(key, conv): "legend.title_fontsize": validate_fontsize_None, # the relative size of legend markers vs. original "legend.markerscale": validate_float, - "legend.shadow": validate_bool, + "legend.shadow": validate_color_or_bool, # whether or not to draw a frame around legend "legend.frameon": validate_bool, # alpha value of the legend frame diff --git a/lib/matplotlib/tests/baseline_images/test_legend/shadow_color.svg b/lib/matplotlib/tests/baseline_images/test_legend/shadow_color.svg new file mode 100644 index 000000000000..ae83624289a5 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_legend/shadow_color.svg @@ -0,0 +1,875 @@ + + + + + + + + 2020-10-31T15:11:46.903485 + image/svg+xml + + + Matplotlib v3.3.2.post1260.dev0+gf6a929c25, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index cebf26ea066e..2fd516985a88 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -508,6 +508,33 @@ def test_shadow_framealpha(): assert leg.get_frame().get_alpha() == 1 +@image_comparison(['shadow_color.svg']) +def test_shadow_color(): + # Test whether a shadow of the appropriate color + # is generated when shadow's value is colorlike + fig, ax = plt.subplots() + ax.plot(range(100), label="test") + legends = { + 'true': plt.legend(loc="upper left", shadow=True), + 'false': plt.legend(loc="center left", shadow=False), + 'default': plt.legend(loc="lower left"), + 'colorstring': plt.legend(loc="upper right", shadow='red'), + 'colortuple': plt.legend(loc="center right", shadow=(0.1, 0.2, 0.5)), + 'colortab': plt.legend(loc="lower right", shadow='tab:cyan') + } + for x in legends: + plt.gca().add_artist(legends[x]) + + +def test_shadow_bad_type(): + # Ensure an error is thrown if the value of shadow + # is neither colorlike nor bool + fig, ax = plt.subplots() + ax.plot(range(100), label="test") + with pytest.raises(ValueError, match="color or bool"): + ax.legend(loc="upper left", shadow="aardvark") + + def test_legend_title_empty(): # test that if we don't set the legend title, that # it comes back as an empty string, and that it is not diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 4705b975d60a..66b11e2fc92c 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -19,6 +19,7 @@ validate_bool_maybe_none, validate_color, validate_colorlist, + validate_color_or_bool, validate_cycler, validate_float, validate_fontweight, @@ -338,6 +339,32 @@ def generate_validator_testcases(valid): ('(0, 1, "0.5")', ValueError), # last one not a float ), }, + {'validator': validate_color_or_bool, + 'success': (('None', 'none'), + ('none', 'none'), + ('AABBCC', '#AABBCC'), # RGB hex code + ('AABBCC00', '#AABBCC00'), # RGBA hex code + ('tab:blue', 'tab:blue'), # named color + ('C12', 'C12'), # color from cycle + ('(0, 1, 0)', (0.0, 1.0, 0.0)), # RGB tuple + ((0, 1, 0), (0, 1, 0)), # non-string version + ('(0, 1, 0, 1)', (0.0, 1.0, 0.0, 1.0)), # RGBA tuple + ((0, 1, 0, 1), (0, 1, 0, 1)), # non-string version + *((_, True) for _ in + ('t', 'yes', 'on', 'true', 1, True)), + *((_, False) for _ in + ('f', 'n', 'no', 'off', 'false', 0, False)), + ), + 'fail': (('tab:veryblue', ValueError), # invalid name + ('(0, 1)', ValueError), # tuple with length < 3 + ('(0, 1, 0, 1, 0)', ValueError), # tuple with length > 4 + ('(0, 1, none)', ValueError), # cannot cast none to float + ('(0, 1, "0.5")', ValueError), # last one not a float + (2, ValueError), + (-1, ValueError), + ([], ValueError), + ), + }, {'validator': validate_hist_bins, 'success': (('auto', 'auto'), ('fd', 'fd'),