PR Summary
This is my implementation of the geometry manager (See #1109).
This is heavily based on exploratory work by @Tillsten using the kiwi
solver, and of course on tight_layout
.
Latest what's new: https://5396-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/users/next_whats_new/constrained_layout.html
Latest tutorial: https://5398-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/tutorials/intermediate/constrainedlayout_guide.html#sphx-glr-tutorials-intermediate-constrainedlayout-guide-py
Intro
tight_layout()
is great for what it does, but it doesn't handle some cases generally. i.e. if we have two GridSpecFromSubplotSpec
instances, they need to be positioned manually using tight_layout()
. Colorbars for individual axes work fine, but colorbars for more than one axis doesn't work. Etc.
This PR implements "constrained_layout" via plt.figure(constrained_layout=True)
for automatic re-drawing. Currently, it works for subplots and colorbars, legends, and suptitles. Nested gridspecs are respected. Spacing is provided both as a pad around axes and as a space between subplots.
The following code gives this plot:
fig = plt.figure(constrained_layout=True)
gs = gridspec.GridSpec(1, 2, fig=fig)
gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0])
gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1])
axsl = []
for gs in gsl:
ax = fig.add_subplot(gs)
axsl += [ax]
example_plot(ax, fontsize=12)
ax.set_xlabel('x-label\nMultiLine')
axsr = []
for gs in gsr:
ax = fig.add_subplot(gs)
axsr += [ax]
pcm = example_pcolor(ax, fontsize=12)
fig.colorbar(pcm, ax = axsr, pad=0.01,
location='bottom', ticks=ticker.MaxNLocator(nbins=5))
A few things to note in the above:
- all the subplots in the left side set of plots (
gsl
) have the same margins. They are a bit big because the bottom-right plot has a two-line x-label.
- the x-ticklabels for the rhs plot overrun, because
constrained_layout
doesn't do any management at the axis level.
Installation:
In addition to the normal matplotlib
installation, you will need to involve kiwisolver: pip install kiwisolver
now works.
API dfferences (so far)
In order to get this to work, there are a couple of API changes:
gridspec.GridSpec
now has a fig
keyword. If this isn't supplied then constrained_layout
won't work.
figure
now has a constrained_layout
keyword. Currently, I don't have it checking for tight_layout
, so they could both run at the same time. These upper level user-facing decisions probaby require input.
- Internal calling issue: We don't want axes that have user-called
ax.set_position()
to participate in constrained_layout
, presumably because the user was purposefully putting the axis where they put it. So, that means internally, we set the layoutbox
properties of this axis to None
. However, ax.set_position
gets called internally (i.e. by colorbar
and aspect
) and for those we presumably want to keep constrained_layout as a possibility. So I made a ax._set_position()
for internal calls, which the public ax.set_position()
uses. Internal calls to set_position
should use _set_position
if the axis is to continue to participate in constrained_layout.
I think most of these changes are backwards compatible, in that if you don't want to use constrained_layout
you don't need to change any calls. Backwards compatibility introduces some awkwardness, but is probably worth it.
Implementation:
Implementation is via a new package layoutbox.py
. Basically a nested array of layoutboxe objects are set up, attached to the figure, the gridspec(s), the subplotspecs. An axes has two layoutboxes: one that contains the tight bounding box of the axes, and the second that contains the "position" of the axes (i.e the rectangle that is passed to ax.set_position()
).
A linear constraint solver is used to decide dimensions. The trick here is that we define margins between an axes bounding box and its position. These margins, and the outer dimesnion of the plot (0,0,1,1) are what constrains the problem externally. Internally, the problem is constrained by relationships between the layoutboxes.
Tests:
These are in lib/matplotlib/tests/test_constrainedlayout.py
.
PR Checklist
- [x] Sort out padding in a non-figure unit
- [x] Add
legend
to constrained objects
- [x] Add kiwi to requirements: need a new release to make OK w/ 2.7. Current requirement works 3.6
- [x] Has Pytest style unit tests.
- [x] Code is PEP 8 compliant
- [x] New features are documented, with examples if plot related
- [x] Documentation is sphinx and numpydoc compliant
- [x] Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
- [x] Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way
- [x] Add
suptitle
to constrained objects
- [x]
sharex
and sharey
?
- [x] Adjust spacing for
colorbars
; respect pad
.
- [x] Implement respect for
wspace
and hspace
variables. Actually pretty easy.
- [x] Check
set_position
axes
- [x] Spacing between gridspecs
- [x] gridspec width_ratios and height_ratios
- [x]
twinx
, twiny
? #5474
- [x] Extend to putting labels outside tick labels and titles outside of all?
Release critical topic: geometry manager