diff --git a/doc/users/next_whats_new/legend_shadow_colors.rst b/doc/users/next_whats_new/legend_shadow_colors.rst new file mode 100644 index 000000000000..6d501dcf07fe --- /dev/null +++ b/doc/users/next_whats_new/legend_shadow_colors.rst @@ -0,0 +1,17 @@ +Configurable legend shadows +--------------------------- +The *shadow* parameter of legends now accepts dicts in addition to booleans. +Dictionaries can contain any keywords for `.patches.Patch`. +For example, this allows one to set the color and/or the transparency of a legend shadow: + +.. code-block:: python + + ax.legend(loc='center left', shadow={'color': 'red', 'alpha': 0.5}) + +and to control the shadow location: + +.. code-block:: python + + ax.legend(loc='center left', shadow={"ox":20, "oy":-20}) + +Configuration is currently not supported via :rc:`legend.shadow`. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index b0e50d67f9b7..90be3344e828 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -178,8 +178,10 @@ 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 : None, bool or dict, default: :rc:`legend.shadow` Whether to draw a shadow behind the legend. + The shadow can be configured using `.Patch` keywords. + Customization via :rc:`legend.shadow` is currently not supported. framealpha : float, default: :rc:`legend.framealpha` The alpha transparency of the legend's background. @@ -558,6 +560,22 @@ def val_or_rc(val, rc_name): self._mode = mode self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) + # Figure out if self.shadow is valid + # If shadow was None, rcParams loads False + # So it shouldn't be None here + + self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets + if isinstance(self.shadow, dict): + self._shadow_props.update(self.shadow) + self.shadow = True + elif self.shadow in (0, 1, True, False): + self.shadow = bool(self.shadow) + else: + raise ValueError( + 'Legend shadow must be a dict or bool, not ' + f'{self.shadow!r} of type {type(self.shadow)}.' + ) + # We use FancyBboxPatch to draw a legend frame. The location # and size of the box will be updated during the drawing time. @@ -743,8 +761,11 @@ def draw(self, renderer): self.legendPatch.set_bounds(bbox.bounds) self.legendPatch.set_mutation_scale(fontsize) + # self.shadow is validated in __init__ + # So by here it is a bool and self._shadow_props contains any configs + if self.shadow: - Shadow(self.legendPatch, 2, -2).draw(renderer) + Shadow(self.legendPatch, **self._shadow_props).draw(renderer) self.legendPatch.draw(renderer) self._legend_box.draw(renderer) diff --git a/lib/matplotlib/legend.pyi b/lib/matplotlib/legend.pyi index 77ef273766c2..a0b464b90b43 100644 --- a/lib/matplotlib/legend.pyi +++ b/lib/matplotlib/legend.pyi @@ -78,7 +78,7 @@ class Legend(Artist): ncols: int = ..., mode: Literal["expand"] | None = ..., fancybox: bool | None = ..., - shadow: bool | None = ..., + shadow: bool | dict[str, Any] | None = ..., title: str | None = ..., title_fontsize: float | None = ..., framealpha: float | None = ..., diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 663ff4b70536..f75ad21acc59 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1078,6 +1078,7 @@ def _convert_validator_spec(key, conv): "legend.labelcolor": _validate_color_or_linecolor, # the relative size of legend markers vs. original "legend.markerscale": validate_float, + # using dict in rcParams not yet supported, so make sure it is bool "legend.shadow": validate_bool, # whether or not to draw a frame around legend "legend.frameon": validate_bool, diff --git a/lib/matplotlib/tests/baseline_images/test_legend/shadow_argument_types.png b/lib/matplotlib/tests/baseline_images/test_legend/shadow_argument_types.png new file mode 100644 index 000000000000..c38699467d55 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_legend/shadow_argument_types.png differ diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index c94a0f5f6169..e66ed6bcdc0d 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -663,6 +663,38 @@ def test_empty_bar_chart_with_legend(): plt.legend() +@image_comparison(['shadow_argument_types.png'], remove_text=True, + style='mpl20') +def test_shadow_argument_types(): + # Test that different arguments for shadow work as expected + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='test') + + # Test various shadow configurations + # as well as different ways of specifying colors + legs = (ax.legend(loc='upper left', shadow=True), # True + ax.legend(loc='upper right', shadow=False), # False + ax.legend(loc='center left', # string + shadow={'color': 'red', 'alpha': 0.1}), + ax.legend(loc='center right', # tuple + shadow={'color': (0.1, 0.2, 0.5), 'oy': -5}), + ax.legend(loc='lower left', # tab + shadow={'color': 'tab:cyan', 'ox': 10}) + ) + for l in legs: + ax.add_artist(l) + ax.legend(loc='lower right') # default + + +def test_shadow_invalid_argument(): + # Test if invalid argument to legend shadow + # (i.e. not [color|bool]) raises ValueError + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='test') + with pytest.raises(ValueError, match="dict or bool"): + ax.legend(loc="upper left", shadow="aardvark") # Bad argument + + def test_shadow_framealpha(): # Test if framealpha is activated when shadow is True # and framealpha is not explicitly passed'''