Skip to content

Commit aba3cb1

Browse files
Add shadow coloring for legends and associated tests
Co-authored-by: Tranquilled <Tranquilled@users.noreply.github.com>
1 parent f1e0274 commit aba3cb1

File tree

5 files changed

+67
-2
lines changed

5 files changed

+67
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Configurable legend shadows
2+
-------------------------
3+
The *shadow* parameter of legends now accepts dicts in addition to booleans.
4+
Dictionaries can contain any keywords for `.patches.Patch`.
5+
This allows, for example, the color and/or the transparency of a legend shadow to be set:
6+
.. code-block:: python
7+
ax.legend(loc='center left', shadow={'color': 'red', 'alpha': 0.5})
8+
and better control of the shadow location:
9+
.. code-block:: python
10+
ax.legend(loc='center left', shadow={"ox":20, "oy":-20})
11+
Configuration is currently not supported via :rc:`legend.shadow`.

lib/matplotlib/legend.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,10 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
178178
Whether round edges should be enabled around the `.FancyBboxPatch` which
179179
makes up the legend's background.
180180
181-
shadow : bool, default: :rc:`legend.shadow`
181+
shadow : None, bool or dict, default: :rc:`legend.shadow`
182182
Whether to draw a shadow behind the legend.
183+
The shadow can be configured using `.Patch` keywords.
184+
Currently customization in :rc:`legend.shadow` not supported.
183185
184186
framealpha : float, default: :rc:`legend.framealpha`
185187
The alpha transparency of the legend's background.
@@ -558,6 +560,22 @@ def val_or_rc(val, rc_name):
558560
self._mode = mode
559561
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
560562

563+
# Figure out if self.shadow is valid
564+
# If shadow was None, rcParams loads False
565+
# So it shouldn't be None here
566+
567+
self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets
568+
if isinstance(self.shadow, dict):
569+
self._shadow_props.update(self.shadow)
570+
self.shadow = True
571+
elif self.shadow in (0, 1, True, False):
572+
self.shadow = bool(self.shadow)
573+
else:
574+
raise ValueError(
575+
'Legend shadow must be a dict or bool, not '
576+
f'{self.shadow!r} of type {type(self.shadow)}.'
577+
)
578+
561579
# We use FancyBboxPatch to draw a legend frame. The location
562580
# and size of the box will be updated during the drawing time.
563581

@@ -743,8 +761,11 @@ def draw(self, renderer):
743761
self.legendPatch.set_bounds(bbox.bounds)
744762
self.legendPatch.set_mutation_scale(fontsize)
745763

764+
# self.shadow is validated in __init__
765+
# So by here it is a bool and self._shadow_props contains any configs
766+
746767
if self.shadow:
747-
Shadow(self.legendPatch, 2, -2).draw(renderer)
768+
Shadow(self.legendPatch, **self._shadow_props).draw(renderer)
748769

749770
self.legendPatch.draw(renderer)
750771
self._legend_box.draw(renderer)

lib/matplotlib/rcsetup.py

+1
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,7 @@ def _convert_validator_spec(key, conv):
10781078
"legend.labelcolor": _validate_color_or_linecolor,
10791079
# the relative size of legend markers vs. original
10801080
"legend.markerscale": validate_float,
1081+
# using dict in rcParams not yet supported, so make sure it is bool
10811082
"legend.shadow": validate_bool,
10821083
# whether or not to draw a frame around legend
10831084
"legend.frameon": validate_bool,

lib/matplotlib/tests/test_legend.py

+32
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,38 @@ def test_empty_bar_chart_with_legend():
663663
plt.legend()
664664

665665

666+
@image_comparison(['shadow_argument_types.png'], remove_text=True,
667+
style='mpl20')
668+
def test_shadow_argument_types():
669+
# Test that different arguments for shadow work as expected
670+
fig, ax = plt.subplots()
671+
ax.plot([1, 2, 3], label='test')
672+
673+
# Test various shadow configurations
674+
# as well as different ways of specifying colors
675+
legs = (ax.legend(loc='upper left', shadow=True), # True
676+
ax.legend(loc='upper right', shadow=False), # False
677+
ax.legend(loc='center left', # string
678+
shadow={'color': 'red', 'alpha': 0.1}),
679+
ax.legend(loc='center right', # tuple
680+
shadow={'color': (0.1, 0.2, 0.5), 'oy': -5}),
681+
ax.legend(loc='lower left', # tab
682+
shadow={'color': 'tab:cyan', 'ox': 10})
683+
)
684+
for l in legs:
685+
ax.add_artist(l)
686+
ax.legend(loc='lower right') # default
687+
688+
689+
def test_shadow_invalid_argument():
690+
# Test if invalid argument to legend shadow
691+
# (i.e. not [color|bool]) raises ValueError
692+
fig, ax = plt.subplots()
693+
ax.plot([1, 2, 3], label='test')
694+
with pytest.raises(ValueError, match="dict or bool"):
695+
ax.legend(loc="upper left", shadow="aardvark") # Bad argument
696+
697+
666698
def test_shadow_framealpha():
667699
# Test if framealpha is activated when shadow is True
668700
# and framealpha is not explicitly passed'''

0 commit comments

Comments
 (0)