Skip to content

Commit 8117387

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

File tree

5 files changed

+67
-2
lines changed

5 files changed

+67
-2
lines changed
Lines changed: 11 additions & 0 deletions
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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,10 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
216216
Whether round edges should be enabled around the `.FancyBboxPatch` which
217217
makes up the legend's background.
218218
219-
shadow : bool, default: :rc:`legend.shadow`
219+
shadow : None, bool or dict, default: :rc:`legend.shadow`
220220
Whether to draw a shadow behind the legend.
221+
The shadow can be configured using `.Patch` keywords.
222+
Currently customization in :rc:`legend.shadow` not supported.
221223
222224
framealpha : float, default: :rc:`legend.framealpha`
223225
The alpha transparency of the legend's background.
@@ -480,6 +482,22 @@ def val_or_rc(val, rc_name):
480482
self._mode = mode
481483
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
482484

485+
# Figure out if self.shadow is valid
486+
# If shadow was None, rcParams loads False
487+
# So it shouldn't be None here
488+
489+
self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets
490+
if isinstance(self.shadow, dict):
491+
self._shadow_props.update(self.shadow)
492+
self.shadow = True
493+
elif self.shadow in (0, 1, True, False):
494+
self.shadow = bool(self.shadow)
495+
else:
496+
raise ValueError(
497+
'Legend shadow must be a dict or bool, not '
498+
f'{self.shadow!r} of type {type(self.shadow)}.'
499+
)
500+
483501
# We use FancyBboxPatch to draw a legend frame. The location
484502
# and size of the box will be updated during the drawing time.
485503

@@ -662,8 +680,11 @@ def draw(self, renderer):
662680
self.legendPatch.set_bounds(bbox.bounds)
663681
self.legendPatch.set_mutation_scale(fontsize)
664682

683+
# self.shadow is validated in __init__
684+
# So by here it is a bool and self._shadow_props contains any configs
685+
665686
if self.shadow:
666-
Shadow(self.legendPatch, 2, -2).draw(renderer)
687+
Shadow(self.legendPatch, **self._shadow_props).draw(renderer)
667688

668689
self.legendPatch.draw(renderer)
669690
self._legend_box.draw(renderer)

lib/matplotlib/rcsetup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,7 @@ def _convert_validator_spec(key, conv):
10591059
"legend.labelcolor": _validate_color_or_linecolor,
10601060
# the relative size of legend markers vs. original
10611061
"legend.markerscale": validate_float,
1062+
# using dict in rcParams not yet supported, so make sure it is bool
10621063
"legend.shadow": validate_bool,
10631064
# whether or not to draw a frame around legend
10641065
"legend.frameon": validate_bool,

lib/matplotlib/tests/test_legend.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,38 @@ def test_empty_bar_chart_with_legend():
532532
plt.legend()
533533

534534

535+
@image_comparison(['shadow_argument_types.png'], remove_text=True,
536+
style='mpl20')
537+
def test_shadow_argument_types():
538+
# Test that different arguments for shadow work as expected
539+
fig, ax = plt.subplots()
540+
ax.plot([1, 2, 3], label='test')
541+
542+
# Test various shadow configurations
543+
# as well as different ways of specifying colors
544+
legs = (ax.legend(loc='upper left', shadow=True), # True
545+
ax.legend(loc='upper right', shadow=False), # False
546+
ax.legend(loc='center left', # string
547+
shadow={'color': 'red', 'alpha': 0.1}),
548+
ax.legend(loc='center right', # tuple
549+
shadow={'color': (0.1, 0.2, 0.5), 'oy': -5}),
550+
ax.legend(loc='lower left', # tab
551+
shadow={'color': 'tab:cyan', 'ox': 10})
552+
)
553+
for l in legs:
554+
ax.add_artist(l)
555+
ax.legend(loc='lower right') # default
556+
557+
558+
def test_shadow_invalid_argument():
559+
# Test if invalid argument to legend shadow
560+
# (i.e. not [color|bool]) raises ValueError
561+
fig, ax = plt.subplots()
562+
ax.plot([1, 2, 3], label='test')
563+
with pytest.raises(ValueError, match="dict or bool"):
564+
ax.legend(loc="upper left", shadow="aardvark") # Bad argument
565+
566+
535567
def test_shadow_framealpha():
536568
# Test if framealpha is activated when shadow is True
537569
# and framealpha is not explicitly passed'''

0 commit comments

Comments
 (0)