Skip to content

ENH: Introduce compass-notation for legend #12679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions doc/users/next_whats_new/comapss_notation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
:orphan:

Compass notation for legend and other anchored artists
------------------------------------------------------

The ``loc`` parameter for legends and other anchored artists now accepts
"compass" strings. E.g. to locate such element in the upper right corner,
in addition to ``'upper right'`` and ``1``, you can now use ``'NE'`` as
well as ``'northeast'``. This satisfies the wish for more intuitive and
unambiguous location of legends. The following (case-sensitive) location
specifications are now allowed.

============ ============== =============== =============
Compass Code Compass String Location String Location Code
============ ============== =============== =============
.. 'best' 0
'NE' 'northeast' 'upper right' 1
'NW' 'northwest' 'upper left' 2
'SW' 'southwest' 'lower left' 3
'SE' 'southeast' 'lower right' 4
.. 'right' 5
'W' 'west' 'center left' 6
'E' 'east' 'center right' 7
'S' 'south' 'lower center' 8
'N' 'north' 'upper center' 9
'C' 'center' 'center' 10
============ ============== =============== =============

Those apply to

* the axes legends; `matplotlib.pyplot.legend` and
`matplotlib.axes.Axes.legend`,

and, with the exception of ``'best'`` and ``0``, to

* the figure legends; `matplotlib.pyplot.figlegend` and
`matplotlib.figure.Figure.legend`, as well as the general
`matplotlib.legend.Legend` class,
* the `matplotlib.offsetbox`'s `matplotlib.offsetbox.AnchoredOffsetbox` and
`matplotlib.offsetbox.AnchoredText`,
* the `mpl_toolkits.axes_grid1.anchored_artists`'s
`~.AnchoredDrawingArea`, `~.AnchoredAuxTransformBox`,
`~.AnchoredEllipse`, `~.AnchoredSizeBar`, `~.AnchoredDirectionArrows`
* the `mpl_toolkits.axes_grid1.inset_locator`'s
`~.axes_grid1.inset_locator.inset_axes`,
`~.axes_grid1.inset_locator.zoomed_inset_axes` and the
`~.axes_grid1.inset_locator.AnchoredSizeLocator` and
`~.axes_grid1.inset_locator.AnchoredZoomLocator`

Note that those new compass strings *do not* apply to ``table``.


Getter/setter for legend and other anchored artists location
------------------------------------------------------------

The above mentioned classes (in particular `~.legend.Legend`,
`~.offsetbox.AnchoredOffsetbox`, `~.offsetbox.AnchoredText` etc.)
now have a getter/setter for the location.
This allows to e.g. change the location *after* creating a legend::

legend = ax.legend(loc="west")
legend.set_loc("southeast")
83 changes: 83 additions & 0 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2156,3 +2156,86 @@ def _check_in_list(values, **kwargs):
raise ValueError(
"{!r} is not a valid value for {}; supported values are {}"
.format(v, k, ', '.join(map(repr, values))))


# Theses are the valid compass notation codes used internally by legend
# and other anchored boxes.
_COMPASS_LOCS = ['NE', 'NW', 'SW', 'SE', 'E', 'W', 'S', 'N', 'C']


def _map_loc_to_compass(loc, **kwargs):
"""
Map a location (string) to a compass notation string. This is used by
AnchoredOffsetbox and Legend.

loc : A location, like 'upper right', 'NE', 'northeast', 1
allowtuple : bool, Whether to allow a tuple of numbers like (0.5, 0.2).
This is useful for legends; other artists may not allow this.
allowbest : bool, Whether to allow for 'best' or 0 as input for loc.
warnonly : bool, if True, warn on invalid input and use fallback,
if False, error out.
fallback : The fallback return value in case of warnonly=True.
asrcparam : string, Use if this function is used as validator for rcParams
"""
codes = {
'upper right': 'NE', 'northeast': 'NE', 1: 'NE',
'upper left': 'NW', 'northwest': 'NW', 2: 'NW',
'lower left': 'SW', 'southwest': 'SW', 3: 'SW',
'lower right': 'SE', 'southeast': 'SE', 4: 'SE',
'right': 'E', 5: 'E',
'center left': 'W', 'west': 'W', 6: 'W',
'center right': 'E', 'east': 'E', 7: 'E',
'lower center': 'S', 'south': 'S', 8: 'S',
'upper center': 'N', 'north': 'N', 9: 'N',
'center': 'C', 10: 'C'
}

allowtuple = kwargs.get("allowtuple", False)
allowbest = kwargs.get("allowbest", False)
fallback = kwargs.get("fallback", 'NE')
warnonly = kwargs.get("warnonly", False)
asrcparam = kwargs.get("asrcparam", None)

if allowbest:
codes.update({'best': 'best', 0: 'best'})

if loc in _COMPASS_LOCS:
return loc

if isinstance(loc, str) or isinstance(loc, int):
if loc in codes:
return codes[loc]

if allowtuple:
if hasattr(loc, '__len__') and len(loc) == 2:
x, y = loc[0], loc[1]
if isinstance(x, numbers.Number) and isinstance(y, numbers.Number):
return tuple((x, y))

msg = "Unrecognized location {!r}. ".format(loc)
if asrcparam:
msg += "This is no valid rc parameter for {!r}. ".format(asrcparam)
if isinstance(loc, str):
if loc.lower() in codes:
fallback = codes[loc.lower()]
elif loc.upper() in _COMPASS_LOCS:
fallback = loc.upper()
msg += "Location strings are now case-sensitive. "
if warnonly:
msg += "Falling back on {!r}. ".format(fallback)
if not allowbest and loc in [0, 'best']:
msg += "Automatic legend placement (loc='best') is not "
msg += "implemented for figure legends or other artists. "
vcodes = [k for k in codes if not isinstance(k, int)] + _COMPASS_LOCS
msg += "Valid locations are '{}'".format("', '".join(vcodes))
if not asrcparam:
startn = 0 if allowbest else 1
msg += " as well as the numbers {} to 10. ".format(startn)
if allowtuple:
msg += " In addition a tuple (x,y) of coordinates can be supplied."
if warnonly:
msg += " This will raise an exception %(removal)s."
warn_deprecated("3.1", message=msg)
return fallback
else:
raise ValueError(msg)
Loading