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
importnumpyasnpimportmatplotlib.pyplotasplt# Fixing random state for reproducibilitynp.random.seed(19680801)
N=50x=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
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.
use fig.legend, in which case you will have to keep or make a list of all the legend items and their labels.
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()
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?
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.
@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:
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.
@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.
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:
importnumpyasnpimportmatplotlib.pyplotaspltimportmatplotlib.transformsasmtransforms# Fixing random state for reproducibilitynp.random.seed(19680801)
N=50x=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()
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").
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.
importnumpyasnpimportmatplotlib.pyplotasplt# Fixing random state for reproducibilitynp.random.seed(19680801)
N=50x=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.
Activity
tacaswell commented on Apr 25, 2018
attn @jklymak
jklymak commented on Apr 25, 2018
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.
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()
deepak18 commented on Apr 25, 2018
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:
What if I don't use fig.transFigure in ax.legend() and do something like:
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?
ImportanceOfBeingErnest commented on Apr 25, 2018
Can't you call tight_layout first, then create the legend?
deepak18 commented on Apr 25, 2018
@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.
jklymak commented on Apr 25, 2018
Cross ref #11041
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:
@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:
artist.exclude_from_layout() would be an artist property and the user could exclude anything from automatic layout using tight_layout or constrained_layout.
ImportanceOfBeingErnest commented on Apr 25, 2018
@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.
jklymak commented on Apr 25, 2018
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:
ImportanceOfBeingErnest commented on Apr 25, 2018
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").
deepak18 commented on Apr 25, 2018
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:
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.
jklymak commented on Apr 25, 2018
If you have a tall legend:
works great though...
jklymak commented on Apr 29, 2018
cross ref #10682
jklymak commented on Apr 29, 2018
Ooops, that already exists: so the following gets you the old behaviour. Its slightly more verbose, but makes more logical sense.
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.