From 19935d60e2c67e6c70f699923e49aa7a560a20ab Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 31 Dec 2018 17:45:45 -0800 Subject: [PATCH 01/16] ENH: add figure.legend_outside --- .../figlegendoutside_demo.py | 35 +++++ lib/matplotlib/_constrained_layout.py | 41 ++++++ lib/matplotlib/_layoutbox.py | 2 +- lib/matplotlib/axes/_subplots.py | 1 - lib/matplotlib/figure.py | 126 +++++++++++++++++- lib/matplotlib/gridspec.py | 81 ++++++++++- lib/matplotlib/legend.py | 12 +- lib/matplotlib/tests/test_legend.py | 38 +++++- 8 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 examples/text_labels_and_annotations/figlegendoutside_demo.py diff --git a/examples/text_labels_and_annotations/figlegendoutside_demo.py b/examples/text_labels_and_annotations/figlegendoutside_demo.py new file mode 100644 index 000000000000..70c88a873736 --- /dev/null +++ b/examples/text_labels_and_annotations/figlegendoutside_demo.py @@ -0,0 +1,35 @@ +""" +========================== +Figure legend outside axes +========================== + +Instead of plotting a legend on each axis, a legend for all the artists on all +the sub-axes of a figure can be plotted instead. If constrained layout is +used (:doc:`/tutorials/intermediate/constrainedlayout_guide`) then room +can be made automatically for the legend by using `~.Figure.legend_outside`. + +""" + +import numpy as np +import matplotlib.pyplot as plt + +fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True) + +x = np.arange(0.0, 2.0, 0.02) +y1 = np.sin(2 * np.pi * x) +y2 = np.exp(-x) +axs[0].plot(x, y1, 'rs-', label='Line1') +h2, = axs[0].plot(x, y2, 'go', label='Line2') + +axs[0].set_ylabel('DATA') + +y3 = np.sin(4 * np.pi * x) +y4 = np.exp(-2 * x) +axs[1].plot(x, y3, 'yd-', label='Line3') +h4, = axs[1].plot(x, y4, 'k^', label='Line4') + +fig.legend_outside(loc='upper center', ncol=2) +fig.legend_outside(axs=[axs[1]], loc='lower right') +fig.legend_outside(handles=[h2, h4], labels=['curve2', 'curve4'], + loc='center left', borderaxespad=6) +plt.show() diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index b9fac97d30a8..7bb8a0cb3fe1 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -49,6 +49,7 @@ import numpy as np +import matplotlib.gridspec as gridspec import matplotlib.cbook as cbook import matplotlib._layoutbox as layoutbox @@ -182,6 +183,10 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, # reserve at top of figure include an h_pad above and below suptitle._layoutbox.edit_height(height + h_pad * 2) + # do layout for any legend_offsets + for gs in gss: + _do_offset_legend_layout(gs._layoutbox) + # OK, the above lines up ax._poslayoutbox with ax._layoutbox # now we need to # 1) arrange the subplotspecs. We do it at this level because @@ -227,11 +232,46 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, else: if suptitle is not None and suptitle._layoutbox is not None: suptitle._layoutbox.edit_height(0) + # now set the position of any offset legends... + for gs in gss: + _do_offset_legend_position(gs._layoutbox) else: cbook._warn_external('constrained_layout not applied. At least ' 'one axes collapsed to zero width or height.') +def _do_offset_legend_layout(gslayoutbox): + """ + Helper to get the right width and height for an offset legend. + """ + for child in gslayoutbox.children: + if child._is_subplotspec_layoutbox(): + # check for nested gridspecs... + for child2 in child.children: + # check for gridspec children... + if child2._is_gridspec_layoutbox(): + _do_offset_legend_layout(child2) + elif isinstance(child.artist, gridspec.LegendLayout): + child.artist._update_width_height() + + +def _do_offset_legend_position(gslayoutbox): + """ + Helper to properly set the offset box for the offset legends... + """ + for child in gslayoutbox.children: + if child._is_subplotspec_layoutbox(): + # check for nested gridspecs... + for child2 in child.children: + # check for gridspec children... + if child2._is_gridspec_layoutbox(): + _do_offset_legend_position(child2) + elif isinstance(child.artist, gridspec.LegendLayout): + # update position... + child.artist.set_bbox_to_anchor(gslayoutbox.get_rect()) + child.artist._update_width_height() + + def _make_ghost_gridspec_slots(fig, gs): """ Check for unoccupied gridspec slots and make ghost axes for these @@ -477,6 +517,7 @@ def _arrange_subplotspecs(gs, hspace=0, wspace=0): if child2._is_gridspec_layoutbox(): _arrange_subplotspecs(child2, hspace=hspace, wspace=wspace) sschildren += [child] + # now arrange the subplots... for child0 in sschildren: ss0 = child0.artist diff --git a/lib/matplotlib/_layoutbox.py b/lib/matplotlib/_layoutbox.py index 4f31f7bdb95e..ed7c6e24a6ab 100644 --- a/lib/matplotlib/_layoutbox.py +++ b/lib/matplotlib/_layoutbox.py @@ -456,7 +456,7 @@ def layout_from_subplotspec(self, subspec, self.width == parent.width * width, self.height == parent.height * height] for c in cs: - self.solver.addConstraint(c | 'required') + self.solver.addConstraint((c | 'strong')) return lb diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 6ba9439f6b86..9c9e93757d0e 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -68,7 +68,6 @@ def __init__(self, fig, *args, **kwargs): raise ValueError(f'Illegal argument(s) to subplot: {args}') self.update_params() - # _axes_class is set in the subplot_class_factory self._axes_class.__init__(self, fig, self.figbox, **kwargs) # add a layout box to this, for both the full axis, and the poss diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 4e60d82a136f..e9bf2123f443 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -30,7 +30,7 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput -from matplotlib.gridspec import GridSpec +from matplotlib.gridspec import GridSpec, GridSpecBase import matplotlib.legend as mlegend from matplotlib.patches import Rectangle from matplotlib.projections import (get_projection_names, @@ -663,6 +663,10 @@ def get_children(self): *self.images, *self.legends] + def get_gridspecs(self): + """Get a list of gridspecs associated with the figure.""" + return self._gridspecs + def contains(self, mouseevent): """ Test whether the mouse event occurred on the figure. @@ -1414,6 +1418,7 @@ def add_subplot(self, *args, **kwargs): # more similar to add_axes. self._axstack.remove(ax) + a = subplot_class_factory(projection_class)(self, *args, **kwargs) return self._add_axes_internal(key, a) @@ -1556,11 +1561,7 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, subplot_kw = subplot_kw.copy() gridspec_kw = gridspec_kw.copy() - if self.get_constrained_layout(): - gs = GridSpec(nrows, ncols, figure=self, **gridspec_kw) - else: - # this should turn constrained_layout off if we don't want it - gs = GridSpec(nrows, ncols, figure=None, **gridspec_kw) + gs = GridSpec(nrows, ncols, figure=self, **gridspec_kw) self._gridspecs.append(gs) # Create array to hold all axes. @@ -1830,6 +1831,119 @@ def legend(self, *args, **kwargs): return l @cbook._delete_parameter("3.1", "withdash") + @docstring.dedent_interpd + def legend_outside(self, *, loc=None, axs=None, **kwargs): + """ + Place a legend on the figure outside a list of axes, and automatically + make room, stealing space from the axes specified by *axs* (analogous + to what colorbar does). + + To make a legend from existing artists on every axes:: + + legend() + + To specify the axes to put the legend beside:: + + legend(axs=[ax1, ax2]) + + However, note that the legend will appear beside the gridspec that + owns these axes, so the following two calls will be the same:: + + fig, axs = plt.subplots(2, 2) + legend(axs=[axs[0, 0], axs[1, 0]]) + legend(axs=axs) + + To make a legend for a list of lines and labels:: + + legend( + handles=(line1, line2, line3), + labels=('label1', 'label2', 'label3'), + loc='upper right') + + + Parameters + ---------- + + loc: location code for legend, optional, default=1 + A legend location code, but does not support ``best`` or + ``center``. + + axs : sequence of `.Axes` or a single `.GridSpecBase`, optional + A list of axes to put the legend beside, above, or below. This is + also the list of axes that artists will be taken from if *handles* + is empty. Note that the legend will be placed adjacent to all the + axes in the gridspec that the first element in *axs* belongs to, + so mixing axes from different gridspecs may lead to confusing + results. Also, its not possible to put the legend between + two columns or rows of the same gridspec; the legend is always + outside the gridspec. This can also be passed as a gridspec + instance directly. + + handles : sequence of `.Artist`, optional + A list of Artists (lines, patches) to be added to the legend. + Use this together with *labels*, if you need full control on what + is shown in the legend and the automatic mechanism described above + is not sufficient. + + The length of handles and labels should be the same in this + case. If they are not, they are truncated to the smaller length. + + labels : sequence of strings, optional + A list of labels to show next to the artists. + Use this together with *handles*, if you need full control on what + is shown in the legend and the automatic mechanism described above + is not sufficient. + + Other Parameters + ---------------- + + %(_legend_kw_doc)s + + Returns + ------- + :class:`matplotlib.legend.Legend` instance + + Notes + ----- + Not all kinds of artist are supported by the legend command. See + :doc:`/tutorials/intermediate/legend_guide` for details. + + Currently, `~figure.legend_outside` only works if + ``constrained_layout=True``. + + See Also + -------- + .figure.legend + .gridspec.legend + .Axes.axes.legend + + """ + + if not self.get_constrained_layout(): + cbook._warn_external('legend_outside method needs ' + 'constrained_layout, using default legend') + leg = self.legend(loc=loc, **kwargs) + return leg + + if loc is None: + loc = 1 # upper right + + if axs is None: + gs = self.get_gridspecs()[0] + handles, labels, extra_args, kwargs = mlegend._parse_legend_args( + self.axes, **kwargs) + else: + if isinstance(axs, GridSpecBase): + gs = axs + else: + gs = axs[0].get_gridspec() + + handles, labels, extra_args, kwargs = mlegend._parse_legend_args( + axs, **kwargs) + + return gs.legend_outside(loc=loc, handles=handles, labels=labels, + **kwargs) + @docstring.dedent_interpd def text(self, x, y, s, fontdict=None, withdash=False, **kwargs): """ diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 9fe5a0179ea4..1a4dad20654c 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -14,8 +14,10 @@ import numpy as np +import warnings + import matplotlib as mpl -from matplotlib import _pylab_helpers, cbook, tight_layout, rcParams +from matplotlib import _pylab_helpers, cbook, tight_layout, rcParams, legend from matplotlib.transforms import Bbox import matplotlib._layoutbox as layoutbox @@ -174,6 +176,53 @@ def _normalize(key, size, axis): # Includes last index. return SubplotSpec(self, num1, num2) + def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): + """ + legend for this gridspec, offset from all the subplots. + + See `.Figure.legend_outside` for details on how to call. + """ + if not (self.figure and self.figure.get_constrained_layout()): + cbook._warn_external('legend_outside method needs ' + 'constrained_layout') + leg = self.figure.legend(*args, **kwargs) + return leg + + if axs is None: + axs = self.figure.get_axes() + padding = kwargs.pop('borderaxespad', 2.0) + + # convert padding from points to figure relative units.... + padding = padding / 72.0 + paddingw = padding / self.figure.get_size_inches()[0] + paddingh = padding / self.figure.get_size_inches()[1] + + + handles, labels, extra_args, kwargs = legend._parse_legend_args( + axs, handles=handles, labels=labels, **kwargs) + leg = LegendLayout(self, self.figure, handles, labels, *extra_args, + **kwargs) + # put to the right of any subplots in this gridspec: + + leg._update_width_height() + + for child in self._layoutbox.children: + if child._is_subplotspec_layoutbox(): + if leg._loc in [1, 4, 5, 7]: + # stack to the right... + layoutbox.hstack([child, leg._layoutbox], padding=paddingw) + elif leg._loc in [2, 3, 6]: + # stack to the left... + layoutbox.hstack([leg._layoutbox, child], padding=paddingw) + elif leg._loc in [8]: + # stack to the bottom... + layoutbox.vstack([child, leg._layoutbox], padding=paddingh) + elif leg._loc in [9]: + # stack to the top... + layoutbox.vstack([leg._layoutbox, child], padding=paddingh) + self.figure.legends.append(leg) + return leg + class GridSpec(GridSpecBase): """ @@ -346,6 +395,33 @@ def tight_layout(self, figure, renderer=None, self.update(**kwargs) + +class LegendLayout(legend.Legend): + """ + `.Legend` subclass that carries layout information.... + """ + + def __init__(self, parent, parent_figure, handles, labels, *args, **kwargs): + super().__init__(parent_figure, handles, labels, *args, **kwargs) + self._layoutbox = layoutbox.LayoutBox( + parent=parent._layoutbox, + name=parent._layoutbox.name + 'legend' + layoutbox.seq_id(), + artist=self) + + def _update_width_height(self): + + invTransFig = self.figure.transFigure.inverted().transform_bbox + + bbox = invTransFig(self.get_window_extent(self.figure.canvas.get_renderer())) + height = bbox.height + h_pad = 0 + w_pad = 0 + + self._layoutbox.edit_height(height+h_pad) + width = bbox.width + self._layoutbox.edit_width(width+w_pad) + + class GridSpecFromSubplotSpec(GridSpecBase): """ GridSpec whose subplot layout parameters are inherited from the @@ -369,6 +445,7 @@ def __init__(self, nrows, ncols, width_ratios=width_ratios, height_ratios=height_ratios) # do the layoutboxes + self.figure = subplot_spec._gridspec.figure subspeclb = subplot_spec._layoutbox if subspeclb is None: self._layoutbox = None @@ -428,11 +505,11 @@ def __init__(self, gridspec, num1, num2=None): self.num1 = num1 self.num2 = num2 if gridspec._layoutbox is not None: - glb = gridspec._layoutbox # So note that here we don't assign any layout yet, # just make the layoutbox that will contain all items # associated w/ this axis. This can include other axes like # a colorbar or a legend. + glb = gridspec._layoutbox self._layoutbox = layoutbox.LayoutBox( parent=glb, name=glb.name + '.ss' + layoutbox.seq_id(), diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index ee754bd37379..bb11c4cd6722 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -407,6 +407,7 @@ def __init__(self, parent, handles, labels, """ # local import only to avoid circularity from matplotlib.axes import Axes + from matplotlib.gridspec import GridSpec from matplotlib.figure import Figure Artist.__init__(self) @@ -482,6 +483,9 @@ def __init__(self, parent, handles, labels, self.isaxes = True self.axes = parent self.set_figure(parent.figure) + elif isinstance(parent, GridSpec): + self.isaxes=False + self.set_figure(parent.figure) elif isinstance(parent, Figure): self.isaxes = False self.set_figure(parent) @@ -993,9 +997,11 @@ def get_window_extent(self, renderer=None): 'Return extent of the legend.' if renderer is None: renderer = self.figure._cachedRenderer - return self._legend_box.get_window_extent(renderer=renderer) + bbox = self._legend_box.get_window_extent(renderer) + + return bbox - def get_tightbbox(self, renderer): + def get_tightbbox(self, renderer=None): """ Like `.Legend.get_window_extent`, but uses the box for the legend. @@ -1009,6 +1015,8 @@ def get_tightbbox(self, renderer): ------- `.BboxBase` : containing the bounding box in figure pixel co-ordinates. """ + if renderer is None: + renderer = self.figure._cachedRenderer return self._legend_box.get_window_extent(renderer) def get_frame_on(self): diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index aa3f51b69a83..3373b6d5b93c 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -3,6 +3,8 @@ from unittest import mock import numpy as np +from numpy.testing import ( + assert_allclose, assert_array_equal, assert_array_almost_equal) import pytest from matplotlib.testing.decorators import image_comparison @@ -357,7 +359,41 @@ def test_warn_args_kwargs(self): "be discarded.") -@image_comparison(['legend_stackplot.png']) +def test_figure_legend_outside(): + todos = [1, 2, 3, 4, 5, 6, 7, 8, 9] + axbb = [[ 20.347556, 27.722556, 664.805222, 588.833], # upper right + [146.125333, 27.722556, 790.583 , 588.833], # upper left + [146.125333, 27.722556, 790.583 , 588.833], # lower left + [ 20.347556, 27.722556, 664.805222, 588.833], # lower right + [ 20.347556, 27.722556, 664.805222, 588.833], # right + [146.125333, 27.722556, 790.583 , 588.833], # center left + [ 20.347556, 27.722556, 664.805222, 588.833], # center right + [ 20.347556, 65.500333, 790.583 , 588.833], # lower center + [ 20.347556, 27.722556, 790.583 , 551.055222], # upper center + ] + legbb = [[667., 555., 790., 590.], + [10., 555., 133., 590.], + [ 10., 10., 133., 45.], + [667, 10. , 790. , 45.], + [667. , 282.5, 790. , 317.5], + [ 10. , 282.5, 133. , 317.5], + [667. , 282.5, 790. , 317.5], + [338.5, 10. , 461.5, 45.], + [338.5, 555., 461.5, 590.], + ] + for nn, todo in enumerate(todos): + fig, axs = plt.subplots(constrained_layout=True, dpi=100) + axs.plot(range(10), label=f'Boo1') + leg = fig.legend_outside(loc=todo) + renderer = fig.canvas.get_renderer() + fig.canvas.draw() + assert_allclose(axs.get_window_extent(renderer=renderer).extents, + axbb[nn]) + assert_allclose(leg.get_window_extent(renderer=renderer).extents, + legbb[nn]) + + +@image_comparison(baseline_images=['legend_stackplot'], extensions=['png']) def test_legend_stackplot(): '''test legend for PolyCollection using stackplot''' # related to #1341, #1943, and PR #3303 From 20a56fb0d2bbf6b2b3ac656de214ad72fe823c9c Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 31 Dec 2018 17:48:33 -0800 Subject: [PATCH 02/16] ENH: add figure.legend_outside --- lib/matplotlib/figure.py | 3 +-- lib/matplotlib/gridspec.py | 10 +++++----- lib/matplotlib/legend.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index e9bf2123f443..e8ec3c7c624f 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1418,7 +1418,6 @@ def add_subplot(self, *args, **kwargs): # more similar to add_axes. self._axstack.remove(ax) - a = subplot_class_factory(projection_class)(self, *args, **kwargs) return self._add_axes_internal(key, a) @@ -1926,7 +1925,7 @@ def legend_outside(self, *, loc=None, axs=None, **kwargs): return leg if loc is None: - loc = 1 # upper right + loc = 1 # upper right if axs is None: gs = self.get_gridspecs()[0] diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 1a4dad20654c..475481319842 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -185,7 +185,7 @@ def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): if not (self.figure and self.figure.get_constrained_layout()): cbook._warn_external('legend_outside method needs ' 'constrained_layout') - leg = self.figure.legend(*args, **kwargs) + leg = self.figure.legend(**kwargs) return leg if axs is None: @@ -197,7 +197,6 @@ def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): paddingw = padding / self.figure.get_size_inches()[0] paddingh = padding / self.figure.get_size_inches()[1] - handles, labels, extra_args, kwargs = legend._parse_legend_args( axs, handles=handles, labels=labels, **kwargs) leg = LegendLayout(self, self.figure, handles, labels, *extra_args, @@ -395,13 +394,13 @@ def tight_layout(self, figure, renderer=None, self.update(**kwargs) - class LegendLayout(legend.Legend): """ `.Legend` subclass that carries layout information.... """ - def __init__(self, parent, parent_figure, handles, labels, *args, **kwargs): + def __init__(self, parent, parent_figure, handles, labels, *args, + **kwargs): super().__init__(parent_figure, handles, labels, *args, **kwargs) self._layoutbox = layoutbox.LayoutBox( parent=parent._layoutbox, @@ -412,7 +411,8 @@ def _update_width_height(self): invTransFig = self.figure.transFigure.inverted().transform_bbox - bbox = invTransFig(self.get_window_extent(self.figure.canvas.get_renderer())) + bbox = invTransFig( + self.get_window_extent(self.figure.canvas.get_renderer())) height = bbox.height h_pad = 0 w_pad = 0 diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index bb11c4cd6722..fae5c941d077 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -484,7 +484,7 @@ def __init__(self, parent, handles, labels, self.axes = parent self.set_figure(parent.figure) elif isinstance(parent, GridSpec): - self.isaxes=False + self.isaxes = False self.set_figure(parent.figure) elif isinstance(parent, Figure): self.isaxes = False From f655c1481b00afda44d0cc2aabd545d811afc92e Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 31 Dec 2018 19:33:51 -0800 Subject: [PATCH 03/16] ENH: add figure.legend_outside --- lib/matplotlib/figure.py | 4 ++-- lib/matplotlib/tests/test_legend.py | 30 ++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index e8ec3c7c624f..1f2487a8abf8 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1867,7 +1867,7 @@ def legend_outside(self, *, loc=None, axs=None, **kwargs): A legend location code, but does not support ``best`` or ``center``. - axs : sequence of `.Axes` or a single `.GridSpecBase`, optional + axs : sequence of `.axes.Axes` or a single `.GridSpecBase`, optional A list of axes to put the legend beside, above, or below. This is also the list of axes that artists will be taken from if *handles* is empty. Note that the legend will be placed adjacent to all the @@ -1914,7 +1914,7 @@ def legend_outside(self, *, loc=None, axs=None, **kwargs): -------- .figure.legend .gridspec.legend - .Axes.axes.legend + .axes.Axes.legend """ diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 3373b6d5b93c..8994d68732a1 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -361,24 +361,24 @@ def test_warn_args_kwargs(self): def test_figure_legend_outside(): todos = [1, 2, 3, 4, 5, 6, 7, 8, 9] - axbb = [[ 20.347556, 27.722556, 664.805222, 588.833], # upper right - [146.125333, 27.722556, 790.583 , 588.833], # upper left - [146.125333, 27.722556, 790.583 , 588.833], # lower left - [ 20.347556, 27.722556, 664.805222, 588.833], # lower right - [ 20.347556, 27.722556, 664.805222, 588.833], # right - [146.125333, 27.722556, 790.583 , 588.833], # center left - [ 20.347556, 27.722556, 664.805222, 588.833], # center right - [ 20.347556, 65.500333, 790.583 , 588.833], # lower center - [ 20.347556, 27.722556, 790.583 , 551.055222], # upper center + axbb = [[20.347556, 27.722556, 664.805222, 588.833], # upper right + [146.125333, 27.722556, 790.583, 588.833], # upper left + [146.125333, 27.722556, 790.583, 588.833], # lower left + [20.347556, 27.722556, 664.805222, 588.833], # lower right + [20.347556, 27.722556, 664.805222, 588.833], # right + [146.125333, 27.722556, 790.583, 588.833], # center left + [20.347556, 27.722556, 664.805222, 588.833], # center right + [20.347556, 65.500333, 790.583, 588.833], # lower center + [20.347556, 27.722556, 790.583, 551.055222], # upper center ] legbb = [[667., 555., 790., 590.], [10., 555., 133., 590.], - [ 10., 10., 133., 45.], - [667, 10. , 790. , 45.], - [667. , 282.5, 790. , 317.5], - [ 10. , 282.5, 133. , 317.5], - [667. , 282.5, 790. , 317.5], - [338.5, 10. , 461.5, 45.], + [10., 10., 133., 45.], + [667, 10., 790., 45.], + [667., 282.5, 790., 317.5], + [10., 282.5, 133., 317.5], + [667., 282.5, 790., 317.5], + [338.5, 10., 461.5, 45.], [338.5, 555., 461.5, 590.], ] for nn, todo in enumerate(todos): From a761639cd2b358b1e54c15ecbb960edc8a8bceb7 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 1 Jan 2019 16:34:16 -0800 Subject: [PATCH 04/16] FIX: add legend kwarg outside --- .../figlegendoutside_demo.py | 8 +- lib/matplotlib/figure.py | 161 ++++-------------- lib/matplotlib/gridspec.py | 1 + lib/matplotlib/tests/test_legend.py | 2 +- 4 files changed, 39 insertions(+), 133 deletions(-) diff --git a/examples/text_labels_and_annotations/figlegendoutside_demo.py b/examples/text_labels_and_annotations/figlegendoutside_demo.py index 70c88a873736..52acbe8bb640 100644 --- a/examples/text_labels_and_annotations/figlegendoutside_demo.py +++ b/examples/text_labels_and_annotations/figlegendoutside_demo.py @@ -28,8 +28,8 @@ axs[1].plot(x, y3, 'yd-', label='Line3') h4, = axs[1].plot(x, y4, 'k^', label='Line4') -fig.legend_outside(loc='upper center', ncol=2) -fig.legend_outside(axs=[axs[1]], loc='lower right') -fig.legend_outside(handles=[h2, h4], labels=['curve2', 'curve4'], - loc='center left', borderaxespad=6) +fig.legend(loc='upper center', outside=True, ncol=2) +fig.legend(axs=[axs[1]], outside=True, loc='lower right') +fig.legend(handles=[h2, h4], labels=['curve2', 'curve4'], + outside=True, loc='center left', borderaxespad=6) plt.show() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 1f2487a8abf8..aff24cb958ee 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1755,7 +1755,7 @@ def get_axes(self): # docstring of pyplot.figlegend. @docstring.dedent_interpd - def legend(self, *args, **kwargs): + def legend(self, *args, outside=False, axs=None, **kwargs): """ Place a legend on the figure. @@ -1779,6 +1779,16 @@ def legend(self, *args, **kwargs): Parameters ---------- + + outside: bool + If ``constrained_layout=True``, then try and place legend outside + axes listed in *axs*, or highest-level gridspec if axs is empty. + Note, "center" and "best" options to *loc* do not work with + ``outside=True`` + + axs : sequence of `~.axes.Axes` + axes to gather handles from (if *handles* is empty). + handles : list of `.Artist`, optional A list of Artists (lines, patches) to be added to the legend. Use this together with *labels*, if you need full control on what @@ -1807,142 +1817,37 @@ def legend(self, *args, **kwargs): Not all kinds of artist are supported by the legend command. See :doc:`/tutorials/intermediate/legend_guide` for details. """ + if axs is None: + axs = self.axes handles, labels, extra_args, kwargs = mlegend._parse_legend_args( - self.axes, + axs, *args, **kwargs) - # check for third arg - if len(extra_args): - # cbook.warn_deprecated( - # "2.1", - # message="Figure.legend will accept no more than two " - # "positional arguments in the future. Use " - # "'fig.legend(handles, labels, loc=location)' " - # "instead.") - # kwargs['loc'] = extra_args[0] - # extra_args = extra_args[1:] - pass - l = mlegend.Legend(self, handles, labels, *extra_args, **kwargs) - self.legends.append(l) + if outside and not self.get_constrained_layout(): + cbook._warn_external('legend outside=True method needs ' + 'constrained_layout=True. Setting False') + outside = False + + if not outside: + l = mlegend.Legend(self, handles, labels, *extra_args, **kwargs) + self.legends.append(l) + else: + loc = kwargs.pop('loc') + if axs is None: + gs = self.get_gridspecs()[0] + else: + if isinstance(axs, GridSpecBase): + gs = axs + else: + gs = axs[0].get_gridspec() + l = gs.legend_outside(loc=loc, handles=handles, labels=labels, + **kwargs) l._remove_method = self.legends.remove self.stale = True return l @cbook._delete_parameter("3.1", "withdash") - @docstring.dedent_interpd - def legend_outside(self, *, loc=None, axs=None, **kwargs): - """ - Place a legend on the figure outside a list of axes, and automatically - make room, stealing space from the axes specified by *axs* (analogous - to what colorbar does). - - To make a legend from existing artists on every axes:: - - legend() - - To specify the axes to put the legend beside:: - - legend(axs=[ax1, ax2]) - - However, note that the legend will appear beside the gridspec that - owns these axes, so the following two calls will be the same:: - - fig, axs = plt.subplots(2, 2) - legend(axs=[axs[0, 0], axs[1, 0]]) - legend(axs=axs) - - To make a legend for a list of lines and labels:: - - legend( - handles=(line1, line2, line3), - labels=('label1', 'label2', 'label3'), - loc='upper right') - - - Parameters - ---------- - - loc: location code for legend, optional, default=1 - A legend location code, but does not support ``best`` or - ``center``. - - axs : sequence of `.axes.Axes` or a single `.GridSpecBase`, optional - A list of axes to put the legend beside, above, or below. This is - also the list of axes that artists will be taken from if *handles* - is empty. Note that the legend will be placed adjacent to all the - axes in the gridspec that the first element in *axs* belongs to, - so mixing axes from different gridspecs may lead to confusing - results. Also, its not possible to put the legend between - two columns or rows of the same gridspec; the legend is always - outside the gridspec. This can also be passed as a gridspec - instance directly. - - handles : sequence of `.Artist`, optional - A list of Artists (lines, patches) to be added to the legend. - Use this together with *labels*, if you need full control on what - is shown in the legend and the automatic mechanism described above - is not sufficient. - - The length of handles and labels should be the same in this - case. If they are not, they are truncated to the smaller length. - - labels : sequence of strings, optional - A list of labels to show next to the artists. - Use this together with *handles*, if you need full control on what - is shown in the legend and the automatic mechanism described above - is not sufficient. - - Other Parameters - ---------------- - - %(_legend_kw_doc)s - - Returns - ------- - :class:`matplotlib.legend.Legend` instance - - Notes - ----- - Not all kinds of artist are supported by the legend command. See - :doc:`/tutorials/intermediate/legend_guide` for details. - - Currently, `~figure.legend_outside` only works if - ``constrained_layout=True``. - - See Also - -------- - .figure.legend - .gridspec.legend - .axes.Axes.legend - - """ - - if not self.get_constrained_layout(): - cbook._warn_external('legend_outside method needs ' - 'constrained_layout, using default legend') - leg = self.legend(loc=loc, **kwargs) - return leg - - if loc is None: - loc = 1 # upper right - - if axs is None: - gs = self.get_gridspecs()[0] - handles, labels, extra_args, kwargs = mlegend._parse_legend_args( - self.axes, **kwargs) - else: - if isinstance(axs, GridSpecBase): - gs = axs - else: - gs = axs[0].get_gridspec() - - handles, labels, extra_args, kwargs = mlegend._parse_legend_args( - axs, **kwargs) - - return gs.legend_outside(loc=loc, handles=handles, labels=labels, - **kwargs) - @docstring.dedent_interpd def text(self, x, y, s, fontdict=None, withdash=False, **kwargs): """ diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 475481319842..0d3e02a1dc41 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -220,6 +220,7 @@ def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): # stack to the top... layoutbox.vstack([leg._layoutbox, child], padding=paddingh) self.figure.legends.append(leg) + return leg diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 8994d68732a1..f2cf861d62d7 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -384,7 +384,7 @@ def test_figure_legend_outside(): for nn, todo in enumerate(todos): fig, axs = plt.subplots(constrained_layout=True, dpi=100) axs.plot(range(10), label=f'Boo1') - leg = fig.legend_outside(loc=todo) + leg = fig.legend(loc=todo, outside=True) renderer = fig.canvas.get_renderer() fig.canvas.draw() assert_allclose(axs.get_window_extent(renderer=renderer).extents, From 43ce090dc381ea4bc4b5fd30486227e9130b2bd8 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 1 Jan 2019 16:39:47 -0800 Subject: [PATCH 05/16] FIX: add legend kwarg outside --- lib/matplotlib/figure.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index aff24cb958ee..20eee175e61d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1828,6 +1828,9 @@ def legend(self, *args, outside=False, axs=None, **kwargs): cbook._warn_external('legend outside=True method needs ' 'constrained_layout=True. Setting False') outside = False + if outside and kwargs.get('bbox_to_anchor') is not None: + cbook._warn_external('legend outside=True ignores bbox_to_anchor ' + 'kwarg') if not outside: l = mlegend.Legend(self, handles, labels, *extra_args, **kwargs) From 81209e6ae9719ff009d9a813986f398d1a87652b Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jan 2019 10:15:09 -0800 Subject: [PATCH 06/16] ENH: add vertical layout --- lib/matplotlib/figure.py | 11 +++++++---- lib/matplotlib/gridspec.py | 27 +++++++++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 20eee175e61d..4190b77a4061 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1780,11 +1780,14 @@ def legend(self, *args, outside=False, axs=None, **kwargs): Parameters ---------- - outside: bool + outside: bool or string If ``constrained_layout=True``, then try and place legend outside axes listed in *axs*, or highest-level gridspec if axs is empty. Note, "center" and "best" options to *loc* do not work with - ``outside=True`` + ``outside=True``. The corner values of *loc* (i.e. "upper right") + will default to a horizontal layout of the legend, but this can + be changed by specifying a string + ``outside="vertical", loc="upper right"``. axs : sequence of `~.axes.Axes` axes to gather handles from (if *handles* is empty). @@ -1844,8 +1847,8 @@ def legend(self, *args, outside=False, axs=None, **kwargs): gs = axs else: gs = axs[0].get_gridspec() - l = gs.legend_outside(loc=loc, handles=handles, labels=labels, - **kwargs) + l = gs.legend_outside(loc=loc, align=outside, handles=handles, + labels=labels, **kwargs) l._remove_method = self.legends.remove self.stale = True return l diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 0d3e02a1dc41..b5fae7563e71 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -176,7 +176,8 @@ def _normalize(key, size, axis): # Includes last index. return SubplotSpec(self, num1, num2) - def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): + def legend_outside(self, handles=None, labels=None, axs=None, + align='horizontal', **kwargs): """ legend for this gridspec, offset from all the subplots. @@ -205,18 +206,32 @@ def legend_outside(self, handles=None, labels=None, axs=None, **kwargs): leg._update_width_height() + if leg._loc in [5, 7, 4, 1]: + stack = 'right' + elif leg._loc in [6, 2, 3]: + stack = 'left' + elif leg._loc in [8]: + stack = 'bottom' + else: + stack = 'top' + + if align == 'vertical': + if leg._loc in [1, 2]: + stack = 'top' + elif leg._loc in [3, 4]: + stack = 'bottom' + for child in self._layoutbox.children: if child._is_subplotspec_layoutbox(): - if leg._loc in [1, 4, 5, 7]: - # stack to the right... + if stack == 'right': layoutbox.hstack([child, leg._layoutbox], padding=paddingw) - elif leg._loc in [2, 3, 6]: + elif stack == 'left': # stack to the left... layoutbox.hstack([leg._layoutbox, child], padding=paddingw) - elif leg._loc in [8]: + elif stack == 'bottom': # stack to the bottom... layoutbox.vstack([child, leg._layoutbox], padding=paddingh) - elif leg._loc in [9]: + elif stack == 'top': # stack to the top... layoutbox.vstack([leg._layoutbox, child], padding=paddingh) self.figure.legends.append(leg) From f3c138aba852791b70291d615f42b2a3439a6c6e Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jan 2019 12:15:33 -0800 Subject: [PATCH 07/16] ENH: change axs to ax --- lib/matplotlib/figure.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 4190b77a4061..531a32946814 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1755,7 +1755,7 @@ def get_axes(self): # docstring of pyplot.figlegend. @docstring.dedent_interpd - def legend(self, *args, outside=False, axs=None, **kwargs): + def legend(self, *args, outside=False, ax=None, **kwargs): """ Place a legend on the figure. @@ -1789,7 +1789,7 @@ def legend(self, *args, outside=False, axs=None, **kwargs): be changed by specifying a string ``outside="vertical", loc="upper right"``. - axs : sequence of `~.axes.Axes` + ax : sequence of `~.axes.Axes` axes to gather handles from (if *handles* is empty). handles : list of `.Artist`, optional @@ -1820,11 +1820,11 @@ def legend(self, *args, outside=False, axs=None, **kwargs): Not all kinds of artist are supported by the legend command. See :doc:`/tutorials/intermediate/legend_guide` for details. """ - if axs is None: - axs = self.axes + if ax is None: + ax = self.axes handles, labels, extra_args, kwargs = mlegend._parse_legend_args( - axs, + ax, *args, **kwargs) if outside and not self.get_constrained_layout(): @@ -1840,13 +1840,13 @@ def legend(self, *args, outside=False, axs=None, **kwargs): self.legends.append(l) else: loc = kwargs.pop('loc') - if axs is None: + if ax is None: gs = self.get_gridspecs()[0] else: - if isinstance(axs, GridSpecBase): - gs = axs + if isinstance(ax, GridSpecBase): + gs = ax else: - gs = axs[0].get_gridspec() + gs = ax[0].get_gridspec() l = gs.legend_outside(loc=loc, align=outside, handles=handles, labels=labels, **kwargs) l._remove_method = self.legends.remove From 004755bd326761a1502ce3579ac11a5f46239e14 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jan 2019 13:26:44 -0800 Subject: [PATCH 08/16] DOC: fix and expand example --- .../figlegendoutside_demo.py | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/examples/text_labels_and_annotations/figlegendoutside_demo.py b/examples/text_labels_and_annotations/figlegendoutside_demo.py index 52acbe8bb640..cc0c4f296712 100644 --- a/examples/text_labels_and_annotations/figlegendoutside_demo.py +++ b/examples/text_labels_and_annotations/figlegendoutside_demo.py @@ -6,7 +6,8 @@ Instead of plotting a legend on each axis, a legend for all the artists on all the sub-axes of a figure can be plotted instead. If constrained layout is used (:doc:`/tutorials/intermediate/constrainedlayout_guide`) then room -can be made automatically for the legend by using `~.Figure.legend_outside`. +can be made automatically for the legend by using `~.Figure.legend` with the +``outside=True`` kwarg. """ @@ -29,7 +30,49 @@ h4, = axs[1].plot(x, y4, 'k^', label='Line4') fig.legend(loc='upper center', outside=True, ncol=2) -fig.legend(axs=[axs[1]], outside=True, loc='lower right') +fig.legend(ax=[axs[1]], outside=True, loc='lower right') fig.legend(handles=[h2, h4], labels=['curve2', 'curve4'], outside=True, loc='center left', borderaxespad=6) plt.show() + +############################################################################### +# The usual codes for the *loc* kwarg are allowed, however, the corner +# codes have an ambiguity as to whether the legend is stacked +# horizontally (the default) or vertically. To specify the vertical stacking +# the *outside* kwarg can be specified with ``"vertical"`` instead of just +# the booloean *True*: + +fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True) +axs[0].plot(x, y1, 'rs-', label='Line1') +h2, = axs[0].plot(x, y2, 'go', label='Line2') + +axs[0].set_ylabel('DATA') +axs[1].plot(x, y3, 'yd-', label='Line3') +h4, = axs[1].plot(x, y4, 'k^', label='Line4') + +fig.legend(loc='upper right', outside='vertical', ncol=2) +plt.show() + +############################################################################### +# Significantly more complicated layouts are possible using the gridspec +# organization of subplots: + +fig = plt.figure(constrained_layout=True) +gs0 = fig.add_gridspec(1, 2) + +gs = gs0[0].subgridspec(1, 1) +for i in range(1): + ax = fig.add_subplot(gs[i,0]) + ax.plot(range(10), label=f'Boo{i}') +lg = fig.legend(ax=[ax], loc='upper left', outside=True, borderaxespad=4) + +gs2 = gs0[1].subgridspec(3, 1) +axx = [] +for i in range(3): + ax = fig.add_subplot(gs2[i, 0]) + ax.plot(range(10), label=f'Who{i}', color=f'C{i+1}') + if i < 2: + ax.set_xticklabels('') + axx += [ax] +lg2 = fig.legend(ax=axx[:-1], loc='upper right', outside=True, borderaxespad=4) +plt.show() From 67cd29916107ed8e3e92e16bc08f37c4ae032191 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jan 2019 15:11:16 -0800 Subject: [PATCH 09/16] DOC: fix and expand example --- examples/text_labels_and_annotations/figlegendoutside_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/text_labels_and_annotations/figlegendoutside_demo.py b/examples/text_labels_and_annotations/figlegendoutside_demo.py index cc0c4f296712..670d29c30365 100644 --- a/examples/text_labels_and_annotations/figlegendoutside_demo.py +++ b/examples/text_labels_and_annotations/figlegendoutside_demo.py @@ -62,7 +62,7 @@ gs = gs0[0].subgridspec(1, 1) for i in range(1): - ax = fig.add_subplot(gs[i,0]) + ax = fig.add_subplot(gs[i, 0]) ax.plot(range(10), label=f'Boo{i}') lg = fig.legend(ax=[ax], loc='upper left', outside=True, borderaxespad=4) From 82f44987d48fbec85f89c617be7405b469cb8136 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jan 2019 16:31:08 -0800 Subject: [PATCH 10/16] DOC: fix and expand example --- examples/text_labels_and_annotations/figlegendoutside_demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/text_labels_and_annotations/figlegendoutside_demo.py b/examples/text_labels_and_annotations/figlegendoutside_demo.py index 670d29c30365..b0134c361275 100644 --- a/examples/text_labels_and_annotations/figlegendoutside_demo.py +++ b/examples/text_labels_and_annotations/figlegendoutside_demo.py @@ -40,7 +40,7 @@ # codes have an ambiguity as to whether the legend is stacked # horizontally (the default) or vertically. To specify the vertical stacking # the *outside* kwarg can be specified with ``"vertical"`` instead of just -# the booloean *True*: +# the boolean *True*: fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True) axs[0].plot(x, y1, 'rs-', label='Line1') @@ -64,7 +64,7 @@ for i in range(1): ax = fig.add_subplot(gs[i, 0]) ax.plot(range(10), label=f'Boo{i}') -lg = fig.legend(ax=[ax], loc='upper left', outside=True, borderaxespad=4) +lg = fig.legend(ax=[ax], loc='lower right', outside=True, borderaxespad=4) gs2 = gs0[1].subgridspec(3, 1) axx = [] From 1bd8a85e792c3703b04391b93d14e205ad87f678 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 4 Jan 2019 12:31:43 -0800 Subject: [PATCH 11/16] ENH: add outside argument to axes legend as well --- lib/matplotlib/legend.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index fae5c941d077..3d9bcd349063 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -375,6 +375,7 @@ def __init__(self, parent, handles, labels, bbox_transform=None, # transform for the bbox frameon=None, # draw frame handler_map=None, + outside=False, ): """ Parameters @@ -430,6 +431,7 @@ def __init__(self, parent, handles, labels, self.legendHandles = [] self._legend_title_box = None + self.outside = outside #: A dictionary with the extra handler mappings for this Legend #: instance. self._custom_handler_map = handler_map @@ -1107,8 +1109,37 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer): c = anchor_coefs[loc] fontsize = renderer.points_to_pixels(self._fontsize) - container = parentbbox.padded(-(self.borderaxespad) * fontsize) - anchored_box = bbox.anchored(c, container=container) + if not self.outside: + container = parentbbox.padded(-(self.borderaxespad) * fontsize) + anchored_box = bbox.anchored(c, container=container) + else: + if c in ['NE', 'SE', 'E']: + stack = 'right' + elif c in ['NW', 'SW', 'W']: + stack = 'left' + elif c in ['N']: + stack = 'top' + else: + stack = 'bottom' + if self.outside == 'vertical': + if c in ['NE', 'NW']: + stack = 'top' + elif c in ['SE', 'SW']: + stack = 'bottom' + anchored_box = bbox.anchored(c, container=parentbbox) + if stack == 'right': + anchored_box.x0 = (anchored_box.x0 + anchored_box.width + + (self.borderaxespad) * fontsize) + elif stack == 'left': + anchored_box.x0 = (anchored_box.x0 - anchored_box.width - + (self.borderaxespad) * fontsize) + elif stack == 'bottom': + anchored_box.y0 = (anchored_box.y0 - anchored_box.height - + (self.borderaxespad) * fontsize) + elif stack == 'top': + anchored_box.y0 = (anchored_box.y0 + anchored_box.height + + (self.borderaxespad) * fontsize) + return anchored_box.x0, anchored_box.y0 def _find_best_position(self, width, height, renderer, consider=None): From ed1a18a52a8593f8547861923bc7822bbedf51c5 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sun, 6 Jan 2019 08:41:25 -0800 Subject: [PATCH 12/16] FIX: make borderaxespad consistent --- lib/matplotlib/gridspec.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index b5fae7563e71..1a48b31ca374 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -191,12 +191,10 @@ def legend_outside(self, handles=None, labels=None, axs=None, if axs is None: axs = self.figure.get_axes() - padding = kwargs.pop('borderaxespad', 2.0) + padding = kwargs.pop('borderaxespad', rcParams["legend.borderaxespad"]) # convert padding from points to figure relative units.... - padding = padding / 72.0 - paddingw = padding / self.figure.get_size_inches()[0] - paddingh = padding / self.figure.get_size_inches()[1] + handles, labels, extra_args, kwargs = legend._parse_legend_args( axs, handles=handles, labels=labels, **kwargs) @@ -221,6 +219,10 @@ def legend_outside(self, handles=None, labels=None, axs=None, elif leg._loc in [3, 4]: stack = 'bottom' + padding = padding * leg._fontsize / 72.0 + paddingw = padding / self.figure.get_size_inches()[0] + paddingh = padding / self.figure.get_size_inches()[1] + for child in self._layoutbox.children: if child._is_subplotspec_layoutbox(): if stack == 'right': From b26f8673d5839c01be38e5e99e998a4abba78234 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sun, 6 Jan 2019 09:32:02 -0800 Subject: [PATCH 13/16] FIX: make borderaxespad consistent --- lib/matplotlib/tests/test_legend.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index f2cf861d62d7..3b674040bc75 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -361,15 +361,15 @@ def test_warn_args_kwargs(self): def test_figure_legend_outside(): todos = [1, 2, 3, 4, 5, 6, 7, 8, 9] - axbb = [[20.347556, 27.722556, 664.805222, 588.833], # upper right - [146.125333, 27.722556, 790.583, 588.833], # upper left - [146.125333, 27.722556, 790.583, 588.833], # lower left - [20.347556, 27.722556, 664.805222, 588.833], # lower right - [20.347556, 27.722556, 664.805222, 588.833], # right - [146.125333, 27.722556, 790.583, 588.833], # center left - [20.347556, 27.722556, 664.805222, 588.833], # center right - [20.347556, 65.500333, 790.583, 588.833], # lower center - [20.347556, 27.722556, 790.583, 551.055222], # upper center + axbb = [[20.347556, 24.722556, 657.583, 589.833], # upper right + [153.347556, 24.722556, 790.583, 589.833], # upper left + [153.347556, 24.722556, 790.583, 589.833], # lower left + [20.347556, 24.722556, 657.583, 589.833], # lower right + [20.347556, 24.722556, 657.583, 589.833], # right + [153.347556, 24.722556, 790.583, 589.833], # center left + [20.347556, 24.722556, 657.583, 589.833], # center right + [20.347556, 69.722556, 790.583, 589.833], # lower center + [20.347556, 24.722556, 790.583, 544.833], # upper center ] legbb = [[667., 555., 790., 590.], [10., 555., 133., 590.], From 69864524816d0d9b3051385baa6742338438dbf3 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sun, 27 Jan 2019 19:25:26 -0800 Subject: [PATCH 14/16] TST: fix test values --- lib/matplotlib/tests/test_legend.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 3b674040bc75..18b0abced8f9 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -361,15 +361,15 @@ def test_warn_args_kwargs(self): def test_figure_legend_outside(): todos = [1, 2, 3, 4, 5, 6, 7, 8, 9] - axbb = [[20.347556, 24.722556, 657.583, 589.833], # upper right - [153.347556, 24.722556, 790.583, 589.833], # upper left - [153.347556, 24.722556, 790.583, 589.833], # lower left - [20.347556, 24.722556, 657.583, 589.833], # lower right - [20.347556, 24.722556, 657.583, 589.833], # right - [153.347556, 24.722556, 790.583, 589.833], # center left - [20.347556, 24.722556, 657.583, 589.833], # center right - [20.347556, 69.722556, 790.583, 589.833], # lower center - [20.347556, 24.722556, 790.583, 544.833], # upper center + axbb = [[20.347556, 27.722556, 657.583, 588.833], # upper right + [153.347556, 27.722556, 790.583, 588.833], # upper left + [153.347556, 27.722556, 790.583, 588.833], # lower left + [20.347556, 27.722556, 657.583, 588.833], # lower right + [20.347556, 27.722556, 657.583, 588.833], # right + [153.347556, 27.722556, 790.583, 588.833], # center left + [20.347556, 27.722556, 657.583, 588.833], # center right + [20.347556, 72.722556, 790.583, 588.833], # lower center + [20.347556, 27.722556, 790.583, 543.833], # upper center ] legbb = [[667., 555., 790., 590.], [10., 555., 133., 590.], From 3aa9fe9678f24e3e42e4e94c945e957dd9c13a92 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 30 Jan 2019 15:05:35 -0800 Subject: [PATCH 15/16] FL8 --- lib/matplotlib/gridspec.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 1a48b31ca374..231833c9efa7 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -195,7 +195,6 @@ def legend_outside(self, handles=None, labels=None, axs=None, # convert padding from points to figure relative units.... - handles, labels, extra_args, kwargs = legend._parse_legend_args( axs, handles=handles, labels=labels, **kwargs) leg = LegendLayout(self, self.figure, handles, labels, *extra_args, From 3dbd7037ace43e288e8773fca78d283a6defa2f9 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 30 Jan 2019 15:10:38 -0800 Subject: [PATCH 16/16] Small cleanup --- lib/matplotlib/figure.py | 9 +++------ lib/matplotlib/gridspec.py | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 531a32946814..eb2ff3f4fee7 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1840,13 +1840,10 @@ def legend(self, *args, outside=False, ax=None, **kwargs): self.legends.append(l) else: loc = kwargs.pop('loc') - if ax is None: - gs = self.get_gridspecs()[0] + if isinstance(ax, GridSpecBase): + gs = ax else: - if isinstance(ax, GridSpecBase): - gs = ax - else: - gs = ax[0].get_gridspec() + gs = ax[0].get_gridspec() l = gs.legend_outside(loc=loc, align=outside, handles=handles, labels=labels, **kwargs) l._remove_method = self.legends.remove diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 231833c9efa7..9be2ab0e970c 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -181,7 +181,8 @@ def legend_outside(self, handles=None, labels=None, axs=None, """ legend for this gridspec, offset from all the subplots. - See `.Figure.legend_outside` for details on how to call. + See the *outside* argument for `.Figure.legend` for details on how to + call. """ if not (self.figure and self.figure.get_constrained_layout()): cbook._warn_external('legend_outside method needs '