Skip to content

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

Closed

Description

@joelostblom

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?

Activity

  1. 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()`[/+] on May 7, 2015
  2. tacaswell commented on May 7, 2015

    @tacaswell
    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).

  3. added this to the milestone on May 7, 2015
  4. joelostblom commented on May 7, 2015

    @joelostblom
    ContributorAuthor

    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

  5. modified the milestones: , on May 7, 2015
  6. modified the milestones: , on May 7, 2015
  7. tacaswell commented on May 7, 2015

    @tacaswell
    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).

  8. modified the milestones: , on Oct 3, 2017
  9. jklymak commented on Mar 8, 2018

    @jklymak
    Member

    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.

  10. 1 remaining item

  11. joelostblom commented on Mar 28, 2018

    @joelostblom
    ContributorAuthor

    @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
    
  12. ImportanceOfBeingErnest commented on Mar 28, 2018

    @ImportanceOfBeingErnest
    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?

  13. jklymak commented on Mar 28, 2018

    @jklymak
    Member

    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.

  14. self-assigned this
    on Mar 28, 2018
  15. ImportanceOfBeingErnest commented on Mar 28, 2018

    @ImportanceOfBeingErnest
    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?

  16. jklymak commented on Mar 28, 2018

    @jklymak
    Member

    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.

  17. ImportanceOfBeingErnest commented on Mar 28, 2018

    @ImportanceOfBeingErnest
    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.

  18. modified the milestones: , on Mar 28, 2018
  19. jklymak commented on Mar 28, 2018

    @jklymak
    Member

    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.
  20. ImportanceOfBeingErnest commented on Mar 29, 2018

    @ImportanceOfBeingErnest
    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.".

  21. joelostblom commented on Mar 29, 2018

    @joelostblom
    ContributorAuthor

    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?).

  22. jklymak commented on Mar 29, 2018

    @jklymak
    Member

    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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions