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 ' pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy