From 8d9ca906ca23f98b3781aca64f9ada4b601fef48 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 2 Jun 2022 19:03:55 +0200 Subject: [PATCH] ENH: add width_ratios and height_ratios to subplots --- .../next_whats_new/width_height_ratios.rst | 7 +++ lib/matplotlib/figure.py | 52 ++++++++++++++++++- lib/matplotlib/pyplot.py | 45 ++++++++++++---- lib/matplotlib/tests/test_subplots.py | 41 ++++++++++++++- tutorials/provisional/mosaic.py | 12 ++--- 5 files changed, 138 insertions(+), 19 deletions(-) create mode 100644 doc/users/next_whats_new/width_height_ratios.rst diff --git a/doc/users/next_whats_new/width_height_ratios.rst b/doc/users/next_whats_new/width_height_ratios.rst new file mode 100644 index 000000000000..017c34a55cc4 --- /dev/null +++ b/doc/users/next_whats_new/width_height_ratios.rst @@ -0,0 +1,7 @@ +``subplots``, ``subplot_mosaic`` accept *height_ratios* and *width_ratios* arguments +------------------------------------------------------------------------------------ + +The relative width and height of columns and rows in `~.Figure.subplots` and +`~.Figure.subplot_mosaic` can be controlled by passing *height_ratios* and +*width_ratios* keyword arguments to the methods. Previously, this required +passing the ratios in *gridspec_kws* arguments. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f8aa99d48a09..dedaaa7dcf5a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -764,7 +764,8 @@ def _add_axes_internal(self, ax, key): return ax def subplots(self, nrows=1, ncols=1, *, sharex=False, sharey=False, - squeeze=True, subplot_kw=None, gridspec_kw=None): + squeeze=True, width_ratios=None, height_ratios=None, + subplot_kw=None, gridspec_kw=None): """ Add a set of subplots to this figure. @@ -807,6 +808,18 @@ def subplots(self, nrows=1, ncols=1, *, sharex=False, sharey=False, is always a 2D array containing Axes instances, even if it ends up being 1x1. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Equivalent + to ``gridspec_kw={'height_ratios': [...]}``. + subplot_kw : dict, optional Dict with keywords passed to the `.Figure.add_subplot` call used to create each subplot. @@ -871,6 +884,17 @@ def subplots(self, nrows=1, ncols=1, *, sharex=False, sharey=False, """ if gridspec_kw is None: gridspec_kw = {} + if height_ratios is not None: + if 'height_ratios' in gridspec_kw: + raise ValueError("'height_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['height_ratios'] = height_ratios + if width_ratios is not None: + if 'width_ratios' in gridspec_kw: + raise ValueError("'width_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['width_ratios'] = width_ratios + gs = self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) axs = gs.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, subplot_kw=subplot_kw) @@ -1683,7 +1707,8 @@ def _normalize_grid_string(layout): return [list(ln) for ln in layout.strip('\n').split('\n')] def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False, - subplot_kw=None, gridspec_kw=None, empty_sentinel='.'): + width_ratios=None, height_ratios=None, + empty_sentinel='.', subplot_kw=None, gridspec_kw=None): """ Build a layout of Axes based on ASCII art or nested lists. @@ -1739,6 +1764,18 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False, units behave as for `subplots`. If False, each subplot's x- or y-axis will be independent. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Equivalent + to ``gridspec_kw={'height_ratios': [...]}``. + subplot_kw : dict, optional Dictionary with keywords passed to the `.Figure.add_subplot` call used to create each subplot. @@ -1763,6 +1800,17 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False, """ subplot_kw = subplot_kw or {} gridspec_kw = gridspec_kw or {} + if height_ratios is not None: + if 'height_ratios' in gridspec_kw: + raise ValueError("'height_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['height_ratios'] = height_ratios + if width_ratios is not None: + if 'width_ratios' in gridspec_kw: + raise ValueError("'width_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['width_ratios'] = width_ratios + # special-case string input if isinstance(mosaic, str): mosaic = self._normalize_grid_string(mosaic) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e3c66ea67cad..faa023f8c082 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1323,6 +1323,7 @@ def subplot(*args, **kwargs): def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True, + width_ratios=None, height_ratios=None, subplot_kw=None, gridspec_kw=None, **fig_kw): """ Create a figure and a set of subplots. @@ -1368,6 +1369,18 @@ def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True, always a 2D array containing Axes instances, even if it ends up being 1x1. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Convenience + for ``gridspec_kw={'height_ratios': [...]}``. + subplot_kw : dict, optional Dict with keywords passed to the `~matplotlib.figure.Figure.add_subplot` call used to create each @@ -1458,13 +1471,14 @@ def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True, fig = figure(**fig_kw) axs = fig.subplots(nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey, squeeze=squeeze, subplot_kw=subplot_kw, - gridspec_kw=gridspec_kw) + gridspec_kw=gridspec_kw, height_ratios=height_ratios, + width_ratios=width_ratios) return fig, axs def subplot_mosaic(mosaic, *, sharex=False, sharey=False, - subplot_kw=None, gridspec_kw=None, empty_sentinel='.', - **fig_kw): + width_ratios=None, height_ratios=None, empty_sentinel='.', + subplot_kw=None, gridspec_kw=None, **fig_kw): """ Build a layout of Axes based on ASCII art or nested lists. @@ -1515,13 +1529,17 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False, behave as for `subplots`. If False, each subplot's x- or y-axis will be independent. - subplot_kw : dict, optional - Dictionary with keywords passed to the `.Figure.add_subplot` call - used to create each subplot. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Convenience + for ``gridspec_kw={'width_ratios': [...]}``. - gridspec_kw : dict, optional - Dictionary with keywords passed to the `.GridSpec` constructor used - to create the grid the subplots are placed on. + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Convenience + for ``gridspec_kw={'height_ratios': [...]}``. empty_sentinel : object, optional Entry in the layout to mean "leave this space empty". Defaults @@ -1529,6 +1547,14 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False, `inspect.cleandoc` to remove leading white space, which may interfere with using white-space as the empty sentinel. + subplot_kw : dict, optional + Dictionary with keywords passed to the `.Figure.add_subplot` call + used to create each subplot. + + gridspec_kw : dict, optional + Dictionary with keywords passed to the `.GridSpec` constructor used + to create the grid the subplots are placed on. + **fig_kw All additional keyword arguments are passed to the `.pyplot.figure` call. @@ -1547,6 +1573,7 @@ def subplot_mosaic(mosaic, *, sharex=False, sharey=False, fig = figure(**fig_kw) ax_dict = fig.subplot_mosaic( mosaic, sharex=sharex, sharey=sharey, + height_ratios=height_ratios, width_ratios=width_ratios, subplot_kw=subplot_kw, gridspec_kw=gridspec_kw, empty_sentinel=empty_sentinel ) diff --git a/lib/matplotlib/tests/test_subplots.py b/lib/matplotlib/tests/test_subplots.py index 8d707e014749..f299440ef53e 100644 --- a/lib/matplotlib/tests/test_subplots.py +++ b/lib/matplotlib/tests/test_subplots.py @@ -4,7 +4,7 @@ import pytest import matplotlib.pyplot as plt -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import check_figures_equal, image_comparison import matplotlib.axes as maxes @@ -212,3 +212,42 @@ def test_dont_mutate_kwargs(): def test_subplot_factory_reapplication(): assert maxes.subplot_class_factory(maxes.Axes) is maxes.Subplot assert maxes.subplot_class_factory(maxes.Subplot) is maxes.Subplot + + +@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]]) +@pytest.mark.parametrize("height_ratios", [None, [1, 2]]) +@check_figures_equal(extensions=['png']) +def test_width_and_height_ratios(fig_test, fig_ref, + height_ratios, width_ratios): + fig_test.subplots(2, 3, height_ratios=height_ratios, + width_ratios=width_ratios) + fig_ref.subplots(2, 3, gridspec_kw={ + 'height_ratios': height_ratios, + 'width_ratios': width_ratios}) + + +@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]]) +@pytest.mark.parametrize("height_ratios", [None, [1, 2]]) +@check_figures_equal(extensions=['png']) +def test_width_and_height_ratios_mosaic(fig_test, fig_ref, + height_ratios, width_ratios): + mosaic_spec = [['A', 'B', 'B'], ['A', 'C', 'D']] + fig_test.subplot_mosaic(mosaic_spec, height_ratios=height_ratios, + width_ratios=width_ratios) + fig_ref.subplot_mosaic(mosaic_spec, gridspec_kw={ + 'height_ratios': height_ratios, + 'width_ratios': width_ratios}) + + +@pytest.mark.parametrize('method,args', [ + ('subplots', (2, 3)), + ('subplot_mosaic', ('abc;def', )) + ] +) +def test_ratio_overlapping_kws(method, args): + with pytest.raises(ValueError, match='height_ratios'): + getattr(plt, method)(*args, height_ratios=[1, 2], + gridspec_kw={'height_ratios': [1, 2]}) + with pytest.raises(ValueError, match='width_ratios'): + getattr(plt, method)(*args, width_ratios=[1, 2, 3], + gridspec_kw={'width_ratios': [1, 2, 3]}) diff --git a/tutorials/provisional/mosaic.py b/tutorials/provisional/mosaic.py index 05623e852ed7..202ada6eb332 100644 --- a/tutorials/provisional/mosaic.py +++ b/tutorials/provisional/mosaic.py @@ -219,12 +219,10 @@ def identify_axes(ax_dict, fontsize=48): bAc .d. """, - gridspec_kw={ - # set the height ratios between the rows - "height_ratios": [1, 3.5, 1], - # set the width ratios between the columns - "width_ratios": [1, 3.5, 1], - }, + # set the height ratios between the rows + height_ratios=[1, 3.5, 1], + # set the width ratios between the columns + width_ratios=[1, 3.5, 1], ) identify_axes(axd) @@ -301,7 +299,7 @@ def identify_axes(ax_dict, fontsize=48): ["main", "BLANK"], ], empty_sentinel="BLANK", - gridspec_kw={"width_ratios": [2, 1]}, + width_ratios=[2, 1], ) identify_axes(axd)