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

Skip to content

Commit 2b8181a

Browse files
loc short uppercase, longlowercase
1 parent c2a6c1b commit 2b8181a

File tree

9 files changed

+399
-281
lines changed

9 files changed

+399
-281
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,3 +2156,64 @@ def _check_in_list(values, **kwargs):
21562156
raise ValueError(
21572157
"{!r} is not a valid value for {}; supported values are {}"
21582158
.format(v, k, ', '.join(map(repr, values))))
2159+
2160+
2161+
_valid_compass = ['NE','NW', 'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C']
2162+
2163+
def _map_loc_to_compass(loc, allowtuple=False, allowbest=False,
2164+
fallback='NE', warnonly=True):
2165+
"""
2166+
Map a location string to a compass notation string. This is used by
2167+
AnchoredOffsetbox and Legend.
2168+
"""
2169+
codes = {
2170+
'upper right': 'NE', 'northeast': 'NE', 1: 'NE',
2171+
'upper left': 'NW', 'northwest': 'NW', 2: 'NW',
2172+
'lower left': 'SW', 'southwest': 'SW', 3: 'SW',
2173+
'lower right': 'SE', 'southeast': 'SE', 4: 'SE',
2174+
'right': 'E', 5: 'E',
2175+
'center left': 'W', 'west': 'W', 6: 'W',
2176+
'center right': 'E', 'east': 'E', 7: 'E',
2177+
'lower center': 'S', 'south': 'S', 8: 'S',
2178+
'upper center': 'N', 'north': 'N', 9: 'N',
2179+
'center': 'C', 10: 'C'
2180+
}
2181+
2182+
if allowbest:
2183+
codes.update({'best': 'best', 0: 'best'})
2184+
2185+
if loc in _valid_compass:
2186+
return loc
2187+
2188+
if isinstance(loc, str) or isinstance(loc, int):
2189+
if loc in codes:
2190+
return codes[loc]
2191+
2192+
if allowtuple:
2193+
if hasattr(loc, '__len__') and len(loc) == 2:
2194+
x, y = loc[0], loc[1]
2195+
if isinstance(x, numbers.Number) and isinstance(y, numbers.Number):
2196+
return tuple((x,y))
2197+
2198+
msg = "Unrecognized location {}. ".format(loc)
2199+
if isinstance(loc, str):
2200+
if loc.lower() in codes:
2201+
fallback = codes[loc.lower()]
2202+
elif loc.upper() in _valid_compass:
2203+
fallback = loc.upper()
2204+
msg += "Location strings are now case-sensitive. "
2205+
if warnonly:
2206+
msg += "Falling back on {}. ".format(fallback)
2207+
if not allowbest and loc in [0, 'best']:
2208+
msg += "Note that automatic legend placement (loc='best') is not "
2209+
msg += "implemented for figure legends or other artists. "
2210+
vcodes = [k for k in codes if not isinstance(k,int)] + _valid_compass
2211+
msg += "Valid locations are\n\t{}\n".format('\n\t'.join(vcodes))
2212+
msg += " as well as the numbers {} to 10.\n".format(0 if allowbest else 1)
2213+
if warnonly:
2214+
msg += "This will raise an exception %(removal)s."
2215+
if warnonly:
2216+
warn_deprecated("3.1", message=msg)
2217+
return fallback
2218+
else:
2219+
raise ValueError(msg)

lib/matplotlib/legend.py

Lines changed: 64 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525

2626
import numpy as np
2727

28-
from matplotlib import cbook
2928
from matplotlib import rcParams
3029
from matplotlib import cbook, docstring
3130
from matplotlib.artist import Artist, allow_rasterization
32-
from matplotlib.cbook import silent_list, is_hashable, warn_deprecated
31+
from matplotlib.cbook import (silent_list, is_hashable, warn_deprecated,
32+
_valid_compass, _map_loc_to_compass)
3333
from matplotlib.font_manager import FontProperties
3434
from matplotlib.lines import Line2D
3535
from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
@@ -115,13 +115,14 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
115115
The location of the legend.
116116
117117
The strings
118-
``'upper left', 'upper right', 'lower left', 'lower right'``
119-
place the legend at the corresponding corner of the axes/figure.
118+
``'upper left', 'upper right', 'lower left', 'lower right'`` and their
119+
corresponding compass strings (see table below) place the legend at the
120+
respective corner of the axes/figure.
120121
121122
The strings
122-
``'upper center', 'lower center', 'center left', 'center right'``
123-
place the legend at the center of the corresponding edge of the
124-
axes/figure.
123+
``'upper center', 'lower center', 'center left', 'center right'`` and their
124+
corresponding compass strings place the legend at the center of the
125+
respective edge of the axes/figure.
125126
126127
The string ``'center'`` places the legend at the center of the axes/figure.
127128
@@ -136,23 +137,28 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
136137
137138
For back-compatibility, ``'center right'`` (but no other location) can also
138139
be spelled ``'right'``, and each "string" locations can also be given as a
139-
numeric value:
140-
141-
=============== =============
142-
Location String Location Code
143-
=============== =============
144-
'best' 0
145-
'upper right' 1
146-
'upper left' 2
147-
'lower left' 3
148-
'lower right' 4
149-
'right' 5
150-
'center left' 6
151-
'center right' 7
152-
'lower center' 8
153-
'upper center' 9
154-
'center' 10
155-
=============== =============
140+
numeric value. Possible (case-sensitive) strings and codes are:
141+
142+
============ ============== =============== =============
143+
Compass Code Compass String Location String Location Code
144+
============ ============== =============== =============
145+
.. 'best' 0
146+
'NE' 'northeast' 'upper right' 1
147+
'NW' 'northwest' 'upper left' 2
148+
'SW' 'southwest' 'lower left' 3
149+
'SE' 'southeast' 'lower right' 4
150+
.. 'right' 5
151+
'W' 'west' 'center left' 6
152+
'E' 'east' 'center right' 7
153+
'S' 'south' 'lower center' 8
154+
'N' 'north' 'upper center' 9
155+
'C' 'center' 'center' 10
156+
============ ============== =============== =============
157+
158+
159+
Alternatively can be a 2-tuple giving ``x, y`` of the lower-left
160+
corner of the legend in axes coordinates (in which case
161+
``bbox_to_anchor`` will be ignored).
156162
157163
158164
bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
@@ -324,21 +330,7 @@ class Legend(Artist):
324330
Place a legend on the axes at location loc.
325331
326332
"""
327-
codes = {'best': 0, # only implemented for axes legends
328-
'upper right': 1,
329-
'upper left': 2,
330-
'lower left': 3,
331-
'lower right': 4,
332-
'right': 5,
333-
'center left': 6,
334-
'center right': 7,
335-
'lower center': 8,
336-
'upper center': 9,
337-
'center': 10,
338-
}
339-
compasscodes = {'nw': 2, 'n': 9, 'ne': 1, 'w': 6, 'c': 10, 'e': 7,
340-
'sw': 3, 's': 8, 'se': 4}
341-
allcodes = {**codes, **compasscodes}
333+
342334
zorder = 5
343335

344336
def __str__(self):
@@ -502,36 +494,6 @@ def __init__(self, parent, handles, labels,
502494
raise TypeError("Legend needs either Axes or Figure as parent")
503495
self.parent = parent
504496

505-
self._loc_used_default = loc is None
506-
if loc is None:
507-
loc = rcParams["legend.loc"]
508-
if not self.isaxes and loc in [0, 'best']:
509-
loc = 'upper right'
510-
if isinstance(loc, str):
511-
if loc.lower() not in self.allcodes:
512-
if self.isaxes:
513-
cbook.warn_deprecated(
514-
"3.1", message="Unrecognized location {!r}. Falling "
515-
"back on 'best'; valid locations are\n\t{}\n"
516-
"This will raise an exception %(removal)s."
517-
.format(loc, '\n\t'.join(self.allcodes)))
518-
loc = 0
519-
else:
520-
cbook.warn_deprecated(
521-
"3.1", message="Unrecognized location {!r}. Falling "
522-
"back on 'upper right'; valid locations are\n\t{}\n'"
523-
"This will raise an exception %(removal)s."
524-
.format(loc, '\n\t'.join(self.allcodes)))
525-
loc = 1
526-
else:
527-
loc = self.allcodes[loc.lower()]
528-
if not self.isaxes and loc == 0:
529-
cbook.warn_deprecated(
530-
"3.1", message="Automatic legend placement (loc='best') not "
531-
"implemented for figure legend. Falling back on 'upper "
532-
"right'. This will raise an exception %(removal)s.")
533-
loc = 1
534-
535497
self._mode = mode
536498
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
537499

@@ -577,6 +539,9 @@ def __init__(self, parent, handles, labels,
577539
# init with null renderer
578540
self._init_legend_box(handles, labels, markerfirst)
579541

542+
# location must be set after _legend_box is created.
543+
self._loc = loc
544+
580545
# If shadow is activated use framealpha if not
581546
# explicitly passed. See Issue 8943
582547
if framealpha is None:
@@ -587,10 +552,6 @@ def __init__(self, parent, handles, labels,
587552
else:
588553
self.get_frame().set_alpha(framealpha)
589554

590-
tmp = self._loc_used_default
591-
self._set_loc(loc)
592-
self._loc_used_default = tmp # ignore changes done by _set_loc
593-
594555
# figure out title fontsize:
595556
if title_fontsize is None:
596557
title_fontsize = rcParams['legend.title_fontsize']
@@ -611,6 +572,16 @@ def _set_artist_props(self, a):
611572
a.set_transform(self.get_transform())
612573

613574
def _set_loc(self, loc):
575+
if loc is None:
576+
loc = rcParams["legend.loc"]
577+
if not self.isaxes and loc in [0, 'best']:
578+
loc = 'NE'
579+
if self.isaxes:
580+
loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=True,
581+
fallback="best", warnonly=True)
582+
else:
583+
loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=False,
584+
fallback="NE", warnonly=True)
614585
# find_offset function will be provided to _legend_box and
615586
# _legend_box will draw itself at the location of the return
616587
# value of the find_offset.
@@ -624,12 +595,26 @@ def _get_loc(self):
624595

625596
_loc = property(_get_loc, _set_loc)
626597

598+
def set_loc(self, loc):
599+
"""
600+
Set the legend location. For possible values see the `~.Axes.legend`
601+
docstring.
602+
"""
603+
self._set_loc(loc)
604+
605+
def get_loc(self):
606+
"""
607+
Get the legend location. This will be one of 'best', 'NE', 'NW',
608+
'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C' or a tuple of floats.
609+
"""
610+
return self._get_loc()
611+
627612
def _findoffset(self, width, height, xdescent, ydescent, renderer):
628613
"Helper function to locate the legend."
629614

630-
if self._loc == 0: # "best".
615+
if self._loc == 'best': # "best".
631616
x, y = self._find_best_position(width, height, renderer)
632-
elif self._loc in Legend.codes.values(): # Fixed location.
617+
elif isinstance(self._loc, str): # Fixed location.
633618
bbox = Bbox.from_bounds(0, 0, width, height)
634619
x, y = self._get_anchored_bbox(self._loc, bbox,
635620
self.get_bbox_to_anchor(),
@@ -1098,26 +1083,12 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
10981083
- parentbbox: a parent box which will contain the bbox. In
10991084
display coordinates.
11001085
"""
1101-
assert loc in range(1, 11) # called only internally
1102-
1103-
BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
1104-
1105-
anchor_coefs = {UR: "NE",
1106-
UL: "NW",
1107-
LL: "SW",
1108-
LR: "SE",
1109-
R: "E",
1110-
CL: "W",
1111-
CR: "E",
1112-
LC: "S",
1113-
UC: "N",
1114-
C: "C"}
11151086

1116-
c = anchor_coefs[loc]
1087+
assert loc in _valid_compass # as this is called only internally
11171088

11181089
fontsize = renderer.points_to_pixels(self._fontsize)
11191090
container = parentbbox.padded(-(self.borderaxespad) * fontsize)
1120-
anchored_box = bbox.anchored(c, container=container)
1091+
anchored_box = bbox.anchored(loc, container=container)
11211092
return anchored_box.x0, anchored_box.y0
11221093

11231094
def _find_best_position(self, width, height, renderer, consider=None):
@@ -1140,10 +1111,10 @@ def _find_best_position(self, width, height, renderer, consider=None):
11401111

11411112
bbox = Bbox.from_bounds(0, 0, width, height)
11421113
if consider is None:
1143-
consider = [self._get_anchored_bbox(x, bbox,
1114+
consider = [self._get_anchored_bbox(loc, bbox,
11441115
self.get_bbox_to_anchor(),
11451116
renderer)
1146-
for x in range(1, len(self.codes))]
1117+
for loc in _valid_compass]
11471118

11481119
candidates = []
11491120
for idx, (l, b) in enumerate(consider):

0 commit comments

Comments
 (0)