FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Legend positioning behavior change from matplotlib {2.1.0 -> 2.2.2} · Issue #11126 · matplotlib/matplotlib · GitHub

Repository navigation

Legend positioning behavior change from matplotlib {2.1.0 -> 2.2.2} #11126

Closed

Description

Bug report

Bug summary
The behavior of positioning a legend outside the plot by using the figure transformation(fig.transFigure) has changed, by shortening the size of plot and leaving too much space between plot and legend.

Code for reproduction

import numpy as np
import matplotlib.pyplot as plt

# Fixing random state for reproducibility
np.random.seed(19680801)

N = 50
x = 15*np.random.rand(N)
y = 15*np.random.rand(N)
colors = np.random.rand(N)

fig, ax = plt.subplots(figsize=(8,6))

ax.scatter(x, y, c=colors, alpha=0.5, label='Scatter plot')
ax.plot([0, 15],[0, 15], color='r', linestyle='-', linewidth=2.5, label = 'Straight line')

ax.legend(bbox_to_anchor=(1, 0), loc="upper right",
           bbox_transform=fig.transFigure, columnspacing=0.5, ncol=2)

fig.tight_layout()
plt.savefig(_image_name_, bbox_inches="tight")

Actual outcome

Expected outcome
Notice the empty space between plot and the legend. This was not the case with Matplotlib version: 2.1.0
Below are the plots for the same stuff in both the versions:
Matplotlib 2.1.0:

Matplotb 2.2.2:

Matplotlib version

  • Operating system: Windows 10
  • Matplotlib version: '2.2.2'
  • Matplotlib backend: Qt5Agg or module://ipykernel.pylab.backend_inline
  • Python version: Python 3.6.5 |Anaconda custom (64-bit)
  • Jupyter Notebook version: 6.3.1

I have the Python installed through Anaconda and I spotted the above change after I updated my packages through Anaconda.

Activity

  1. tacaswell commented on Apr 25, 2018

    Member
  2. added this to the v2.2.3 milestone on Apr 25, 2018
  3. jklymak commented on Apr 25, 2018

    Member

    Yes - https://matplotlib.org/users/whats_new.html#axes-legends-now-included-in-tight-bbox (#9164)

    You have two choices with tight_layout - you can either make people mad because it includes legends, or you can make them mad because it doesn't include legends.

    To use 2.2 you have two choices.

    1. use fig.legend, in which case you will have to keep or make a list of all the legend items and their labels.
    2. don't use tight_layout and position your axes manually

    We discussed this on the call a bit this week - the consensus was to provide a ready handle/label gatherer for passing to figure.legend. i.e. handles, labels = ax.get_handles_and_labels()

  4. self-assigned this
    on Apr 25, 2018
  5. deepak18 commented on Apr 25, 2018

    Author
    1. If I don't use fig.tight_layout, the plot is not of the exact size as I mention in plt.subplots(figsize=(w,h)) like:

    2. What if I don't use fig.transFigure in ax.legend() and do something like:

    plt.legend(bbox_to_anchor=(1, -0.1), loc="upper right",
                columnspacing=0.5, ncol=2)
    
    fig.tight_layout()

    Then it will be of the size I mention and I can avoid the unnecessary space between plot and legend like:

    But, then the thing with putting legend corresponding to axes is that there is no general solution. If xtick-labels are long enough the postion that worked for the above fig won't work anymore like below. Every plot will require a customized positioning.

    Do you know, if I can tackle this in a different way?

  6. ImportanceOfBeingErnest commented on Apr 25, 2018

    Member

    Can't you call tight_layout first, then create the legend?

  7. deepak18 commented on Apr 25, 2018

    Author

    @ImportanceOfBeingErnest , that seems to work like a charm. Thank you so much.

    Since the legend is also included in tight_layout it makes sense to fit the plot first and then use the figure coordinates to place the legend. Previously, it just didn't matter.

  8. jklymak commented on Apr 25, 2018

    Member

    Cross ref #11041

    If I don't use fig.tight_layout, the plot is not of the exact size as I mention in plt.subplots(figsize=(w,h))

    It can be - you just need to call it once with tight_layout write down the position and then hardcode that.

    For the second solution, you just need to gather the handles and labels:

    import numpy as np
    import matplotlib.pyplot as plt
    
    # Fixing random state for reproducibility
    np.random.seed(19680801)
    
    N = 50
    x = 15*np.random.rand(N)
    y = 15*np.random.rand(N)
    colors = np.random.rand(N)
    
    fig, ax = plt.subplots(figsize=(8,6))
    
    s = ax.scatter(x, y, c=colors, alpha=0.5, label='Scatter plot')
    alist = [s]
    p = ax.plot([0, 15],[0, 15], color='r', linestyle='-', linewidth=2.5, label = 'Straight line')
    alist += p
    labels = [a.get_label() for a in alist]
    fig.legend(handles=alist, labels=labels, bbox_to_anchor=(1, 0), loc="lower right",
               bbox_transform=fig.transFigure, columnspacing=0.5, ncol=2)
    
    fig.tight_layout()
    
    plt.show()

    @ImportanceOfBeingErnest workaround is good for tight_layout. For constrained_layout it won't help.

    But I don't quite see how this was supposed to work before. You must have manually placed the legend to not overlap the axes labels, or manually moved the axes - tight layout used to ignore the legend. If I use your code above on 2.1:

    Anyway, our todo on this was:

    1. make this possible:
     ax.plot(range(10), label='boo')
     handles, labels = ax.get_handles_labels()
     fig.legend(handles, labels)
    1. Look into making the following possible:
     ax.plot(range(10), label='boo')
     leg = ax.legend(handles, labels)
     leg.exclude_from_layout(True)  # or some such name TBD;

    artist.exclude_from_layout() would be an artist property and the user could exclude anything from automatic layout using tight_layout or constrained_layout.

  9. ImportanceOfBeingErnest commented on Apr 25, 2018

    Member

    @jklymak I cannot reproduce the image you show. The legend should be below the ticklabels. The strategy of the OP was to position the legend outside the figure (bbox_to_anchor=(1, 0), loc="upper right",). Then tight_layout will make sure everything but the legend stays within the figure. Hence, no overlap between legend and axes/labels etc. This seems to be a useful strategy in general, because it can be applied without hardcoding any special coordinates and is completely independent on the actual figure content.

  10. jklymak commented on Apr 25, 2018

    Member

    Ooops, sorry, my fault, I changed the upper right to lower right so it would display.

    That way of doing things works if you never want to see the legend on the screen... (i.e. w/ plt.show()). Not a very satisfactory state of affairs in general.

    You could also, using 2.2, do:

    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.transforms as mtransforms
    
    # Fixing random state for reproducibility
    np.random.seed(19680801)
    
    N = 50
    x = 15*np.random.rand(N)
    y = 15*np.random.rand(N)
    colors = np.random.rand(N)
    
    fig, ax = plt.subplots(figsize=(8,6))
    
    ax.scatter(x, y, c=colors, alpha=0.5, label='Scatter plot')
    ax.plot([0, 15],[0, 15], color='r', linestyle='-', linewidth=2.5, label = 'Straight line')
    
    ax.legend(bbox_to_anchor=(1, 0), loc="lower right",
               bbox_transform=mtransforms.blended_transform_factory(
        ax.transAxes, fig.transFigure), columnspacing=0.5, ncol=2)
    
    fig.tight_layout()
    
    plt.show()

  11. ImportanceOfBeingErnest commented on Apr 25, 2018

    Member

    Don't forget that a large number of people use matplotlib in jupyter notebooks where the inline backend displays automatically what you would otherwise get via savefig(.., bbox_inches="tight").

  12. deepak18 commented on Apr 25, 2018

    Author

    In matplotlib 2.2, I won't prefer mtransforms in my case because it will fail if I have legends in multiple rows like below:

    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.transforms as mtransforms
    
    # Fixing random state for reproducibility
    np.random.seed(19680801)
    
    N = 50
    x = 15*np.random.rand(N)
    y = 15*np.random.rand(N)
    colors = np.random.rand(N)
    
    fig, ax = plt.subplots(figsize=(8,6))
    
    ax.scatter(x, y, c=colors, alpha=0.5, label='Scatter plot')
    ax.plot([0, 10],[0, 15], color='b', linestyle='-', linewidth=2.5, label = 'Straight line1')
    ax.plot([0, 15],[0, 15], color='r', linestyle='-', linewidth=2.5, label = 'Straight line2')
    ax.plot([0, 15],[0, 10], color='g', linestyle='-', linewidth=2.5, label = 'Straight line3')
    
    ax.legend(bbox_to_anchor=(1, 0), loc="lower right",
               bbox_transform=mtransforms.blended_transform_factory(
        ax.transAxes, fig.transFigure), columnspacing=0.5, ncol=1)
    
    fig.tight_layout()
    
    plt.show()

    And again even if I do upper right, it will leave empty space between plot and legend.

    But, yeah thanks the example to use fig.legend() and for introducing constrained_layout and mtransforms.blended_transform_factory to me.

  13. jklymak commented on Apr 25, 2018

    Member

    If you have a tall legend:

    ax.legend(bbox_to_anchor=(1, 0), loc="lower left",
               bbox_transform=ax.transAxes, columnspacing=0.5, ncol=1)

    works great though...

  14. jklymak commented on Apr 29, 2018

    Member

    cross ref #10682

  15. jklymak commented on Apr 29, 2018

    Member

    We discussed this on the call a bit this week - the consensus was to provide a ready handle/label gatherer for passing to figure.legend. i.e. handles, labels = ax.get_handles_and_labels()

    Ooops, that already exists: so the following gets you the old behaviour. Its slightly more verbose, but makes more logical sense.

    import numpy as np
    import matplotlib.pyplot as plt
    
    # Fixing random state for reproducibility
    np.random.seed(19680801)
    
    N = 50
    x = 15*np.random.rand(N)
    y = 15*np.random.rand(N)
    colors = np.random.rand(N)
    
    fig, ax = plt.subplots(figsize=(8,6))
    
    ax.scatter(x, y, c=colors, alpha=0.5, label='Scatter plot')
    ax.plot([0, 15],[0, 15], color='r', linestyle='-', linewidth=2.5, label = 'Straight line')
    
    h, l = ax.get_legend_handles_labels()
    
    fig.legend(h, l, bbox_to_anchor=(1, 0), loc="upper right",
               bbox_transform=fig.transFigure, columnspacing=0.5, ncol=2)
    
    fig.tight_layout()
    plt.savefig(_image_name_, bbox_inches="tight")

    Note #10682 is meant to let you just exclude the legend from tight_layout by setting leg.inbbox=False and will achieve the same thing, but won't be available until 3.0

    I'm going to close, but feel free to argue it should be re-opened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL