Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 75670d0

Browse files
loc short uppercase, longlowercase
1 parent 2e0c8c5 commit 75670d0

File tree

11 files changed

+419
-253
lines changed

11 files changed

+419
-253
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
:orphan:
2+
3+
Compass notation for legend and other anchored artists
4+
------------------------------------------------------
5+
6+
The ``loc`` parameter for legends and other anchored artists now accepts
7+
"compass" strings. E.g. to locate such element in the upper right corner,
8+
in addition to ``'upper right'`` and ``1``, you can now use ``'NE'`` as
9+
well as ``'northeast'``. This satisfies the wish for more intuitive and
10+
unambiguous location of legends. The following (case-sensitive) location
11+
specifications are now allowed.
12+
13+
============ ============== =============== =============
14+
Compass Code Compass String Location String Location Code
15+
============ ============== =============== =============
16+
.. 'best' 0
17+
'NE' 'northeast' 'upper right' 1
18+
'NW' 'northwest' 'upper left' 2
19+
'SW' 'southwest' 'lower left' 3
20+
'SE' 'southeast' 'lower right' 4
21+
.. 'right' 5
22+
'W' 'west' 'center left' 6
23+
'E' 'east' 'center right' 7
24+
'S' 'south' 'lower center' 8
25+
'N' 'north' 'upper center' 9
26+
'C' 'center' 'center' 10
27+
============ ============== =============== =============
28+
29+
Those apply to
30+
31+
* the legends; `matplotlib.pyplot.legend`, `matplotlib.axes.Axes.legend`,
32+
`matplotlib.legend.Legend`, `matplotlib.pyplot.figlegend`,
33+
`matplotlib.figure.Figure.legend`,
34+
35+
and, with the exception of ``'best'`` and ``0``, to
36+
37+
* the `matplotlib.offsetbox`'s `matplotlib.offsetbox.AnchoredOffsetbox` and
38+
`matplotlib.offsetbox.AnchoredText`,
39+
* the `mpl_toolkits.axes_grid1.anchored_artists`'s
40+
`~.AnchoredDrawingArea`, `~.AnchoredAuxTransformBox`,
41+
`~.AnchoredEllipse`, `~.AnchoredSizeBar`, `~.AnchoredDirectionArrows`
42+
* the `mpl_toolkits.axes_grid1.inset_locator`'s
43+
`~.axes_grid1.inset_locator.inset_axes`,
44+
`~.axes_grid1.inset_locator.zoomed_inset_axes` and the
45+
`~.axes_grid1.inset_locator.AnchoredSizeLocator` and
46+
`~.axes_grid1.inset_locator.AnchoredZoomLocator`
47+
48+
Note that those new compass strings *do not* apply to ``table``.
49+
50+
The above mentioned classes also now have a getter/setter for the location.
51+
This allows to e.g. change the location *after* creating a legend::
52+
53+
legend = ax.legend(loc="west")
54+
legend.set_loc("southeast")

lib/matplotlib/cbook/__init__.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,3 +2075,90 @@ def _check_and_log_subprocess(command, logger, **kwargs):
20752075
.format(command, exc.output.decode('utf-8')))
20762076
logger.debug(report)
20772077
return report
2078+
2079+
2080+
def _check_not_matrix(**kwargs):
2081+
"""
2082+
If any value in *kwargs* is a `np.matrix`, raise a TypeError with the key
2083+
name in its message.
2084+
"""
2085+
for k, v in kwargs.items():
2086+
if isinstance(v, np.matrix):
2087+
raise TypeError(f"Argument {k!r} cannot be a np.matrix")
2088+
2089+
2090+
def _check_in_list(values, **kwargs):
2091+
"""
2092+
For each *key, value* pair in *kwargs*, check that *value* is in *values*;
2093+
if not, raise an appropriate ValueError.
2094+
2095+
Examples
2096+
--------
2097+
>>> cbook._check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg)
2098+
"""
2099+
for k, v in kwargs.items():
2100+
if v not in values:
2101+
raise ValueError(
2102+
"{!r} is not a valid value for {}; supported values are {}"
2103+
.format(v, k, ', '.join(map(repr, values))))
2104+
2105+
2106+
_valid_compass = ['NE','NW', 'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C']
2107+
2108+
def _map_loc_to_compass(loc, allowtuple=False, allowbest=False,
2109+
fallback='NE', warnonly=True):
2110+
"""
2111+
Map a location string to a compass notation string. This is used by
2112+
AnchoredOffsetbox and Legend.
2113+
"""
2114+
codes = {
2115+
'upper right': 'NE', 'northeast': 'NE', 1: 'NE',
2116+
'upper left': 'NW', 'northwest': 'NW', 2: 'NW',
2117+
'lower left': 'SW', 'southwest': 'SW', 3: 'SW',
2118+
'lower right': 'SE', 'southeast': 'SE', 4: 'SE',
2119+
'right': 'E', 5: 'E',
2120+
'center left': 'W', 'west': 'W', 6: 'W',
2121+
'center right': 'E', 'east': 'E', 7: 'E',
2122+
'lower center': 'S', 'south': 'S', 8: 'S',
2123+
'upper center': 'N', 'north': 'N', 9: 'N',
2124+
'center': 'C', 10: 'C'
2125+
}
2126+
2127+
if allowbest:
2128+
codes.update({'best': 'best', 0: 'best'})
2129+
2130+
if loc in _valid_compass:
2131+
return loc
2132+
2133+
if isinstance(loc, str) or isinstance(loc, int):
2134+
if loc in codes:
2135+
return codes[loc]
2136+
2137+
if allowtuple:
2138+
if hasattr(loc, '__len__') and len(loc) == 2:
2139+
x, y = loc[0], loc[1]
2140+
if isinstance(x, numbers.Number) and isinstance(y, numbers.Number):
2141+
return tuple((x,y))
2142+
2143+
msg = "Unrecognized location {}. ".format(loc)
2144+
if isinstance(loc, str):
2145+
if loc.lower() in codes:
2146+
fallback = codes[loc.lower()]
2147+
elif loc.upper() in _valid_compass:
2148+
fallback = loc.upper()
2149+
msg += "Location strings are now case-sensitive. "
2150+
if warnonly:
2151+
msg += "Falling back on {}. ".format(fallback)
2152+
if not allowbest and loc in [0, 'best']:
2153+
msg += "Note that automatic legend placement (loc='best') is not "
2154+
msg += "implemented for figure legends or other artists. "
2155+
vcodes = [k for k in codes if not isinstance(k,int)] + _valid_compass
2156+
msg += "Valid locations are\n\t{}\n".format('\n\t'.join(vcodes))
2157+
msg += " as well as the numbers {} to 10.\n".format(0 if allowbest else 1)
2158+
if warnonly:
2159+
msg += "This will raise an exception %(removal)s."
2160+
if warnonly:
2161+
warn_deprecated("3.1", message=msg)
2162+
return fallback
2163+
else:
2164+
raise ValueError(msg)

lib/matplotlib/legend.py

Lines changed: 55 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626

2727
import numpy as np
2828

29+
2930
from matplotlib import rcParams
3031
from matplotlib import cbook, docstring
3132
from matplotlib.artist import Artist, allow_rasterization
32-
from matplotlib.cbook import silent_list, is_hashable, warn_deprecated
33+
from matplotlib.cbook import (silent_list, is_hashable, warn_deprecated,
34+
_valid_compass, _map_loc_to_compass)
3335
from matplotlib.font_manager import FontProperties
3436
from matplotlib.lines import Line2D
3537
from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
@@ -113,23 +115,23 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
113115
loc : int or string or pair of floats, default: :rc:`legend.loc` ('best' for \
114116
axes, 'upper right' for figures)
115117
The location of the legend.
116-
Possible (case-insensitive) strings and codes are:
117-
118-
=============== ============== =============
119-
Location String Compass String Location Code
120-
=============== ============== =============
121-
'best' 0
122-
'upper right' 'NE' 1
123-
'upper left' 'NW' 2
124-
'lower left' 'SW' 3
125-
'lower right' 'SE' 4
126-
'right' 5
127-
'center left' 'W' 6
128-
'center right' 'E' 7
129-
'lower center' 'S' 8
130-
'upper center' 'N' 9
131-
'center' 'C' 10
132-
=============== ============== =============
118+
Possible (case-sensitive) strings and codes are:
119+
120+
============ ============== =============== =============
121+
Compass Code Compass String Location String Location Code
122+
============ ============== =============== =============
123+
.. 'best' 0
124+
'NE' 'northeast' 'upper right' 1
125+
'NW' 'northwest' 'upper left' 2
126+
'SW' 'southwest' 'lower left' 3
127+
'SE' 'southeast' 'lower right' 4
128+
.. 'right' 5
129+
'W' 'west' 'center left' 6
130+
'E' 'east' 'center right' 7
131+
'S' 'south' 'lower center' 8
132+
'N' 'north' 'upper center' 9
133+
'C' 'center' 'center' 10
134+
============ ============== =============== =============
133135
134136
135137
Alternatively can be a 2-tuple giving ``x, y`` of the lower-left
@@ -309,21 +311,7 @@ class Legend(Artist):
309311
Place a legend on the axes at location loc.
310312
311313
"""
312-
codes = {'best': 0, # only implemented for axes legends
313-
'upper right': 1,
314-
'upper left': 2,
315-
'lower left': 3,
316-
'lower right': 4,
317-
'right': 5,
318-
'center left': 6,
319-
'center right': 7,
320-
'lower center': 8,
321-
'upper center': 9,
322-
'center': 10,
323-
}
324-
compasscodes = {'nw': 2, 'n': 9, 'ne': 1, 'w': 6, 'c': 10, 'e': 7,
325-
'sw': 3, 's': 8, 'se': 4}
326-
allcodes = {**codes, **compasscodes}
314+
327315
zorder = 5
328316

329317
def __str__(self):
@@ -487,35 +475,6 @@ def __init__(self, parent, handles, labels,
487475
raise TypeError("Legend needs either Axes or Figure as parent")
488476
self.parent = parent
489477

490-
if loc is None:
491-
loc = rcParams["legend.loc"]
492-
if not self.isaxes and loc in [0, 'best']:
493-
loc = 'upper right'
494-
if isinstance(loc, str):
495-
if loc.lower() not in self.allcodes:
496-
if self.isaxes:
497-
cbook.warn_deprecated(
498-
"3.1", message="Unrecognized location {!r}. Falling "
499-
"back on 'best'; valid locations are\n\t{}\n"
500-
"This will raise an exception %(removal)s."
501-
.format(loc, '\n\t'.join(self.allcodes)))
502-
loc = 0
503-
else:
504-
cbook.warn_deprecated(
505-
"3.1", message="Unrecognized location {!r}. Falling "
506-
"back on 'upper right'; valid locations are\n\t{}\n'"
507-
"This will raise an exception %(removal)s."
508-
.format(loc, '\n\t'.join(self.allcodes)))
509-
loc = 1
510-
else:
511-
loc = self.allcodes[loc.lower()]
512-
if not self.isaxes and loc == 0:
513-
cbook.warn_deprecated(
514-
"3.1", message="Automatic legend placement (loc='best') not "
515-
"implemented for figure legend. Falling back on 'upper "
516-
"right'. This will raise an exception %(removal)s.")
517-
loc = 1
518-
519478
self._mode = mode
520479
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
521480

@@ -561,6 +520,9 @@ def __init__(self, parent, handles, labels,
561520
# init with null renderer
562521
self._init_legend_box(handles, labels, markerfirst)
563522

523+
# location must be set after _legend_box is created.
524+
self._loc = loc
525+
564526
# If shadow is activated use framealpha if not
565527
# explicitly passed. See Issue 8943
566528
if framealpha is None:
@@ -571,7 +533,6 @@ def __init__(self, parent, handles, labels,
571533
else:
572534
self.get_frame().set_alpha(framealpha)
573535

574-
self._loc = loc
575536
# figure out title fontsize:
576537
if title_fontsize is None:
577538
title_fontsize = rcParams['legend.title_fontsize']
@@ -592,6 +553,16 @@ def _set_artist_props(self, a):
592553
a.set_transform(self.get_transform())
593554

594555
def _set_loc(self, loc):
556+
if loc is None:
557+
loc = rcParams["legend.loc"]
558+
if not self.isaxes and loc in [0, 'best']:
559+
loc = 'NE'
560+
if self.isaxes:
561+
loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=True,
562+
fallback="best", warnonly=True)
563+
else:
564+
loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=False,
565+
fallback="NE", warnonly=True)
595566
# find_offset function will be provided to _legend_box and
596567
# _legend_box will draw itself at the location of the return
597568
# value of the find_offset.
@@ -604,12 +575,26 @@ def _get_loc(self):
604575

605576
_loc = property(_get_loc, _set_loc)
606577

578+
def set_loc(self, loc):
579+
"""
580+
Set the legend location. For possible values see the `~.Axes.legend`
581+
docstring.
582+
"""
583+
self._set_loc(loc)
584+
585+
def get_loc(self):
586+
"""
587+
Get the legend location. This will be one of 'best', 'NE', 'NW',
588+
'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C' or a tuple of floats.
589+
"""
590+
return self._get_loc()
591+
607592
def _findoffset(self, width, height, xdescent, ydescent, renderer):
608593
"Helper function to locate the legend."
609594

610-
if self._loc == 0: # "best".
595+
if self._loc == 'best': # "best".
611596
x, y = self._find_best_position(width, height, renderer)
612-
elif self._loc in Legend.codes.values(): # Fixed location.
597+
elif isinstance(self._loc, str): # Fixed location.
613598
bbox = Bbox.from_bounds(0, 0, width, height)
614599
x, y = self._get_anchored_bbox(self._loc, bbox,
615600
self.get_bbox_to_anchor(),
@@ -1079,26 +1064,12 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
10791064
- parentbbox: a parent box which will contain the bbox. In
10801065
display coordinates.
10811066
"""
1082-
assert loc in range(1, 11) # called only internally
1083-
1084-
BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
1085-
1086-
anchor_coefs = {UR: "NE",
1087-
UL: "NW",
1088-
LL: "SW",
1089-
LR: "SE",
1090-
R: "E",
1091-
CL: "W",
1092-
CR: "E",
1093-
LC: "S",
1094-
UC: "N",
1095-
C: "C"}
10961067

1097-
c = anchor_coefs[loc]
1068+
assert loc in _valid_compass # as this is called only internally
10981069

10991070
fontsize = renderer.points_to_pixels(self._fontsize)
11001071
container = parentbbox.padded(-(self.borderaxespad) * fontsize)
1101-
anchored_box = bbox.anchored(c, container=container)
1072+
anchored_box = bbox.anchored(loc, container=container)
11021073
return anchored_box.x0, anchored_box.y0
11031074

11041075
def _find_best_position(self, width, height, renderer, consider=None):
@@ -1115,10 +1086,10 @@ def _find_best_position(self, width, height, renderer, consider=None):
11151086

11161087
bbox = Bbox.from_bounds(0, 0, width, height)
11171088
if consider is None:
1118-
consider = [self._get_anchored_bbox(x, bbox,
1089+
consider = [self._get_anchored_bbox(loc, bbox,
11191090
self.get_bbox_to_anchor(),
11201091
renderer)
1121-
for x in range(1, len(self.codes))]
1092+
for loc in _valid_compass]
11221093

11231094
candidates = []
11241095
for idx, (l, b) in enumerate(consider):
@@ -1199,7 +1170,7 @@ def draggable(self, state=None, use_blit=False, update="loc"):
11991170
is changed. If "bbox", the *bbox_to_anchor* parameter is changed.
12001171
"""
12011172
warn_deprecated("2.2",
1202-
message="Legend.draggable() is drepecated in "
1173+
message="Legend.draggable() is deprecated in "
12031174
"favor of Legend.set_draggable(). "
12041175
"Legend.draggable may be reintroduced as a "
12051176
"property in future releases.")

0 commit comments

Comments
 (0)