Skip to content

Long axis title alters xaxis length and direction with plt.tight_layout() #4413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
joelostblom opened this issue May 7, 2015 · 14 comments · Fixed by #10915
Closed

Long axis title alters xaxis length and direction with plt.tight_layout() #4413

joelostblom opened this issue May 7, 2015 · 14 comments · Fixed by #10915
Assignees
Labels
status: confirmed bug status: needs clarification Issues that need more information to resolve. topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Milestone

Comments

@joelostblom
Copy link
Contributor

I originally reported this as a seaborn issue, but it turns out that it is the tight_layout property that causes the xaxis to get narrower, then reverse and eventually throw an error if the axis title is too long. See this example notebook for details using both matplotlib and seaborn.

As I mentioned on the seaborn page, I don't think this is especially important and it's easy for the user to adjust by shrinking or splitting the title. I was just confused initially when my plots changed direction on the axis and didn't immediately relate it to the title length. Maybe a warning message can pop up when the axis title is very long, saying that it might have unexpected effects on the plot appearance?

@joelostblom joelostblom changed the title Axis title alters xaxis length and direction with plt.tight_layout() Long axis title alters xaxis length and direction with plt.tight_layout() May 7, 2015
@tacaswell
Copy link
Member

Can you reproduce this with out facet grid? Please include the code to reproduce the bug in the thread here rather than external link (which will rot).

@tacaswell tacaswell added this to the unassigned milestone May 7, 2015
@tacaswell tacaswell added the status: needs clarification Issues that need more information to resolve. label May 7, 2015
@joelostblom
Copy link
Contributor Author

Yeah I had an example with matplotlib further down in the notebook. I'm including the relevant code in this post also.

import seaborn.apionly as sns
import matplotlib.pyplot as plt

%matplotlib inline
iris = sns.load_dataset('iris')

#plotting with different title lengths
#narrower axis
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 5 )
    sns.despine()
plt.tight_layout()

#reversed axis
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 10 )
    sns.despine()
plt.tight_layout()

#throws an error if you uncomment `plt.tight_layout()`
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 20 )
    sns.despine()
#plt.tight_layout()

Below is the output, the last graph looks normal because I comment the plt.tight_layout. Throws an error othervise.

image

@tacaswell tacaswell modified the milestones: next point release, unassigned May 7, 2015
@tacaswell tacaswell added status: confirmed bug topic: geometry manager LayoutEngine, Constrained layout, Tight layout labels May 7, 2015
@tacaswell tacaswell modified the milestones: proposed next point release, next point release May 7, 2015
@tacaswell
Copy link
Member

It looks like what is happening is that the right edge of the first axes is getting pushed to match the left edge of the center title and the titles are always centered over the axes.

I am not sure what the correct behavior here is. I suspect you would want the title to wrap which we have a PR in to add (#4342).

@tacaswell tacaswell modified the milestones: 2.1 (next point release), 2.2 (next next feature release) Oct 3, 2017
@jklymak
Copy link
Member

jklymak commented Mar 8, 2018

The flipping the axis is an sns.despine issue. I'm going to close as there isn't much that we can do about this. Feel free to re-open if I'm wrong.

@jklymak jklymak closed this as completed Mar 8, 2018
@joelostblom
Copy link
Contributor Author

joelostblom commented Mar 28, 2018

@jklymak I just ran the code without sns.despine and I see the same issue as before (I am now on matplotlib 2.1.2). It appears that plt.tight_layout causes the reversed axis (and the error message for really long titles), so I think this should be reopened.

Narrow x-axis

import seaborn as sns
import matplotlib.pyplot as plt

%matplotlib inline
iris = sns.load_dataset('iris')

# Plotting with different title lengths

# narrower axis
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 5 )
plt.tight_layout()

image

Reversed x-axis

# reversed axis
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 8 )
plt.tight_layout()

image

# without `plt.tight_layout()` the axis is not reversed
fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 8 )
# plt.tight_layout()

image

Error message for long title

fig, axes = plt.subplots(1,3, figsize=(8,4))
for ax in axes.ravel():
    ax.hist(iris.petal_length)
    ax.set(title='long_title' * 15 )
plt.tight_layout()

Output:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-10-6af1a6b5aca5> in <module>()
     19     ax.hist(iris.petal_length)
     20     ax.set(title='long_title' * 15 )
---> 21 plt.tight_layout()
     22 
     23 

~/anaconda3/lib/python3.6/site-packages/matplotlib/pyplot.py in tight_layout(pad, h_pad, w_pad, rect)
   1342     """
   1343     fig = gcf()
-> 1344     fig.tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
   1345 
   1346 

~/anaconda3/lib/python3.6/site-packages/matplotlib/figure.py in tight_layout(self, renderer, pad, h_pad, w_pad, rect)
   2029             self, self.axes, subplotspec_list, renderer,
   2030             pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
-> 2031         self.subplots_adjust(**kwargs)
   2032 
   2033 

~/anaconda3/lib/python3.6/site-packages/matplotlib/figure.py in subplots_adjust(self, *args, **kwargs)
   1879 
   1880         """
-> 1881         self.subplotpars.update(*args, **kwargs)
   1882         for ax in self.axes:
   1883             if not isinstance(ax, SubplotBase):

~/anaconda3/lib/python3.6/site-packages/matplotlib/figure.py in update(self, left, bottom, right, top, wspace, hspace)
    235             if self.left >= self.right:
    236                 reset()
--> 237                 raise ValueError('left cannot be >= right')
    238 
    239             if self.bottom >= self.top:

ValueError: left cannot be >= right

@ImportanceOfBeingErnest
Copy link
Member

This is totally independent on seaborn and indeed only due to tight_layout in conjunction with long titles. The following animation shows that @tacaswell's comment from some years ago

the right edge of the first axes is getting pushed to match the left edge of the center title and the titles are always centered over the axes.

is indeed correct.

tight_layout

There does not seem to be a canonical way to solve this though. Options could be:

  • Check if title overlaps the next plot and if so
    • shrink title fontsize
    • wrap title
    • leave out the title width completely from the adjustments
  • Provide the user with arguments to do the above.

None of those seem very appealing. And one may also just leave it to the user to take care of not using too long titles.

There is this sentence in the tight_layout guide

It assumes that the extra space needed for ticklabels, axis labels, and titles is independent of original location of axes. This is often true, but there are rare cases where it is not.

which seems to suggest exactly this. Maybe it should just be written more comprehensively?

@jklymak
Copy link
Member

jklymak commented Mar 28, 2018

Is subplots_adjust getting negative numbers at some point? If so, either tight_layout could be changed to not emit negative numbers to subplots_adjust or subplots_adjust could not adjust if it gets any negative numbers.

@jklymak jklymak reopened this Mar 28, 2018
@jklymak jklymak self-assigned this Mar 28, 2018
@ImportanceOfBeingErnest
Copy link
Member

It's not negative numbers per se, but rather the wspace which grows and grows indefinitely up to a jump where it changes sign.

image

Code for reproduction
import numpy as np; np.random.seed(42)
import matplotlib.pyplot as plt
import matplotlib.animation

import copy
a = np.random.randn(30)

fig = plt.figure(figsize=(8,4))
title = [""]

param = []

def init():
    fig.clf()
    title[:] = ["titletitletitle"]
    
def update(i):
    fig.clf()
    fig.subplots_adjust(.1,.1,.9,.9,0.1,0.1)
    axes = [plt.subplot(1,3,j+1) for j in range(3)]
    title.append(str(i%10))
    for l,ax in enumerate(axes):
        ax.hist(a)
        for k,bar in enumerate(ax.patches):
            bar.set_color(plt.cm.jet(k/10.))
        ax.set_title("".join(title), y=1+0.04*l , color="C"+str(l))
     

    fig.tight_layout()
    f = fig.subplotpars.__dict__
    param.append([len("".join(title)),copy.copy(f)])
    text = []
    for k,v in f.items():
        text.append("{}: {}".format(k,v))
    axes[1].text(0,1,"\n".join(text))
    
#ani = matplotlib.animation.FuncAnimation(fig,update,frames=60,
#                                         interval=120, init_func=init)
##ani.save("tight_layout.gif", writer="imagemagick")
#plt.show()

param = []
for i in range(60):
    update(i)
    
fig, ax = plt.subplots()

letters = [p[0] for p in param]
wspace = [p[1]["wspace"] for p in param]

ax.plot(letters, wspace)
ax.set_xlabel("number of characters in title")
ax.set_ylabel("value of fig.subplotpars.wspace")
ax.set_yscale("symlog", nonposy='clip')
plt.show()

But where would you draw the line, is a wspace of 0.5, still acceptable? Or one of 1 or 10?

@jklymak
Copy link
Member

jklymak commented Mar 28, 2018

wspace should stop growing when the axes collapse on themselves and a warning should get emitted. If you try and do this w/ constrained_layout=True instead of tight_layout, the axes just collapse to zero width.

@ImportanceOfBeingErnest
Copy link
Member

Yep, but it's not like you would run this kind of continuous checking when doing tight_layout. So you cannot use wspace as a criterion, but instead need to check the actual axes width. And even that is not sufficient, as the axes width itself may be ok, but with an inverted x axis, which not desired. So one might check for the axes width and whether the axes has been inverted; but the latter is cumbersome I suppose.

@jklymak jklymak modified the milestones: needs sorting, v3.0 Mar 28, 2018
@jklymak
Copy link
Member

jklymak commented Mar 28, 2018

This is a pretty easy check. PR coming. What is the preferred failure behaviour?

  1. Right now I'm collapsing the axes to zero width and emitting a warning; thats what constrained_layout currently does.
  2. Could have tight_layout throw an error and then downstream packages (i.e. seaborn) could catch the error.
  3. Or could emit a warning and not apply tight_layout at all.

@ImportanceOfBeingErnest
Copy link
Member

It's all less than ideal. I suppose it is impossible to find out if it fails due to a long title or due to e.g. too long axis labels (which I suppose would show a similar issue?)? Thinking about it, from a user's perspective it would make most sense to go with the "leave out the title width completely from the adjustments" option. That would then allow to have plots like

image

So if it is possible to find out that it's the title that causes the problem I would issue a warning informing the user about the title-problem and even suggesting to first call tight_layout and only after that set the title.
If it is not possible to find that out, I would issue a warning of general nature.
In any case option /1. ("I'm collapsing the axes to zero width and emitting a warning") seems the most useful one to me. The user will directly see how this is problematic and can decide to leave the tight layout out or not. Option /2. calls for trouble ("It's not working; I only get error." without any visual feedback for the user). And option /3. has the look and feel of "This damn thing is simply not having any effect.".

@joelostblom
Copy link
Contributor Author

Thanks for spending time on this!

From a user's perspective, I agree that option 1 would be the most helpful as per the reasons laid out by @ImportanceOfBeingErnest above (perhaps also indicating that the plot appearance will be affected by this). If the long title could be pinpointed as the underlying issue that would be even better, but I don't think it's a big deal if it is cumbersome/not possible to implement (maybe it could just be suggested as a possible underlying issue?).

@jklymak
Copy link
Member

jklymak commented Mar 29, 2018

Please see #10915 It mostly does 1, but the axes doesn't go to zero width...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: confirmed bug status: needs clarification Issues that need more information to resolve. topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants