Skip to content
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

FIX: constrained_layout and repeated calls to suptitle #11035

Merged
merged 1 commit into from May 7, 2018

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Apr 12, 2018

PR Summary

As pointed out by @afvincent on gitter, repeated calls to suptitle using constrained_layout=True were making room for each call. This fixes the logic to only create the layout box if the subtitle is new.

Note that the change is just indenting a block...

import matplotlib.pyplot as plt

fig, axs = plt.subplots(1, 2, num="issue_suptitle", constrained_layout=True)

for idx in range(10):
    fig.suptitle(f"Suptitle #{idx}")
plt.show()

Before

issue_suptitle2

Now

issue_suptitle

PR Checklist

  • Code is PEP 8 compliant

@jklymak jklymak added the topic: geometry manager LayoutEngine, Constrained layout, Tight layout label Apr 12, 2018
@jklymak jklymak added this to the v2.2.3 milestone Apr 12, 2018
@jklymak
Copy link
Member Author

jklymak commented Apr 12, 2018

self-assigning for backport, but this is certainly minor enough that I don't think its a big deal if its not backported.

w_pad, h_pad, wspace, hspace = \
self.get_constrained_layout_pads(
relative=True)
layoutbox.vstack([self._suptitle._layoutbox, child],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jklymak Naive question: does the use of layout.vstack mean that one assume that the suptitle will always be above all the other elements?

@jklymak
Copy link
Member Author

jklymak commented Apr 12, 2018

Yes. Is it possible to place suptitles manually?

@afvincent
Copy link
Contributor

afvincent commented Apr 12, 2018

Apparently, according to the docstring, one can use an arbitrary position for suptitle

import matplotlib.pyplot as plt
plt.ion()
fig, ax = plt.subplots(constrained_layout=True)
fig.suptitle("This is a lower right suptitle", x=0.80, y=0.05)

Not sure what should be the exact behavior though with respect the constrained layout :/

Edit: FWIW, it looks like some space is saved at the top of the figure, even though the suptitle ends up in the lower right corner. (One can increase the font size of the suptitle to better see this behavior.)

@jklymak
Copy link
Member Author

jklymak commented Apr 12, 2018

OK, new push: if user places subtitle position manually, it doesn't get to be part of constrained_layout.

@jklymak
Copy link
Member Author

jklymak commented Apr 18, 2018

Ping @afvincent does this fix the issue for you?

@afvincent
Copy link
Contributor

afvincent commented Apr 24, 2018

It seems that this needs a rebase (sorry for delaying 🐑)

Besides, is it expected that running twice (in an IPython shell, FWIW) the following snippet

import matplotlib.pyplot as plt

fig, axs = plt.subplots(1, 2, num="issue_suptitle", constrained_layout=True,
                        clear=True)  # <- the culprit (as usual)

for idx in range(10):
    fig.suptitle(f"Suptitle #{idx}")
plt.show(block=False)

throws

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-e6132691fc2f> in <module>()
      1 import matplotlib.pyplot as plt
      2 
----> 3 fig, axs = plt.subplots(1, 2, num="issue_suptitle", clear=True, constrained_layout=True)
      4 
      5 for idx in range(10):

~/FOSS/matplotlib/lib/matplotlib/pyplot.py in subplots(nrows, ncols, sharex, sharey, squeeze, subplot_kw, gridspec_kw, **fig_kw)
   1102     subplot
   1103     """
-> 1104     fig = figure(**fig_kw)
   1105     axs = fig.subplots(nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey,
   1106                        squeeze=squeeze, subplot_kw=subplot_kw,

~/FOSS/matplotlib/lib/matplotlib/pyplot.py in figure(num, figsize, dpi, facecolor, edgecolor, frameon, FigureClass, clear, **kwargs)
    549 
    550     if clear:
--> 551         figManager.canvas.figure.clear()
    552 
    553     return figManager.canvas.figure

~/FOSS/matplotlib/lib/matplotlib/figure.py in clear(self, keep_observers)
   1438         Clear the figure -- synonym for :meth:`clf`.
   1439         """
-> 1440         self.clf(keep_observers=keep_observers)
   1441 
   1442     @allow_rasterization

~/FOSS/matplotlib/lib/matplotlib/figure.py in clf(self, keep_observers)
   1431         self._suptitle = None
   1432         if self.get_constrained_layout():
-> 1433             layoutbox.nonetree(self._layoutbox)
   1434         self.stale = True
   1435 

~/FOSS/matplotlib/lib/matplotlib/_layoutbox.py in nonetree(lb)
    671             # Clear the solver.  Hopefully this garbage collects.
    672             lb.solver.reset()
--> 673             nonechildren(lb)
    674         else:
    675             nonetree(lb.parent)

~/FOSS/matplotlib/lib/matplotlib/_layoutbox.py in nonechildren(lb)
    678 def nonechildren(lb):
    679     for child in lb.children:
--> 680         nonechildren(child)
    681     lb.artist._layoutbox = None
    682     lb = None

~/FOSS/matplotlib/lib/matplotlib/_layoutbox.py in nonechildren(lb)
    679     for child in lb.children:
    680         nonechildren(child)
--> 681     lb.artist._layoutbox = None
    682     lb = None
    683 

AttributeError: 'NoneType' object has no attribute '_layoutbox'

if one does not close the first figure window?

(Python 3.6, Matplotlib 2.2.2.post760+ga0e4c044a, Fedora 27)

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

@afvincent its not expected that anyone would use clear=True 😉 Seriously what does that do?

@afvincent
Copy link
Contributor

I have like a feeling of déjà-vu ^^ (but I cannot find the related issue and PR). From the docstring of plt.figure:

clear : bool, optional, default: False
    If True and the figure already exists, then it is cleared.

Personally I use it quite a lot 🐑 since I switched from:

figlabel = "myfigure"
plt.close(figlabel)
fig, ax = plt.subplots(num=figlabel)

to

fig, ax = plt.subplots(num="myfigure", clear=True)  # + constrained_layout=True ^^

which helps me to avoid opening dozens of figures when I am interactively writing plotting scripts through a highly evolved process known as “trial and error”.

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

OK, rebased. I'll make a different PR for the other issue...

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

@afvincent, can you open an issue with a reproducible example of the above behaviour? The following works fine for me on master:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(constrained_layout=True, num='boo')
ax.plot(range(10))
plt.show()

fig, ax = plt.subplots(constrained_layout=True, num='boo', clear=True)
ax.plot(range(30))
plt.show()

EDIT: Ooops, Ok I can reproduce if I do plt.show(block=False) in an ipython session. I dunno, might be a tough fix/won't fix. It needs interactive mode, and two non-standard kwargs to figure, and one to show.... I think trial and error is great, but I just close and re-open the window on each trial (well, what I really do is use a jupyter notebook).

@afvincent
Copy link
Contributor

You have to use a suptitle on the first figure, see #11115 ;)

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

Oh, really OK, so maybe it is this PR...

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

OK, that wasn't as bad as I feared - I wasn't setting the artist for the _suptitle.layoutbox.

manual_position = ('x' in kwargs or 'y' in kwargs)

x = kwargs.pop('x', 0.5)
y = kwargs.pop('y', 0.98)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these two lines really work? I may be naive, but neither 'x' nor 'y' belong to **kwargs, do they not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blame @anntzer 😉 The new signature is def suptitle(self, t, *, x=.5, y=.98, **kwargs): The little star in there means everything past the star is a kwarg; x and y are explicit kwargs...

Copy link
Member Author

@jklymak jklymak Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, actually, I think see what happened. @anntzer put the new signature in, and I put the pop back in. OTOH, now I'm confused if manual position ever comes back false now....

EDIT: no, it works... Thanks for catching this...

Copy link
Contributor

@afvincent afvincent Apr 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, neat ^^, but even if 'x' or 'y' are keyword-only arguments, do they belong to the special dictionary **kwargs?

Edit: yes, I think that the popping just makes it to always default to (0.5, 0.98) I think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @anntzer is this OK, or is there a better way to check if x or y have been set?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you have to use kwargs-popping in that case.

I'm a bit confused though (haven't followed the issue at all): if x and y are not set, are they always going to be 0.5 and 0.98, or is it that the constrained layout manager will move them around? If the latter, then perhaps the default should just be x=y=None, as 0.5/0.98 don't really mean anything? Although I guess they are still meaningful if the constrained layout manager is not in use...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If x and y are not set, then they start life as 0.5 and 0.98. If constrained layout is called, they will move. If the user does set x or y then constrained_layout ignites the suptitle

@afvincent
Copy link
Contributor

@jklymak If I am correct, a correct fix should pass the following test, should it not ?

def test_constrained_layout21():
    '#11035: repeated calls to suptitle should not alter the layout'
    fig, ax = plt.subplots(constrained_layout=True)

    fig.suptitle(f"Suptitle #0")
    fig.canvas.draw()
    extents0 = np.copy(ax.get_position().extents)

    fig.suptitle(f"Suptitle #1")
    fig.canvas.draw()
    extents1 = np.copy(ax.get_position().extents)

    np.testing.assert_allclose(extents0, extents1)


def test_constrained_layout22():
    '#11035: suptitle should not be include in CL if manually positioned'
    fig, ax = plt.subplots(constrained_layout=True)

    fig.canvas.draw()
    extents0 = np.copy(ax.get_position().extents)

    fig.suptitle(f"Suptitle", y=0.5)
    fig.canvas.draw()
    extents1 = np.copy(ax.get_position().extents)

    np.testing.assert_allclose(extents0, extents1)


def test_constrained_layout23():
    '''
    Comment in #11035: suptitle used to cause an exception when
    reusing a figure w/ CL with ``clear=True``.
    '''

    for i in range(2):
        fig, ax = plt.subplots(num="123", constrained_layout=True, clear=True)
        fig.suptitle(f"Suptitle {i}".format(i))

@afvincent
Copy link
Contributor

My previous comment about popping the 'x' or 'y' kwargs is due to the fact that test_constrained_layout22 does not seem to be put at y=0.5 with a recent version of the current PR.

BTW, the tests are free to steal if you want ^^: I was planning to push them against your branch. But you are definitively too reactive for me to follow the latest rebase 🐑 .


fig.suptitle(f"Suptitle", y=0.5)
fig.canvas.draw()
fig.canvas.draw()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double draw?

fig, ax = plt.subplots(constrained_layout=True)

fig.canvas.draw()
fig.canvas.draw()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why double draw?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops... was testing...

@jklymak
Copy link
Member Author

jklymak commented Apr 24, 2018

OK @anntzer:

Before, for this PR I was checking if kwargs x or y we used when calling suptitle. In a recent PR you changed the signature to def suptitle(self, t, *, x=0.5, y=0.85, **kwargs): But then the test:
manual_position = ('x' in kwargs or 'y' in kwargs) always returns False. Is there a way of testing if the user has passed a non-default value to x or y other than testing the values themselves?

Copy link
Contributor

@afvincent afvincent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Conditional to the CI passing (but I wrote parts of the new tests related to this issue so it may be a bit of a borderline self-approval :/)

child],
padding=h_pad*2.,
strength='required')
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purely a matter of personal taste and not a blocker at all, but I would have rather written

if manual_position:
    self._suptitle._layoutbox = None
else:
    figlb = ...

which I find a bit easier to read as in the other case, the long indented blocks may make it easy to misread the level indenting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that it seems more legible to move the manual_position case out first.

@afvincent
Copy link
Contributor

🐑 for the f-strings :/...

@jklymak jklymak force-pushed the fix-suptitle-CL branch 2 times, most recently from b26e978 to 0b69731 Compare April 24, 2018 13:51
@jklymak
Copy link
Member Author

jklymak commented Apr 28, 2018

ping @afvincent

Copy link
Contributor

@afvincent afvincent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two very minor comments if we really care about the docstrings of tests (apologies @jklymak the test drafts that I posted were a bit in a rough state 🐑)... Apart from this optional nitpicking, LGTM :).

I am a bit reluctant to hit the green button myself because I wrote the sketch of the tests and those were not here when @tacaswell gave his approval IIRC. I would not want to make it look like borderline self-merging.

Last question, should this actually be backported into 2.2.3? (Not sure that I followed the last updates on where we decided to put the line)



def test_constrained_layout22():
'#11035: suptitle should not be include in CL if manually positioned'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

included?
(this one comes from me, sorry :/)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to proofread the test comment strings! 😆

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have written "... very (very) minor comments..." in my review ^^.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your help on this! Great that someone (other than me) is using CL. BTW< don't be shy about making more bug reports if you have them.

def test_constrained_layout23():
'''
Comment in #11035: suptitle used to cause an exception when
reusing a figure w/ CL with ``clear=True``.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being there, a better phrasing might be "... a figure with CL and clear=True".

@jklymak
Copy link
Member Author

jklymak commented May 3, 2018

2.2.3 backports should be benign; This is all self contained, so I think benign.

artist=self._suptitle,
name=figlb.name+'.suptitle')
for child in figlb.children:
if not (child == self._suptitle._layoutbox):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if child != self._suptitle._layoutbox
or even
if child is not self._suptitle._layoutbox
given that identity should be the same as equality for layoutboxes?

self.get_constrained_layout_pads(
relative=True)
layoutbox.vstack([self._suptitle._layoutbox,
child],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slightly weird indentation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, I rearranged this a bit so the indentation wasn't as bad.

OTOH, I was purposely stacking children on top of each other for v-stack as a cisual clue they are being vertically stacked. So I kept that in.

FIX: make suptitle be in CL only if placed automatically
FIX: suptitle needed an artist as well
FIX: revert change to signature so we can test if kwargs used
fix CL tests
@jklymak
Copy link
Member Author

jklymak commented May 7, 2018

Comments addressed; squashed; rebased

@anntzer anntzer merged commit 922c950 into matplotlib:master May 7, 2018
@lumberbot-app
Copy link

lumberbot-app bot commented May 7, 2018

There seem to be a conflict, please backport manually

@jklymak
Copy link
Member Author

jklymak commented May 7, 2018

Manual backport at #11184

jklymak added a commit that referenced this pull request May 7, 2018
@jklymak jklymak deleted the fix-suptitle-CL branch May 7, 2018 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants