Skip to content

ENH: constrained_layout simple compress axes #20016

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
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions lib/matplotlib/_constrained_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@

######################################################
def do_constrained_layout(fig, renderer, h_pad, w_pad,
hspace=None, wspace=None):
hspace=None, wspace=None, compress=False):
"""
Do the constrained_layout. Called at draw time in
``figure.constrained_layout()``
Expand All @@ -83,6 +83,11 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
A value of 0.2 for a three-column layout would have a space
of 0.1 of the figure width between each column.
If h/wspace < h/w_pad, then the pads are used instead.

compress : boolean, False
Whether to try and push axes together if their aspect ratios
make it so that the they will have lots of extra white space
between them. Useful for grids of images or maps.
"""

# list of unique gridspecs that contain child axes:
Expand All @@ -98,7 +103,7 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
'Possibly did not call parent GridSpec with the'
' "figure" keyword')

for _ in range(2):
for nn in range(2):
# do the algorithm twice. This has to be done because decorations
# change size after the first re-position (i.e. x/yticklabels get
# larger/smaller). This second reposition tends to be much milder,
Expand All @@ -118,16 +123,56 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
# update all the variables in the layout.
fig._layoutgrid.update_variables()

warn_collapsed = ('constrained_layout not applied because '
'axes sizes collapsed to zero. Try making '
'figure larger or axes decorations smaller.')
if _check_no_collapsed_axes(fig):
_reposition_axes(fig, renderer, h_pad=h_pad, w_pad=w_pad,
hspace=hspace, wspace=wspace)
if compress:
_compress_fixed_aspect(fig)
# update all the variables in the layout.
fig._layoutgrid.update_variables()
if _check_no_collapsed_axes(fig):
_reposition_axes(fig, renderer, h_pad=h_pad, w_pad=w_pad,
hspace=hspace, wspace=wspace)
else:
_api.warn_external(warn_collapsed)
else:
_api.warn_external('constrained_layout not applied because '
'axes sizes collapsed to zero. Try making '
'figure larger or axes decorations smaller.')
_api.warn_external(warn_collapsed)
_reset_margins(fig)


def _compress_fixed_aspect(fig):
extraw = dict()
extrah = dict()
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
actual = ax.get_position(original=False)
ax.apply_aspect()
sub = ax.get_subplotspec()
gs = sub.get_gridspec()
if gs not in extraw.keys():
extraw[gs] = np.zeros(gs.ncols)
extrah[gs] = np.zeros(gs.nrows)
orig = ax.get_position(original=True)
actual = ax.get_position(original=False)
dw = orig.width - actual.width
if dw > 0:
for i in sub.colspan:
extraw[gs][i] = max(extraw[gs][i], dw)
dh = orig.height - actual.height
if dh > 0:
for i in sub.rowspan:
extrah[gs][i] = max(extrah[gs][i], dh)

fig._layoutgrid.edit_margin_min('left', np.sum(extraw[gs]) / 2)
fig._layoutgrid.edit_margin_min('right', np.sum(extraw[gs]) / 2)

fig._layoutgrid.edit_margin_min('top', np.sum(extrah[gs]) / 2)
fig._layoutgrid.edit_margin_min('bottom', np.sum(extrah[gs]) / 2)


def _check_no_collapsed_axes(fig):
"""
Check that no axes have collapsed to zero size.
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/_layoutgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ def __repr__(self):
f'innerW{self.inner_widths[j].value():1.3f}, ' \
f'innerH{self.inner_heights[i].value():1.3f}, ' \
f'ML{self.margins["left"][j].value():1.3f}, ' \
f'MR{self.margins["right"][j].value():1.3f}, \n'
f'MR{self.margins["right"][j].value():1.3f}, ' \
f'MB{self.margins["bottom"][j].value():1.3f}, ' \
f'MT{self.margins["top"][j].value():1.3f}, \n'
return str

def reset_margins(self):
Expand Down
14 changes: 11 additions & 3 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,10 @@ def set_constrained_layout_pads(self, **kwargs):
Height padding between subplots, expressed as a fraction of the
subplot width. The total padding ends up being h_pad + hspace.

compress : boolean
Try to compress axes in constrained layout. Useful when
axes aspect ratios make it so that there is substantial
white space between them.
"""

todo = ['w_pad', 'h_pad', 'wspace', 'hspace']
Expand All @@ -2458,6 +2462,8 @@ def set_constrained_layout_pads(self, **kwargs):
else:
self._constrained_layout_pads[td] = (
mpl.rcParams['figure.constrained_layout.' + td])
self._constrained_layout_pads['compress'] = (
kwargs.get('compress', False))

def get_constrained_layout_pads(self, relative=False):
"""
Expand All @@ -2477,14 +2483,15 @@ def get_constrained_layout_pads(self, relative=False):
h_pad = self._constrained_layout_pads['h_pad']
wspace = self._constrained_layout_pads['wspace']
hspace = self._constrained_layout_pads['hspace']
compress = self._constrained_layout_pads['compress']

if relative and (w_pad is not None or h_pad is not None):
renderer0 = layoutgrid.get_renderer(self)
dpi = renderer0.dpi
w_pad = w_pad * dpi / renderer0.width
h_pad = h_pad * dpi / renderer0.height

return w_pad, h_pad, wspace, hspace
return w_pad, h_pad, wspace, hspace, compress

def set_canvas(self, canvas):
"""
Expand Down Expand Up @@ -3073,15 +3080,16 @@ def execute_constrained_layout(self, renderer=None):
"or you need to call figure or subplots "
"with the constrained_layout=True kwarg.")
return
w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads()
w_pad, h_pad, wspace, hspace, comp = self.get_constrained_layout_pads()
# convert to unit-relative lengths
fig = self
width, height = fig.get_size_inches()
w_pad = w_pad / width
h_pad = h_pad / height
if renderer is None:
renderer = _get_renderer(fig)
do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace)
do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace,
compress=comp)

def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None):
"""
Expand Down
40 changes: 36 additions & 4 deletions lib/matplotlib/tests/test_constrainedlayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ def example_plot(ax, fontsize=12, nodec=False):
ax.set_yticklabels('')


def example_pcolor(ax, fontsize=12):
def example_pcolor(ax, fontsize=12, hide_labels=False):
dx, dy = 0.6, 0.6
y, x = np.mgrid[slice(-3, 3 + dy, dy),
slice(-3, 3 + dx, dx)]
z = (1 - x / 2. + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)
pcm = ax.pcolormesh(x, y, z[:-1, :-1], cmap='RdBu_r', vmin=-1., vmax=1.,
rasterized=True)
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)
if not hide_labels:
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)
return pcm


Expand Down Expand Up @@ -555,3 +556,34 @@ def test_align_labels():
after_align[1].x0, rtol=0, atol=1e-05)
# ensure labels do not go off the edge
assert after_align[0].x0 >= 1


def test_compressed1():
fig, axs = plt.subplots(3, 2, constrained_layout={'compress': True},
sharex=True, sharey=True)
for ax in axs.flat:
pc = ax.imshow(np.random.randn(20, 20))

fig.colorbar(pc, ax=axs)
fig.draw_no_output()

pos = axs[0, 0].get_position()
np.testing.assert_allclose(pos.x0, 0.2244, atol=1e-3)
pos = axs[0, 1].get_position()
np.testing.assert_allclose(pos.x1, 0.6925, atol=1e-3)

# wider than tall
fig, axs = plt.subplots(2, 3, constrained_layout={'compress': True},
sharex=True, sharey=True, figsize=(5, 4))
for ax in axs.flat:
pc = ax.imshow(np.random.randn(20, 20))

fig.colorbar(pc, ax=axs)
fig.draw_no_output()

pos = axs[0, 0].get_position()
np.testing.assert_allclose(pos.x0, 0.06195, atol=1e-3)
np.testing.assert_allclose(pos.y1, 0.8413, atol=1e-3)
pos = axs[1, 2].get_position()
np.testing.assert_allclose(pos.x1, 0.832587, atol=1e-3)
np.testing.assert_allclose(pos.y0, 0.205377, atol=1e-3)
62 changes: 61 additions & 1 deletion tutorials/intermediate/constrainedlayout_guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec
from matplotlib.tests.test_constrainedlayout import example_pcolor
import numpy as np

plt.rcParams['savefig.facecolor'] = "0.8"
Expand Down Expand Up @@ -230,8 +231,67 @@ def example_plot(ax, fontsize=12, hide_labels=False):
# :align: center
#

##############################################################################
# Grids of fixed-aspect axes
# ==========================
#
# Often we want to layout axes with fixed-aspect ratios. This adds an extra
# constraint to the layout problem, which by default is solved by leaving
# one dimension with large white space between axes:

fig, axs = plt.subplots(2, 2, constrained_layout=True, figsize=(6, 3))
for ax in axs.flat:
pc = example_pcolor(ax, hide_labels=True)
ax.set_aspect(1)
fig.colorbar(pc, ax=axs)
fig.suptitle('Fixed-aspect axes')

##################################
# Now, we could change the size of the figure manually to improve the
# whitespace, but that requires manual intervention.
# To address this, we can set ``constrained_layout`` to "compress" the
# axes:
fig, axs = plt.subplots(2, 2, constrained_layout={'compress': True},
figsize=(6, 3), sharex=True, sharey=True)
for ax in axs.flat:
pc = example_pcolor(ax, hide_labels=True)
ax.set_aspect(1)
fig.colorbar(pc, ax=axs)
fig.suptitle('Fixed-aspect axes')

###################################
# Note this works in the vertical direction as well, though the
# suptitle stays at the top of the plot:
fig, axs = plt.subplots(2, 2, constrained_layout={'compress': True},
figsize=(3, 5), sharex=True, sharey=True)
for ax in axs.flat:
pc = example_pcolor(ax, hide_labels=True)
ax.set_aspect(1)
fig.colorbar(pc, ax=axs)
fig.suptitle('Fixed-aspect axes')

###################################
# Note if only one row of axes have a fixed aspect, there can still be
# the need for manually adjusting the figure size, however, in this case
# widening the figure will make the layout look good again (not shown here)

fig, axs = plt.subplots(2, 2, constrained_layout={'compress': True},
figsize=(4, 6), sharex=True, sharey=True)
for i in range(2):
for j in range(2):
ax = axs[i, j]
pc = example_pcolor(ax, hide_labels=True)
if i == 0:
ax.set_title('asp=1')
ax.set_aspect(1)
else:
ax.set_title('asp="auto"')
fig.colorbar(pc, ax=axs)
fig.suptitle('Fixed-aspect axes')
plt.show()

###############################################################################
# Padding and Spacing
# Padding and spacing
# ===================
#
# Padding between axes is controlled in the horizontal by *w_pad* and
Expand Down