From c2a6c1b6e022c01c88340336812ddf522dc8a71f Mon Sep 17 00:00:00 2001 From: ImportanceOfBeingErnest Date: Tue, 30 Oct 2018 21:41:20 +0100 Subject: [PATCH 1/2] legend-loc-compass-notation --- lib/matplotlib/legend.py | 13 ++++++++----- lib/matplotlib/rcsetup.py | 18 +++++++++--------- lib/matplotlib/tests/test_legend.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index d2c07f562478..f8c2a16670f0 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -154,6 +154,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): 'center' 10 =============== ============= + bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or @@ -335,7 +336,9 @@ class Legend(Artist): 'upper center': 9, 'center': 10, } - + compasscodes = {'nw': 2, 'n': 9, 'ne': 1, 'w': 6, 'c': 10, 'e': 7, + 'sw': 3, 's': 8, 'se': 4} + allcodes = {**codes, **compasscodes} zorder = 5 def __str__(self): @@ -505,23 +508,23 @@ def __init__(self, parent, handles, labels, if not self.isaxes and loc in [0, 'best']: loc = 'upper right' if isinstance(loc, str): - if loc not in self.codes: + if loc.lower() not in self.allcodes: if self.isaxes: cbook.warn_deprecated( "3.1", message="Unrecognized location {!r}. Falling " "back on 'best'; valid locations are\n\t{}\n" "This will raise an exception %(removal)s." - .format(loc, '\n\t'.join(self.codes))) + .format(loc, '\n\t'.join(self.allcodes))) loc = 0 else: cbook.warn_deprecated( "3.1", message="Unrecognized location {!r}. Falling " "back on 'upper right'; valid locations are\n\t{}\n'" "This will raise an exception %(removal)s." - .format(loc, '\n\t'.join(self.codes))) + .format(loc, '\n\t'.join(self.allcodes))) loc = 1 else: - loc = self.codes[loc] + loc = self.allcodes[loc.lower()] if not self.isaxes and loc == 0: cbook.warn_deprecated( "3.1", message="Automatic legend placement (loc='best') not " diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index dd1f08120038..a1c8b1657bf9 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -601,16 +601,16 @@ def validate_markevery(s): validate_legend_loc = ValidateInStrings( 'legend_loc', ['best', - 'upper right', - 'upper left', - 'lower left', - 'lower right', + 'upper right', 'ne', + 'upper left', 'nw', + 'lower left', 'sw', + 'lower right', 'se', 'right', - 'center left', - 'center right', - 'lower center', - 'upper center', - 'center'], ignorecase=True) + 'center left', 'w', + 'center right', 'e', + 'lower center', 's', + 'upper center', 'n', + 'center', 'c'], ignorecase=True) def validate_svg_fonttype(s): diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index b4bcecde81fa..7c01fbb1fc14 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -571,3 +571,32 @@ def test_no_warn_big_data_when_loc_specified(): l = ax.legend('best') fig.canvas.draw() assert len(records) == 0 + + +def test_legend_loc_compass_codes(): + locs = ['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'] + codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] + + for loc in locs: + plt.rcParams["legend.loc"] = loc + assert plt.rcParams["legend.loc"] == loc.lower() + + fig1, axes1 = plt.subplots(3, 3) + fig2, axes2 = plt.subplots(3, 3) + + locs = ['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'] + codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] + + for ax, loc in zip(axes1.flat, locs): + ax.plot([1, 2], label=loc) + ax.legend(loc=loc) + + for ax, loc in zip(axes2.flat, codes): + ax.plot([1, 2], label=loc) + ax.legend(loc=loc) + + for ax1, ax2 in zip(axes1.flat, axes2.flat): + leg1 = ax1.get_legend() + leg2 = ax2.get_legend() + assert leg1._get_loc() == leg2._get_loc() + From 41e2e86affff0894fe24c95b27980e930c3d2451 Mon Sep 17 00:00:00 2001 From: ImportanceOfBeingErnest Date: Sun, 24 Feb 2019 05:56:26 +0100 Subject: [PATCH 2/2] loc short uppercase, longlowercase --- doc/users/next_whats_new/comapss_notation.rst | 62 +++++++ lib/matplotlib/cbook/__init__.py | 83 +++++++++ lib/matplotlib/legend.py | 170 ++++++++---------- lib/matplotlib/offsetbox.py | 89 ++++----- lib/matplotlib/rcsetup.py | 17 +- lib/matplotlib/tests/test_legend.py | 66 ++++--- lib/matplotlib/tests/test_offsetbox.py | 50 ++++-- .../axes_grid1/anchored_artists.py | 138 ++++++++------ lib/mpl_toolkits/axes_grid1/inset_locator.py | 56 +++--- 9 files changed, 438 insertions(+), 293 deletions(-) create mode 100644 doc/users/next_whats_new/comapss_notation.rst diff --git a/doc/users/next_whats_new/comapss_notation.rst b/doc/users/next_whats_new/comapss_notation.rst new file mode 100644 index 000000000000..fdbcabb3d118 --- /dev/null +++ b/doc/users/next_whats_new/comapss_notation.rst @@ -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") diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index e935ceed2bad..4063013a8b6f 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -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) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index f8c2a16670f0..1df362252122 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -25,11 +25,11 @@ import numpy as np -from matplotlib import cbook from matplotlib import rcParams from matplotlib import cbook, docstring from matplotlib.artist import Artist, allow_rasterization -from matplotlib.cbook import silent_list, is_hashable, warn_deprecated +from matplotlib.cbook import (silent_list, is_hashable, warn_deprecated, + _COMPASS_LOCS, _map_loc_to_compass) from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch @@ -115,13 +115,14 @@ def _update_bbox_to_anchor(self, loc_in_canvas): The location of the legend. The strings - ``'upper left', 'upper right', 'lower left', 'lower right'`` - place the legend at the corresponding corner of the axes/figure. + ``'upper left', 'upper right', 'lower left', 'lower right'`` and their + corresponding compass strings (see table below) place the legend at the + respective corner of the axes/figure. The strings - ``'upper center', 'lower center', 'center left', 'center right'`` - place the legend at the center of the corresponding edge of the - axes/figure. + ``'upper center', 'lower center', 'center left', 'center right'`` and their + corresponding compass strings place the legend at the center of the + respective edge of the axes/figure. The string ``'center'`` places the legend at the center of the axes/figure. @@ -130,30 +131,32 @@ def _update_bbox_to_anchor(self, loc_in_canvas): artists. This option can be quite slow for plots with large amounts of data; your plotting speed may benefit from providing a specific location. + For backward-compatibility, ``'right'`` is equivalent to + ``'center right'``; and there are numeric codes for all locations as well. + + Possible (case-sensitive) strings and codes are: + + ============ ============== =============== ============= + 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 + ============ ============== =============== ============= + + The location can also be a 2-tuple giving the coordinates of the lower-left corner of the legend in axes coordinates (in which case *bbox_to_anchor* will be ignored). - For back-compatibility, ``'center right'`` (but no other location) can also - be spelled ``'right'``, and each "string" locations can also be given as a - numeric value: - - =============== ============= - Location String Location Code - =============== ============= - 'best' 0 - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. @@ -324,21 +327,7 @@ class Legend(Artist): Place a legend on the axes at location loc. """ - codes = {'best': 0, # only implemented for axes legends - 'upper right': 1, - 'upper left': 2, - 'lower left': 3, - 'lower right': 4, - 'right': 5, - 'center left': 6, - 'center right': 7, - 'lower center': 8, - 'upper center': 9, - 'center': 10, - } - compasscodes = {'nw': 2, 'n': 9, 'ne': 1, 'w': 6, 'c': 10, 'e': 7, - 'sw': 3, 's': 8, 'se': 4} - allcodes = {**codes, **compasscodes} + zorder = 5 def __str__(self): @@ -502,36 +491,6 @@ def __init__(self, parent, handles, labels, raise TypeError("Legend needs either Axes or Figure as parent") self.parent = parent - self._loc_used_default = loc is None - if loc is None: - loc = rcParams["legend.loc"] - if not self.isaxes and loc in [0, 'best']: - loc = 'upper right' - if isinstance(loc, str): - if loc.lower() not in self.allcodes: - if self.isaxes: - cbook.warn_deprecated( - "3.1", message="Unrecognized location {!r}. Falling " - "back on 'best'; valid locations are\n\t{}\n" - "This will raise an exception %(removal)s." - .format(loc, '\n\t'.join(self.allcodes))) - loc = 0 - else: - cbook.warn_deprecated( - "3.1", message="Unrecognized location {!r}. Falling " - "back on 'upper right'; valid locations are\n\t{}\n'" - "This will raise an exception %(removal)s." - .format(loc, '\n\t'.join(self.allcodes))) - loc = 1 - else: - loc = self.allcodes[loc.lower()] - if not self.isaxes and loc == 0: - cbook.warn_deprecated( - "3.1", message="Automatic legend placement (loc='best') not " - "implemented for figure legend. Falling back on 'upper " - "right'. This will raise an exception %(removal)s.") - loc = 1 - self._mode = mode self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) @@ -577,6 +536,10 @@ def __init__(self, parent, handles, labels, # init with null renderer self._init_legend_box(handles, labels, markerfirst) + # location must be set after _legend_box is created. + self._loc_used_default = loc is None + self._loc = loc + # If shadow is activated use framealpha if not # explicitly passed. See Issue 8943 if framealpha is None: @@ -587,10 +550,6 @@ def __init__(self, parent, handles, labels, else: self.get_frame().set_alpha(framealpha) - tmp = self._loc_used_default - self._set_loc(loc) - self._loc_used_default = tmp # ignore changes done by _set_loc - # figure out title fontsize: if title_fontsize is None: title_fontsize = rcParams['legend.title_fontsize'] @@ -611,10 +570,19 @@ def _set_artist_props(self, a): a.set_transform(self.get_transform()) def _set_loc(self, loc): + if loc is None: + loc = rcParams["legend.loc"] + if not self.isaxes and loc in [0, 'best']: + loc = 'NE' + if self.isaxes: + loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=True, + fallback="best", warnonly=True) + else: + loc = _map_loc_to_compass(loc, allowtuple=True, allowbest=False, + fallback="NE", warnonly=True) # find_offset function will be provided to _legend_box and # _legend_box will draw itself at the location of the return # value of the find_offset. - self._loc_used_default = False self._loc_real = loc self.stale = True self._legend_box.set_offset(self._findoffset) @@ -624,12 +592,29 @@ def _get_loc(self): _loc = property(_get_loc, _set_loc) + # public getters and setters should be introduced in the next release + # see https://github.com/matplotlib/matplotlib/pull/12679 + #def set_loc(self, loc): + # """ + # Set the legend location. For possible values see the `~.Axes.legend` + # docstring. + # """ + # self._loc_used_default = False + # self._set_loc(loc) + # + #def get_loc(self): + # """ + # Get the legend location. This will be one of 'best', 'NE', 'NW', + # 'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C' or a tuple of floats. + # """ + # return self._get_loc() + def _findoffset(self, width, height, xdescent, ydescent, renderer): "Helper function to locate the legend." - if self._loc == 0: # "best". + if self._loc == 'best': x, y = self._find_best_position(width, height, renderer) - elif self._loc in Legend.codes.values(): # Fixed location. + elif isinstance(self._loc, str): # Fixed location. bbox = Bbox.from_bounds(0, 0, width, height) x, y = self._get_anchored_bbox(self._loc, bbox, self.get_bbox_to_anchor(), @@ -1090,34 +1075,19 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer): Place the *bbox* inside the *parentbbox* according to a given location code. Return the (x,y) coordinate of the bbox. - - loc: a location code in range(1, 11). - This corresponds to the possible values for self._loc, excluding - "best". + - loc: a location code, one of 'NE', 'NW', 'SW', 'SE', 'E', + 'W', 'S', 'N', 'C'. - bbox: bbox to be placed, display coordinate units. - parentbbox: a parent box which will contain the bbox. In display coordinates. """ - assert loc in range(1, 11) # called only internally - - BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) - - anchor_coefs = {UR: "NE", - UL: "NW", - LL: "SW", - LR: "SE", - R: "E", - CL: "W", - CR: "E", - LC: "S", - UC: "N", - C: "C"} - c = anchor_coefs[loc] + assert loc in _COMPASS_LOCS # as this is called only internally fontsize = renderer.points_to_pixels(self._fontsize) container = parentbbox.padded(-(self.borderaxespad) * fontsize) - anchored_box = bbox.anchored(c, container=container) + anchored_box = bbox.anchored(loc, container=container) return anchored_box.x0, anchored_box.y0 def _find_best_position(self, width, height, renderer, consider=None): @@ -1140,10 +1110,10 @@ def _find_best_position(self, width, height, renderer, consider=None): bbox = Bbox.from_bounds(0, 0, width, height) if consider is None: - consider = [self._get_anchored_bbox(x, bbox, + consider = [self._get_anchored_bbox(loc, bbox, self.get_bbox_to_anchor(), renderer) - for x in range(1, len(self.codes))] + for loc in _COMPASS_LOCS] candidates = [] for idx, (l, b) in enumerate(consider): diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 70a6d9c54f8b..7365bc1f8475 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -27,7 +27,7 @@ FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist) from matplotlib.text import _AnnotationBase from matplotlib.transforms import Bbox, BboxBase, TransformedBbox - +from matplotlib.cbook import (_COMPASS_LOCS, _map_loc_to_compass) DEBUG = False @@ -963,19 +963,6 @@ class AnchoredOffsetbox(OffsetBox): """ zorder = 5 # zorder of the legend - # Location codes - codes = {'upper right': 1, - 'upper left': 2, - 'lower left': 3, - 'lower right': 4, - 'right': 5, - 'center left': 6, - 'center right': 7, - 'lower center': 8, - 'upper center': 9, - 'center': 10, - } - def __init__(self, loc, pad=0.4, borderpad=0.5, child=None, prop=None, frameon=True, @@ -983,19 +970,26 @@ def __init__(self, loc, bbox_transform=None, **kwargs): """ - loc is a string or an integer specifying the legend location. + loc is a string or an integer specifying the box location. The valid location codes are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, (same as 'center right', for back-compatibility) - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10, + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= + + 'rigth' (5) and 'center right' (7) are identical locations. See + `~.Axes.legend` docstring for explanation. pad : pad around the child for drawing a frame. given in fraction of fontsize. @@ -1018,14 +1012,6 @@ def __init__(self, loc, self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) self.set_child(child) - if isinstance(loc, str): - try: - loc = self.codes[loc] - except KeyError: - raise ValueError('Unrecognized location "%s". Valid ' - 'locations are\n\t%s\n' - % (loc, '\n\t'.join(self.codes))) - self.loc = loc self.borderpad = borderpad self.pad = pad @@ -1048,6 +1034,23 @@ def __init__(self, loc, self.patch.set_boxstyle("square", pad=0) self._drawFrame = frameon + def set_loc(self, loc): + """ + Set the box location. For possible values see the `~.AnchoredOffsetbox` + docstring. + """ + self._loc = _map_loc_to_compass(loc) + self.stale = True + + def get_loc(self): + """ + Get the box location. This will be one of 'NE', 'NW', + 'SW', 'SE', 'E', 'W', 'E', 'S', 'N', 'C'. + """ + return self._loc + + loc = property(get_loc, set_loc) + def set_child(self, child): "set the child to be anchored" self._child = child @@ -1179,25 +1182,9 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad): return the position of the bbox anchored at the parentbbox with the loc code, with the borderpad. """ - assert loc in range(1, 11) # called only internally - - BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) - - anchor_coefs = {UR: "NE", - UL: "NW", - LL: "SW", - LR: "SE", - R: "E", - CL: "W", - CR: "E", - LC: "S", - UC: "N", - C: "C"} - - c = anchor_coefs[loc] - + assert loc in _COMPASS_LOCS # called only internally container = parentbbox.padded(-borderpad) - anchored_box = bbox.anchored(c, container=container) + anchored_box = bbox.anchored(loc, container=container) return anchored_box.x0, anchored_box.y0 diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index a1c8b1657bf9..a4036baa8880 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -598,19 +598,10 @@ def validate_markevery(s): validate_markeverylist = _listify_validator(validate_markevery) -validate_legend_loc = ValidateInStrings( - 'legend_loc', - ['best', - 'upper right', 'ne', - 'upper left', 'nw', - 'lower left', 'sw', - 'lower right', 'se', - 'right', - 'center left', 'w', - 'center right', 'e', - 'lower center', 's', - 'upper center', 'n', - 'center', 'c'], ignorecase=True) + +def validate_legend_loc(loc): + return cbook._map_loc_to_compass(loc, allowbest=True, warnonly=True, + asrcparam='legend.loc') def validate_svg_fonttype(s): diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 7c01fbb1fc14..08b0918abb23 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -548,6 +548,43 @@ def test_alpha_handles(): assert lh.get_edgecolor()[:-1] == hh[1].get_edgecolor()[:-1] +_loc_values = [['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'], + ['northwest', 'north', 'northeast', 'west', 'center', + 'east', 'southwest', 'south', 'southeast'], + ['upper left', 'upper center', 'upper right', 'center left', + 'center', 'right', 'lower left', 'lower center', + 'lower right']] + + +@pytest.mark.parametrize('locs', _loc_values) +def test_legend_loc_compass_codes(locs): + codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] + valid = ['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'] + + for loc, val in zip(locs, valid): + plt.rcParams["legend.loc"] = loc + assert plt.rcParams["legend.loc"] == val + + fig1, axes1 = plt.subplots(3, 3) + fig2, axes2 = plt.subplots(3, 3) + + for ax, loc in zip(axes1.flat, locs): + ax.plot([1, 2], label=loc) + ax.legend(loc=loc) + + for ax, loc in zip(axes2.flat, codes): + ax.plot([1, 2], label=loc) + leg = ax.legend(loc="NW") + leg._set_loc(loc) + + for ax1, ax2 in zip(axes1.flat, axes2.flat): + leg1 = ax1.get_legend() + leg2 = ax2.get_legend() + assert leg1._get_loc() == leg2._get_loc() + fig1.canvas.draw() + fig2.canvas.draw() + + def test_warn_big_data_best_loc(): fig, ax = plt.subplots() ax.plot(np.arange(200001), label='Is this big data?') @@ -571,32 +608,3 @@ def test_no_warn_big_data_when_loc_specified(): l = ax.legend('best') fig.canvas.draw() assert len(records) == 0 - - -def test_legend_loc_compass_codes(): - locs = ['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'] - codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] - - for loc in locs: - plt.rcParams["legend.loc"] = loc - assert plt.rcParams["legend.loc"] == loc.lower() - - fig1, axes1 = plt.subplots(3, 3) - fig2, axes2 = plt.subplots(3, 3) - - locs = ['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'] - codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] - - for ax, loc in zip(axes1.flat, locs): - ax.plot([1, 2], label=loc) - ax.legend(loc=loc) - - for ax, loc in zip(axes2.flat, codes): - ax.plot([1, 2], label=loc) - ax.legend(loc=loc) - - for ax1, ax2 in zip(axes1.flat, axes2.flat): - leg1 = ax1.get_legend() - leg2 = ax2.get_legend() - assert leg1._get_loc() == leg2._get_loc() - diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index c9aff6ec616d..904c76163202 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -79,25 +79,39 @@ def test_offsetbox_clip_children(): assert fig.stale -def test_offsetbox_loc_codes(): - # Check that valid string location codes all work with an AnchoredOffsetbox - codes = {'upper right': 1, - 'upper left': 2, - 'lower left': 3, - 'lower right': 4, - 'right': 5, - 'center left': 6, - 'center right': 7, - 'lower center': 8, - 'upper center': 9, - 'center': 10, - } - fig, ax = plt.subplots() - da = DrawingArea(100, 100) - for code in codes: - anchored_box = AnchoredOffsetbox(loc=code, child=da) +_loc_values = [['NW', 'N', 'NE', 'W', 'C', 'E', 'SW', 'S', 'SE'], + ['northwest', 'north', 'northeast', 'west', 'center', + 'east', 'southwest', 'south', 'southeast'], + ['upper left', 'upper center', 'upper right', 'center left', + 'center', 'right', 'lower left', 'lower center', + 'lower right']] + + +@pytest.mark.parametrize('locs', _loc_values) +def test_offsetbox_loc_codes(locs): + + codes = [2, 9, 1, 6, 10, 7, 3, 8, 4] + + fig1, axes1 = plt.subplots(3, 3) + fig2, axes2 = plt.subplots(3, 3) + + for ax, loc in zip(axes1.flat, locs): + da = DrawingArea(50, 50) + anchored_box = AnchoredOffsetbox(loc=loc, child=da) ax.add_artist(anchored_box) - fig.canvas.draw() + + for ax, loc in zip(axes2.flat, codes): + da = DrawingArea(50, 50) + anchored_box = AnchoredOffsetbox(loc="NW", child=da) + ax.add_artist(anchored_box) + anchored_box.set_loc(loc) + + for ax1, ax2 in zip(axes1.flat, axes2.flat): + ab1, = ax1.findobj(match=AnchoredOffsetbox) + ab2, = ax2.findobj(match=AnchoredOffsetbox) + assert ab1.get_loc() == ab2.get_loc() + fig1.canvas.draw() + fig2.canvas.draw() def test_expand_with_tight_layout(): diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index fef40db2dcb5..83878c117df1 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -28,19 +28,23 @@ def __init__(self, width, height, xdescent, ydescent, xdescent, ydescent : int or float descent of the container in the x- and y- direction, in pixels. - loc : int + loc : str or int Location of this artist. Valid location codes are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= pad : int or float, optional Padding around the child objects, in fraction of the font @@ -101,19 +105,23 @@ def __init__(self, transform, loc, The transformation object for the coordinate system in use, i.e., :attr:`matplotlib.axes.Axes.transData`. - loc : int + loc : str or int Location of this artist. Valid location codes are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= pad : int or float, optional Padding around the child objects, in fraction of the font @@ -176,19 +184,23 @@ def __init__(self, transform, width, height, angle, loc, angle : int or float Rotation of the ellipse, in degrees, anti-clockwise. - loc : int - Location of this size bar. Valid location codes are:: + loc : str or int + Location of this artist. Valid location codes are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= pad : int or float, optional Padding around the ellipse, in fraction of the font size. Defaults @@ -244,19 +256,23 @@ def __init__(self, transform, size, label, loc, label : str Label to display. - loc : int - Location of this size bar. Valid location codes are:: + loc : str or int + Location of this artist. Valid location codes are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= pad : int or float, optional Padding around the label and size bar, in fraction of the font @@ -399,19 +415,23 @@ def __init__(self, transform, label_x, label_y, length=0.15, Size of label strings, given in coordinates of *transform*. Defaults to 0.08. - loc : int, optional - Location of the direction arrows. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + loc : str or int + Location of this artist. Valid location codes are:: + + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= Defaults to 2. diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 9fc40b7b26a5..66a70fb99a61 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -387,7 +387,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', Both sizes used can be specified either in inches or percentage. For example,:: - inset_axes(parent_axes, width='40%%', height='30%%', loc=3) + inset_axes(parent_axes, width='40%%', height='30%%', loc='lower left') creates in inset axes in the lower left corner of *parent_axes* which spans over 30%% in height and 40%% in width of the *parent_axes*. Since the usage @@ -427,19 +427,24 @@ def inset_axes(parent_axes, width, height, loc='upper right', are relative to the parent_axes. Otherwise they are to be understood relative to the bounding box provided via *bbox_to_anchor*. - loc : int or string, optional, default to 1 + loc : int or string, optional, defaults to 'upper right' Location to place the inset axes. The valid locations are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= + bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional Bbox that the inset axes will be anchored to. If None, @@ -545,19 +550,24 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', coordinates (i.e., "zoomed in"), while *zoom* < 1 will shrink the coordinates (i.e., "zoomed out"). - loc : int or string, optional, default to 1 + loc : int or string, optional, default to 'upper right' Location to place the inset axes. The valid locations are:: - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 + ============ ============== =============== ============= + Compass Code Compass String Location String Location Code + ============ ============== =============== ============= + '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 + ============ ============== =============== ============= + bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional Bbox that the inset axes will be anchored to. If None,