From 6fa3fa93921ac1b0fba724955cb5f3c7c9da841b Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sun, 18 Apr 2021 17:45:02 -0700 Subject: [PATCH 1/2] ENH: constrained_layout simple compress axes --- lib/matplotlib/_constrained_layout.py | 55 ++++++++++++++++++++++++--- lib/matplotlib/_layoutgrid.py | 4 +- lib/matplotlib/figure.py | 14 +++++-- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 245679394bc2..fe7b5055673a 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -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()`` @@ -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: @@ -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, @@ -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. diff --git a/lib/matplotlib/_layoutgrid.py b/lib/matplotlib/_layoutgrid.py index e46b3fe8c062..dd0da310acca 100644 --- a/lib/matplotlib/_layoutgrid.py +++ b/lib/matplotlib/_layoutgrid.py @@ -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): diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index fdfcd24de1e8..f505a4587bec 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -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'] @@ -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): """ @@ -2477,6 +2483,7 @@ 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) @@ -2484,7 +2491,7 @@ def get_constrained_layout_pads(self, relative=False): 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): """ @@ -3073,7 +3080,7 @@ 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() @@ -3081,7 +3088,8 @@ def execute_constrained_layout(self, renderer=None): 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): """ From a85440effe898582f9cae31d649c8f85c4450df5 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 21 Apr 2021 22:26:09 -0700 Subject: [PATCH 2/2] TST + DOC --- .../tests/test_constrainedlayout.py | 40 ++++++++++-- .../intermediate/constrainedlayout_guide.py | 62 ++++++++++++++++++- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index d8bd4cffde94..dcbe52775373 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -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 @@ -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) diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 3458667aef4e..c4f74aeb660a 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -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" @@ -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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy