From f4baa7d9f666a03f3f5c85ec05be0c59806bd2cc Mon Sep 17 00:00:00 2001 From: Elan Ernest <23121882+ImportanceOfBeingErnest@users.noreply.github.com> Date: Tue, 2 Oct 2018 23:14:49 +0200 Subject: [PATCH] Backport PR #11648: FIX: colorbar placement in constrained layout --- .flake8 | 2 +- .../colorbar_placement.py | 54 +++++++ lib/matplotlib/_constrained_layout.py | 142 ++++++++++++------ lib/matplotlib/colorbar.py | 1 + .../test_colorbar_location.png | Bin 0 -> 13142 bytes .../tests/test_constrainedlayout.py | 21 +++ .../intermediate/constrainedlayout_guide.py | 38 ++++- 7 files changed, 202 insertions(+), 56 deletions(-) create mode 100644 examples/subplots_axes_and_figures/colorbar_placement.py create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png diff --git a/.flake8 b/.flake8 index 5c63a35efcd6..aaefc9f7d08d 100644 --- a/.flake8 +++ b/.flake8 @@ -95,7 +95,7 @@ per-file-ignores = tutorials/colors/colormaps.py: E501 tutorials/colors/colors.py: E402 tutorials/intermediate/artists.py: E402, E501 - tutorials/intermediate/constrainedlayout_guide.py: E402, E501 + tutorials/intermediate/constrainedlayout_guide.py: E402 tutorials/intermediate/gridspec.py: E402, E501 tutorials/intermediate/legend_guide.py: E402, E501 tutorials/intermediate/tight_layout_guide.py: E402, E501 diff --git a/examples/subplots_axes_and_figures/colorbar_placement.py b/examples/subplots_axes_and_figures/colorbar_placement.py new file mode 100644 index 000000000000..eee99acbea0a --- /dev/null +++ b/examples/subplots_axes_and_figures/colorbar_placement.py @@ -0,0 +1,54 @@ +""" +================= +Placing Colorbars +================= + +Colorbars indicate the quantitative extent of image data. Placing in +a figure is non-trivial because room needs to be made for them. + +The simplest case is just attaching a colorbar to each axes: +""" +import matplotlib.pyplot as plt +import numpy as np + +fig, axs = plt.subplots(2, 2) +cm = ['RdBu_r', 'viridis'] +for col in range(2): + for row in range(2): + ax = axs[row, col] + pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), + cmap=cm[col]) + fig.colorbar(pcm, ax=ax) +plt.show() + +###################################################################### +# The first column has the same type of data in both rows, so it may +# be desirable to combine the colorbar which we do by calling +# `.Figure.colorbar` with a list of axes instead of a single axes. + +fig, axs = plt.subplots(2, 2) +cm = ['RdBu_r', 'viridis'] +for col in range(2): + for row in range(2): + ax = axs[row, col] + pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), + cmap=cm[col]) + fig.colorbar(pcm, ax=axs[:, col], shrink=0.6) +plt.show() + +###################################################################### +# Relatively complicated colorbar layouts are possible using this +# paradigm. Note that this example works far better with +# ``constrained_layout=True`` + +fig, axs = plt.subplots(3, 3, constrained_layout=True) +for ax in axs.flat: + pcm = ax.pcolormesh(np.random.random((20, 20))) + +fig.colorbar(pcm, ax=axs[0, :2], shrink=0.6, location='bottom') +fig.colorbar(pcm, ax=[axs[0, 2]], location='bottom') +fig.colorbar(pcm, ax=axs[1:, :], location='right', shrink=0.6) +fig.colorbar(pcm, ax=[axs[2, 1]], location='left') + + +plt.show() diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 7f740b9980a5..4efb5303cfd5 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -572,6 +572,36 @@ def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05): return lb, lbpos +def _getmaxminrowcolumn(axs): + # helper to get the min/max rows and columns of a list of axes. + maxrow = -100000 + minrow = 1000000 + maxax = None + minax = None + maxcol = -100000 + mincol = 1000000 + maxax_col = None + minax_col = None + + for ax in axs: + subspec = ax.get_subplotspec() + nrows, ncols, row_start, row_stop, col_start, col_stop = \ + subspec.get_rows_columns() + if row_stop > maxrow: + maxrow = row_stop + maxax = ax + if row_start < minrow: + minrow = row_start + minax = ax + if col_stop > maxcol: + maxcol = col_stop + maxax_col = ax + if col_start < mincol: + mincol = col_start + minax_col = ax + return (minrow, maxrow, minax, maxax, mincol, maxcol, minax_col, maxax_col) + + def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05): """ Do the layout for a colorbar, to not oeverly pollute colorbar.py @@ -586,6 +616,10 @@ def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05): lb = layoutbox.LayoutBox(parent=gslb.parent, name=gslb.parent.name + '.cbar', artist=cax) + # figure out the row and column extent of the parents. + (minrow, maxrow, minax_row, maxax_row, + mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents) + if location in ('left', 'right'): lbpos = layoutbox.LayoutBox( parent=lb, @@ -594,39 +628,43 @@ def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05): pos=True, subplot=False, artist=cax) - - if location == 'right': - # arrange to right of the gridpec sibbling - layoutbox.hstack([gslb, lb], padding=pad * gslb.width, - strength='strong') - else: - layoutbox.hstack([lb, gslb], padding=pad * gslb.width) + for ax in parents: + if location == 'right': + order = [ax._layoutbox, lb] + else: + order = [lb, ax._layoutbox] + layoutbox.hstack(order, padding=pad * gslb.width, + strength='strong') # constrain the height and center... # This isn't quite right. We'd like the colorbar # pos to line up w/ the axes poss, not the size of the # gs. - maxrow = -100000 - minrow = 1000000 - maxax = None - minax = None - for ax in parents: - subspec = ax.get_subplotspec() - nrows, ncols = subspec.get_gridspec().get_geometry() - for num in [subspec.num1, subspec.num2]: - rownum1, colnum1 = divmod(subspec.num1, ncols) - if rownum1 > maxrow: - maxrow = rownum1 - maxax = ax - if rownum1 < minrow: - minrow = rownum1 - minax = ax - # invert the order so these are bottom to top: - maxposlb = minax._poslayoutbox - minposlb = maxax._poslayoutbox + # Horizontal Layout: need to check all the axes in this gridspec + for ch in gslb.children: + subspec = ch.artist + nrows, ncols, row_start, row_stop, col_start, col_stop = \ + subspec.get_rows_columns() + if location == 'right': + if col_stop <= maxcol: + order = [subspec._layoutbox, lb] + # arrange to right of the parents + if col_start > maxcol: + order = [lb, subspec._layoutbox] + elif location == 'left': + if col_start >= mincol: + order = [lb, subspec._layoutbox] + if col_stop < mincol: + order = [subspec._layoutbox, lb] + layoutbox.hstack(order, padding=pad * gslb.width, + strength='strong') + + # Vertical layout: + maxposlb = minax_row._poslayoutbox + minposlb = maxax_row._poslayoutbox # now we want the height of the colorbar pos to be - # set by the top and bottom of these poss - # bottom top + # set by the top and bottom of the min/max axes... + # bottom top # b t # h = (top-bottom)*shrink # b = bottom + (top-bottom - h) / 2. @@ -650,29 +688,35 @@ def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05): subplot=False, artist=cax) - if location == 'bottom': - layoutbox.vstack([gslb, lb], padding=pad * gslb.width) - else: - layoutbox.vstack([lb, gslb], padding=pad * gslb.width) - - maxcol = -100000 - mincol = 1000000 - maxax = None - minax = None - for ax in parents: - subspec = ax.get_subplotspec() - nrows, ncols = subspec.get_gridspec().get_geometry() - for num in [subspec.num1, subspec.num2]: - rownum1, colnum1 = divmod(subspec.num1, ncols) - if colnum1 > maxcol: - maxcol = colnum1 - maxax = ax - if rownum1 < mincol: - mincol = colnum1 - minax = ax - maxposlb = maxax._poslayoutbox - minposlb = minax._poslayoutbox + if location == 'bottom': + order = [ax._layoutbox, lb] + else: + order = [lb, ax._layoutbox] + layoutbox.vstack(order, padding=pad * gslb.width, + strength='strong') + + # Vertical Layout: need to check all the axes in this gridspec + for ch in gslb.children: + subspec = ch.artist + nrows, ncols, row_start, row_stop, col_start, col_stop = \ + subspec.get_rows_columns() + if location == 'bottom': + if row_stop <= minrow: + order = [subspec._layoutbox, lb] + if row_start > maxrow: + order = [lb, subspec._layoutbox] + elif location == 'top': + if row_stop < minrow: + order = [subspec._layoutbox, lb] + if row_start >= maxrow: + order = [lb, subspec._layoutbox] + layoutbox.vstack(order, padding=pad * gslb.width, + strength='strong') + + # Do horizontal layout... + maxposlb = maxax_col._poslayoutbox + minposlb = minax_col._poslayoutbox lbpos.constrain_width((maxposlb.right - minposlb.left) * shrink) lbpos.constrain_left( diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 3df090452a02..d2c662d02567 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1243,6 +1243,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, Returns (cax, kw), the child axes and the reduced kw dictionary to be passed when creating the colorbar instance. ''' + locations = ["left", "right", "top", "bottom"] if orientation is not None and location is not None: raise TypeError('position and orientation are mutually exclusive. ' diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png new file mode 100644 index 0000000000000000000000000000000000000000..6d2351922688791674774cf96a33a081356dca4d GIT binary patch literal 13142 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�Oa=%T$#hKV03>bNi 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 0803504cea94..e282cb6ad80d 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -378,3 +378,24 @@ def test_constrained_layout23(): for i in range(2): fig, ax = plt.subplots(num="123", constrained_layout=True, clear=True) fig.suptitle("Suptitle{}".format(i)) + + +@image_comparison(baseline_images=['test_colorbar_location'], + extensions=['png'], remove_text=True, style='mpl20') +def test_colorbar_location(): + """ + Test that colorbar handling is as expected for various complicated + cases... + """ + + fig, axs = plt.subplots(4, 5, constrained_layout=True) + for ax in axs.flatten(): + pcm = example_pcolor(ax) + ax.set_xlabel('') + ax.set_ylabel('') + fig.colorbar(pcm, ax=axs[:, 1], shrink=0.4) + fig.colorbar(pcm, ax=axs[-1, :2], shrink=0.5, location='bottom') + fig.colorbar(pcm, ax=axs[0, 2:], shrink=0.5, location='bottom') + fig.colorbar(pcm, ax=axs[-2, 3:], shrink=0.5, location='top') + fig.colorbar(pcm, ax=axs[0, 0], shrink=0.5, location='left') + fig.colorbar(pcm, ax=axs[1:3, 2], shrink=0.5, location='right') diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 15ee5fcec079..b14f382635e4 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -118,10 +118,10 @@ def example_plot(ax, fontsize=12, nodec=False): # # .. note:: # -# For the `~.axes.Axes.pcolormesh` kwargs (``pc_kwargs``) we use a dictionary. -# Below we will assign one colorbar to a number of axes each containing -# a `~.cm.ScalarMappable`; specifying the norm and colormap ensures -# the colorbar is accurate for all the axes. +# For the `~.axes.Axes.pcolormesh` kwargs (``pc_kwargs``) we use a +# dictionary. Below we will assign one colorbar to a number of axes each +# containing a `~.cm.ScalarMappable`; specifying the norm and colormap +# ensures the colorbar is accurate for all the axes. arr = np.arange(100).reshape((10, 10)) norm = mcolors.Normalize(vmin=0., vmax=100.) @@ -133,14 +133,25 @@ def example_plot(ax, fontsize=12, nodec=False): ############################################################################ # If you specify a list of axes (or other iterable container) to the -# ``ax`` argument of ``colorbar``, constrained_layout will take space from all -# axes that share the same gridspec. +# ``ax`` argument of ``colorbar``, constrained_layout will take space from +# the specified axes. fig, axs = plt.subplots(2, 2, figsize=(4, 4), constrained_layout=True) for ax in axs.flatten(): im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) +############################################################################ +# If you specify a list of axes from inside a grid of axes, the colorbar +# will steal space appropriately, and leave a gap, but all subplots will +# still be the same size. + +fig, axs = plt.subplots(3, 3, figsize=(4, 4), constrained_layout=True) +for ax in axs.flatten(): + im = ax.pcolormesh(arr, **pc_kwargs) +fig.colorbar(im, ax=axs[1:, ][:, 1], shrink=0.8) +fig.colorbar(im, ax=axs[:, -1], shrink=0.6) + ############################################################################ # Note that there is a bit of a subtlety when specifying a single axes # as the parent. In the following, it might be desirable and expected @@ -458,6 +469,21 @@ def docomplicated(suptitle=None): ax2 = fig.add_axes(bb_ax2) ############################################################################### +# Manually turning off ``constrained_layout`` +# =========================================== +# +# ``constrained_layout`` usually adjusts the axes positions on each draw +# of the figure. If you want to get the spacing provided by +# ``constrained_layout`` but not have it update, then do the initial +# draw and then call ``fig.set_constrained_layout(False)``. +# This is potentially useful for animations where the tick labels may +# change length. +# +# Note that ``constrained_layout`` is turned off for ``ZOOM`` and ``PAN`` +# GUI events for the backends that use the toolbar. This prevents the +# axes from changing position during zooming and panning. +# +# # Limitations # ======================== #