diff --git a/doc/users/next_whats_new/rcParams[legend.loc]_supports_float_tuple.rst b/doc/users/next_whats_new/rcParams[legend.loc]_supports_float_tuple.rst new file mode 100644 index 000000000000..a83b91b52e34 --- /dev/null +++ b/doc/users/next_whats_new/rcParams[legend.loc]_supports_float_tuple.rst @@ -0,0 +1,5 @@ +``rcParams['legend.loc']`` now accepts float-tuple inputs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :rc:`legend.loc` rcParams now accepts float-tuple inputs, same as the *loc* keyword argument to `.Legend`. +This allows users to set the location of the legend in a more flexible and consistent way. diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index a9bebd209077..1cadc75273fb 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -718,6 +718,51 @@ def visit_Attribute(self, node): self.generic_visit(node) +# A validator dedicated to the named legend loc +_validate_named_legend_loc = ValidateInStrings( + 'legend.loc', + [ + "best", + "upper right", "upper left", "lower left", "lower right", "right", + "center left", "center right", "lower center", "upper center", + "center"], + ignorecase=True) + + +def _validate_legend_loc(loc): + """ + Confirm that loc is a type which rc.Params["legend.loc"] supports. + + .. versionadded:: 3.8 + + Parameters + ---------- + loc : str | int | (float, float) | str((float, float)) + The location of the legend. + + Returns + ------- + loc : str | int | (float, float) or raise ValueError exception + The location of the legend. + """ + if isinstance(loc, str): + try: + return _validate_named_legend_loc(loc) + except ValueError: + pass + try: + loc = ast.literal_eval(loc) + except (SyntaxError, ValueError): + pass + if isinstance(loc, int): + if 0 <= loc <= 10: + return loc + if isinstance(loc, tuple): + if len(loc) == 2 and all(isinstance(e, Real) for e in loc): + return loc + raise ValueError(f"{loc} is not a valid legend location.") + + def validate_cycler(s): """Return a Cycler object from a string repr or the object itself.""" if isinstance(s, str): @@ -1042,11 +1087,7 @@ def _convert_validator_spec(key, conv): # legend properties "legend.fancybox": validate_bool, - "legend.loc": _ignorecase([ - "best", - "upper right", "upper left", "lower left", "lower right", "right", - "center left", "center right", "lower center", "upper center", - "center"]), + "legend.loc": _validate_legend_loc, # the number of points in the legend line "legend.numpoints": validate_int, diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index bdaf5b593a5f..19f259c48756 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -1,7 +1,6 @@ import copy import os from pathlib import Path -import re import subprocess import sys from unittest import mock @@ -592,8 +591,33 @@ def test_deprecation(monkeypatch): # suppress_matplotlib_deprecation_warning, rather than any explicit check. -def test_rcparams_legend_loc(): - value = (0.9, .7) - match_str = f"{value} is not a valid value for legend.loc;" - with pytest.raises(ValueError, match=re.escape(match_str)): - mpl.RcParams({'legend.loc': value}) +@pytest.mark.parametrize("value", [ + "best", + 1, + "1", + (0.9, .7), + (-0.9, .7), + "(0.9, .7)" +]) +def test_rcparams_legend_loc(value): + # rcParams['legend.loc'] should allow any of the following formats. + # if any of these are not allowed, an exception will be raised + # test for gh issue #22338 + mpl.rcParams["legend.loc"] = value + + +@pytest.mark.parametrize("value", [ + "best", + 1, + (0.9, .7), + (-0.9, .7), +]) +def test_rcparams_legend_loc_from_file(tmpdir, value): + # rcParams['legend.loc'] should be settable from matplotlibrc. + # if any of these are not allowed, an exception will be raised. + # test for gh issue #22338 + rc_path = tmpdir.join("matplotlibrc") + rc_path.write(f"legend.loc: {value}") + + with mpl.rc_context(fname=rc_path): + assert mpl.rcParams["legend.loc"] == value