diff --git a/doc/api/next_api_changes/deprecations/22134-OG.rst b/doc/api/next_api_changes/deprecations/22134-OG.rst new file mode 100644 index 000000000000..a0f60a843239 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22134-OG.rst @@ -0,0 +1,5 @@ +Modules ``tight_bbox`` and ``tight_layout`` deprecated +------------------------------------------------------ + +The modules ``matplotlib.tight_bbox`` and ``matplotlib.tight_layout`` are +considered internal and public access is deprecated. \ No newline at end of file diff --git a/doc/api/prev_api_changes/api_changes_1.4.x.rst b/doc/api/prev_api_changes/api_changes_1.4.x.rst index d0952784677c..c90a49fa6512 100644 --- a/doc/api/prev_api_changes/api_changes_1.4.x.rst +++ b/doc/api/prev_api_changes/api_changes_1.4.x.rst @@ -149,9 +149,9 @@ original location: ``drawRect`` from ``FigureCanvasQTAgg``; they were always an implementation detail of the (preserved) ``drawRectangle()`` function. -* The function signatures of `.tight_bbox.adjust_bbox` and - `.tight_bbox.process_figure_for_rasterizing` have been changed. A new - *fixed_dpi* parameter allows for overriding the ``figure.dpi`` setting +* The function signatures of ``matplotlib.tight_bbox.adjust_bbox`` and + ``matplotlib.tight_bbox.process_figure_for_rasterizing`` have been changed. + A new *fixed_dpi* parameter allows for overriding the ``figure.dpi`` setting instead of trying to deduce the intended behaviour from the file format. * Added support for horizontal/vertical axes padding to diff --git a/doc/api/prev_api_changes/api_changes_2.2.0.rst b/doc/api/prev_api_changes/api_changes_2.2.0.rst index 29ed03649fd8..c8a335172edd 100644 --- a/doc/api/prev_api_changes/api_changes_2.2.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.2.0.rst @@ -159,7 +159,7 @@ If `.MovieWriterRegistry` can't find the requested `.MovieWriter`, a more helpful `RuntimeError` message is now raised instead of the previously raised `KeyError`. -`~.tight_layout.auto_adjust_subplotpars` now raises `ValueError` +``matplotlib.tight_layout.auto_adjust_subplotpars`` now raises `ValueError` instead of `RuntimeError` when sizes of input lists don't match diff --git a/doc/api/prev_api_changes/api_changes_3.0.1.rst b/doc/api/prev_api_changes/api_changes_3.0.1.rst index d214ae9e6652..4b203cd04596 100644 --- a/doc/api/prev_api_changes/api_changes_3.0.1.rst +++ b/doc/api/prev_api_changes/api_changes_3.0.1.rst @@ -1,10 +1,10 @@ API Changes for 3.0.1 ===================== -`.tight_layout.auto_adjust_subplotpars` can return ``None`` now if the new -subplotparams will collapse axes to zero width or height. This prevents -``tight_layout`` from being executed. Similarly -`.tight_layout.get_tight_layout_figure` will return None. +``matplotlib.tight_layout.auto_adjust_subplotpars`` can return ``None`` now if +the new subplotparams will collapse axes to zero width or height. +This prevents ``tight_layout`` from being executed. Similarly +``matplotlib.tight_layout.get_tight_layout_figure`` will return None. To improve import (startup) time, private modules are now imported lazily. These modules are no longer available at these locations: diff --git a/lib/matplotlib/_tight_bbox.py b/lib/matplotlib/_tight_bbox.py new file mode 100644 index 000000000000..2a73624f0535 --- /dev/null +++ b/lib/matplotlib/_tight_bbox.py @@ -0,0 +1,85 @@ +""" +Helper module for the *bbox_inches* parameter in `.Figure.savefig`. +""" + +from matplotlib.transforms import Bbox, TransformedBbox, Affine2D + + +def adjust_bbox(fig, bbox_inches, fixed_dpi=None): + """ + Temporarily adjust the figure so that only the specified area + (bbox_inches) is saved. + + It modifies fig.bbox, fig.bbox_inches, + fig.transFigure._boxout, and fig.patch. While the figure size + changes, the scale of the original figure is conserved. A + function which restores the original values are returned. + """ + origBbox = fig.bbox + origBboxInches = fig.bbox_inches + orig_tight_layout = fig.get_tight_layout() + _boxout = fig.transFigure._boxout + + fig.set_tight_layout(False) + + old_aspect = [] + locator_list = [] + sentinel = object() + for ax in fig.axes: + locator_list.append(ax.get_axes_locator()) + current_pos = ax.get_position(original=False).frozen() + ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos) + # override the method that enforces the aspect ratio on the Axes + if 'apply_aspect' in ax.__dict__: + old_aspect.append(ax.apply_aspect) + else: + old_aspect.append(sentinel) + ax.apply_aspect = lambda pos=None: None + + def restore_bbox(): + for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): + ax.set_axes_locator(loc) + if aspect is sentinel: + # delete our no-op function which un-hides the original method + del ax.apply_aspect + else: + ax.apply_aspect = aspect + + fig.bbox = origBbox + fig.bbox_inches = origBboxInches + fig.set_tight_layout(orig_tight_layout) + fig.transFigure._boxout = _boxout + fig.transFigure.invalidate() + fig.patch.set_bounds(0, 0, 1, 1) + + if fixed_dpi is None: + fixed_dpi = fig.dpi + tr = Affine2D().scale(fixed_dpi) + dpi_scale = fixed_dpi / fig.dpi + + fig.bbox_inches = Bbox.from_bounds(0, 0, *bbox_inches.size) + x0, y0 = tr.transform(bbox_inches.p0) + w1, h1 = fig.bbox.size * dpi_scale + fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) + fig.transFigure.invalidate() + + fig.bbox = TransformedBbox(fig.bbox_inches, tr) + + fig.patch.set_bounds(x0 / w1, y0 / h1, + fig.bbox.width / w1, fig.bbox.height / h1) + + return restore_bbox + + +def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None): + """ + A function that needs to be called when figure dpi changes during the + drawing (e.g., rasterizing). It recovers the bbox and re-adjust it with + the new dpi. + """ + + bbox_inches, restore_bbox = bbox_inches_restore + restore_bbox() + r = adjust_bbox(fig, bbox_inches, fixed_dpi) + + return bbox_inches, r diff --git a/lib/matplotlib/_tight_layout.py b/lib/matplotlib/_tight_layout.py new file mode 100644 index 000000000000..81465f9b5db6 --- /dev/null +++ b/lib/matplotlib/_tight_layout.py @@ -0,0 +1,351 @@ +""" +Routines to adjust subplot params so that subplots are +nicely fit in the figure. In doing so, only axis labels, tick labels, axes +titles and offsetboxes that are anchored to axes are currently considered. + +Internally, this module assumes that the margins (left margin, etc.) which are +differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of +Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as +such cases as when left or right margin are affected by xlabel. +""" + +import numpy as np + +from matplotlib import _api, artist as martist, rcParams +from matplotlib.font_manager import FontProperties +from matplotlib.transforms import Bbox + + +def _auto_adjust_subplotpars( + fig, renderer, shape, span_pairs, subplot_list, + ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): + """ + Return a dict of subplot parameters to adjust spacing between subplots + or ``None`` if resulting axes would have zero height or width. + + Note that this function ignores geometry information of subplot itself, but + uses what is given by the *shape* and *subplot_list* parameters. Also, the + results could be incorrect if some subplots have ``adjustable=datalim``. + + Parameters + ---------- + shape : tuple[int, int] + Number of rows and columns of the grid. + span_pairs : list[tuple[slice, slice]] + List of rowspans and colspans occupied by each subplot. + subplot_list : list of subplots + List of subplots that will be used to calculate optimal subplot_params. + pad : float + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots, as a + fraction of the font size. Defaults to *pad*. + rect : tuple[float, float, float, float] + [left, bottom, right, top] in normalized (0, 1) figure coordinates. + """ + rows, cols = shape + + font_size_inch = ( + FontProperties(size=rcParams["font.size"]).get_size_in_points() / 72) + pad_inch = pad * font_size_inch + vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch + hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch + + if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0: + raise ValueError + + if rect is None: + margin_left = margin_bottom = margin_right = margin_top = None + else: + margin_left, margin_bottom, _right, _top = rect + margin_right = 1 - _right if _right else None + margin_top = 1 - _top if _top else None + + vspaces = np.zeros((rows + 1, cols)) + hspaces = np.zeros((rows, cols + 1)) + + if ax_bbox_list is None: + ax_bbox_list = [ + Bbox.union([ax.get_position(original=True) for ax in subplots]) + for subplots in subplot_list] + + for subplots, ax_bbox, (rowspan, colspan) in zip( + subplot_list, ax_bbox_list, span_pairs): + if all(not ax.get_visible() for ax in subplots): + continue + + bb = [] + for ax in subplots: + if ax.get_visible(): + bb += [martist._get_tightbbox_for_layout_only(ax, renderer)] + + tight_bbox_raw = Bbox.union(bb) + tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw) + + hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l + hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r + vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t + vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b + + fig_width_inch, fig_height_inch = fig.get_size_inches() + + # margins can be negative for axes with aspect applied, so use max(, 0) to + # make them nonnegative. + if not margin_left: + margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch + suplabel = fig._supylabel + if suplabel and suplabel.get_in_layout(): + rel_width = fig.transFigure.inverted().transform_bbox( + suplabel.get_window_extent(renderer)).width + margin_left += rel_width + pad_inch/fig_width_inch + if not margin_right: + margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch + if not margin_top: + margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch + if fig._suptitle and fig._suptitle.get_in_layout(): + rel_height = fig.transFigure.inverted().transform_bbox( + fig._suptitle.get_window_extent(renderer)).height + margin_top += rel_height + pad_inch/fig_height_inch + if not margin_bottom: + margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch + suplabel = fig._supxlabel + if suplabel and suplabel.get_in_layout(): + rel_height = fig.transFigure.inverted().transform_bbox( + suplabel.get_window_extent(renderer)).height + margin_bottom += rel_height + pad_inch/fig_height_inch + + if margin_left + margin_right >= 1: + _api.warn_external('Tight layout not applied. The left and right ' + 'margins cannot be made large enough to ' + 'accommodate all axes decorations.') + return None + if margin_bottom + margin_top >= 1: + _api.warn_external('Tight layout not applied. The bottom and top ' + 'margins cannot be made large enough to ' + 'accommodate all axes decorations.') + return None + + kwargs = dict(left=margin_left, + right=1 - margin_right, + bottom=margin_bottom, + top=1 - margin_top) + + if cols > 1: + hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch + # axes widths: + h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols + if h_axes < 0: + _api.warn_external('Tight layout not applied. tight_layout ' + 'cannot make axes width small enough to ' + 'accommodate all axes decorations') + return None + else: + kwargs["wspace"] = hspace / h_axes + if rows > 1: + vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch + v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows + if v_axes < 0: + _api.warn_external('Tight layout not applied. tight_layout ' + 'cannot make axes height small enough to ' + 'accommodate all axes decorations.') + return None + else: + kwargs["hspace"] = vspace / v_axes + + return kwargs + + +@_api.deprecated("3.5") +def auto_adjust_subplotpars( + fig, renderer, nrows_ncols, num1num2_list, subplot_list, + ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): + """ + Return a dict of subplot parameters to adjust spacing between subplots + or ``None`` if resulting axes would have zero height or width. + + Note that this function ignores geometry information of subplot + itself, but uses what is given by the *nrows_ncols* and *num1num2_list* + parameters. Also, the results could be incorrect if some subplots have + ``adjustable=datalim``. + + Parameters + ---------- + nrows_ncols : tuple[int, int] + Number of rows and number of columns of the grid. + num1num2_list : list[tuple[int, int]] + List of numbers specifying the area occupied by the subplot + subplot_list : list of subplots + List of subplots that will be used to calculate optimal subplot_params. + pad : float + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots, as a + fraction of the font size. Defaults to *pad*. + rect : tuple[float, float, float, float] + [left, bottom, right, top] in normalized (0, 1) figure coordinates. + """ + nrows, ncols = nrows_ncols + span_pairs = [] + for n1, n2 in num1num2_list: + if n2 is None: + n2 = n1 + span_pairs.append((slice(n1 // ncols, n2 // ncols + 1), + slice(n1 % ncols, n2 % ncols + 1))) + return _auto_adjust_subplotpars( + fig, renderer, nrows_ncols, num1num2_list, subplot_list, + ax_bbox_list, pad, h_pad, w_pad, rect) + + +def get_renderer(fig): + if fig._cachedRenderer: + return fig._cachedRenderer + else: + canvas = fig.canvas + if canvas and hasattr(canvas, "get_renderer"): + return canvas.get_renderer() + else: + from . import backend_bases + return backend_bases._get_renderer(fig) + + +def get_subplotspec_list(axes_list, grid_spec=None): + """ + Return a list of subplotspec from the given list of axes. + + For an instance of axes that does not support subplotspec, None is inserted + in the list. + + If grid_spec is given, None is inserted for those not from the given + grid_spec. + """ + subplotspec_list = [] + for ax in axes_list: + axes_or_locator = ax.get_axes_locator() + if axes_or_locator is None: + axes_or_locator = ax + + if hasattr(axes_or_locator, "get_subplotspec"): + subplotspec = axes_or_locator.get_subplotspec() + if subplotspec is not None: + subplotspec = subplotspec.get_topmost_subplotspec() + gs = subplotspec.get_gridspec() + if grid_spec is not None: + if gs != grid_spec: + subplotspec = None + elif gs.locally_modified_subplot_params(): + subplotspec = None + else: + subplotspec = None + + subplotspec_list.append(subplotspec) + + return subplotspec_list + + +def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, + pad=1.08, h_pad=None, w_pad=None, rect=None): + """ + Return subplot parameters for tight-layouted-figure with specified padding. + + Parameters + ---------- + fig : Figure + axes_list : list of Axes + subplotspec_list : list of `.SubplotSpec` + The subplotspecs of each axes. + renderer : renderer + pad : float + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots. Defaults to + *pad*. + rect : tuple[float, float, float, float], optional + (left, bottom, right, top) rectangle in normalized figure coordinates + that the whole subplots area (including labels) will fit into. + Defaults to using the entire figure. + + Returns + ------- + subplotspec or None + subplotspec kwargs to be passed to `.Figure.subplots_adjust` or + None if tight_layout could not be accomplished. + """ + + # Multiple axes can share same subplotspec (e.g., if using axes_grid1); + # we need to group them together. + ss_to_subplots = {ss: [] for ss in subplotspec_list} + for ax, ss in zip(axes_list, subplotspec_list): + ss_to_subplots[ss].append(ax) + ss_to_subplots.pop(None, None) # Skip subplotspec == None. + if not ss_to_subplots: + return {} + subplot_list = list(ss_to_subplots.values()) + ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots] + + max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots) + max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots) + + span_pairs = [] + for ss in ss_to_subplots: + # The intent here is to support axes from different gridspecs where + # one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4), + # but this doesn't actually work because the computed wspace, in + # relative-axes-height, corresponds to different physical spacings for + # the 2-row grid and the 4-row grid. Still, this code is left, mostly + # for backcompat. + rows, cols = ss.get_gridspec().get_geometry() + div_row, mod_row = divmod(max_nrows, rows) + div_col, mod_col = divmod(max_ncols, cols) + if mod_row != 0: + _api.warn_external('tight_layout not applied: number of rows ' + 'in subplot specifications must be ' + 'multiples of one another.') + return {} + if mod_col != 0: + _api.warn_external('tight_layout not applied: number of ' + 'columns in subplot specifications must be ' + 'multiples of one another.') + return {} + span_pairs.append(( + slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row), + slice(ss.colspan.start * div_col, ss.colspan.stop * div_col))) + + kwargs = _auto_adjust_subplotpars(fig, renderer, + shape=(max_nrows, max_ncols), + span_pairs=span_pairs, + subplot_list=subplot_list, + ax_bbox_list=ax_bbox_list, + pad=pad, h_pad=h_pad, w_pad=w_pad) + + # kwargs can be none if tight_layout fails... + if rect is not None and kwargs is not None: + # if rect is given, the whole subplots area (including + # labels) will fit into the rect instead of the + # figure. Note that the rect argument of + # *auto_adjust_subplotpars* specify the area that will be + # covered by the total area of axes.bbox. Thus we call + # auto_adjust_subplotpars twice, where the second run + # with adjusted rect parameters. + + left, bottom, right, top = rect + if left is not None: + left += kwargs["left"] + if bottom is not None: + bottom += kwargs["bottom"] + if right is not None: + right -= (1 - kwargs["right"]) + if top is not None: + top -= (1 - kwargs["top"]) + + kwargs = _auto_adjust_subplotpars(fig, renderer, + shape=(max_nrows, max_ncols), + span_pairs=span_pairs, + subplot_list=subplot_list, + ax_bbox_list=ax_bbox_list, + pad=pad, h_pad=h_pad, w_pad=w_pad, + rect=(left, bottom, right, top)) + + return kwargs diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 541ffcf84db0..2bb397068160 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -44,7 +44,7 @@ import matplotlib as mpl from matplotlib import ( _api, backend_tools as tools, cbook, colors, docstring, textpath, - tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) + _tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) from matplotlib._pylab_helpers import Gcf from matplotlib.backend_managers import ToolManager from matplotlib.cbook import _setattr_cm @@ -2248,7 +2248,7 @@ def print_figure( bbox_inches = bbox_inches.padded(pad_inches) # call adjust_bbox to save only the given area - restore_bbox = tight_bbox.adjust_bbox( + restore_bbox = _tight_bbox.adjust_bbox( self.figure, bbox_inches, self.figure.canvas.fixed_dpi) _bbox_inches_restore = (bbox_inches, restore_bbox) diff --git a/lib/matplotlib/backends/backend_mixed.py b/lib/matplotlib/backends/backend_mixed.py index 54ca8f81ba02..593c2bb3c0d8 100644 --- a/lib/matplotlib/backends/backend_mixed.py +++ b/lib/matplotlib/backends/backend_mixed.py @@ -2,7 +2,7 @@ from matplotlib import cbook from matplotlib.backends.backend_agg import RendererAgg -from matplotlib.tight_bbox import process_figure_for_rasterizing +from matplotlib._tight_bbox import process_figure_for_rasterizing class MixedModeRenderer: diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 50f76903cae4..f8d31df05d8a 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -282,7 +282,7 @@ def _get_nth_label_width(self, nth): figure=fig, size=self.labelFontSizeList[nth], fontproperties=self.labelFontProps) - .get_window_extent(mpl.tight_layout.get_renderer(fig)).width) + .get_window_extent(mpl._tight_layout.get_renderer(fig)).width) @_api.deprecated("3.5") def get_label_width(self, lev, fmt, fsize): @@ -292,7 +292,7 @@ def get_label_width(self, lev, fmt, fsize): fig = self.axes.figure width = (text.Text(0, 0, lev, figure=fig, size=fsize, fontproperties=self.labelFontProps) - .get_window_extent(mpl.tight_layout.get_renderer(fig)).width) + .get_window_extent(mpl._tight_layout.get_renderer(fig)).width) width *= 72 / fig.dpi return width diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index fb57acec9f1a..a12afead3d74 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3181,7 +3181,7 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): .pyplot.tight_layout """ from contextlib import nullcontext - from .tight_layout import ( + from ._tight_layout import ( get_subplotspec_list, get_tight_layout_figure) subplotspec_list = get_subplotspec_list(self.axes) if None in subplotspec_list: diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 2aa59ff22b9f..90d0b57b609b 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -18,7 +18,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, _pylab_helpers, tight_layout, rcParams +from matplotlib import _api, _pylab_helpers, _tight_layout, rcParams from matplotlib.transforms import Bbox _log = logging.getLogger(__name__) @@ -466,7 +466,7 @@ def tight_layout(self, figure, renderer=None, fit into. """ - subplotspec_list = tight_layout.get_subplotspec_list( + subplotspec_list = _tight_layout.get_subplotspec_list( figure.axes, grid_spec=self) if None in subplotspec_list: _api.warn_external("This figure includes Axes that are not " @@ -474,9 +474,9 @@ def tight_layout(self, figure, renderer=None, "might be incorrect.") if renderer is None: - renderer = tight_layout.get_renderer(figure) + renderer = _tight_layout.get_renderer(figure) - kwargs = tight_layout.get_tight_layout_figure( + kwargs = _tight_layout.get_tight_layout_figure( figure, figure.axes, subplotspec_list, renderer, pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) if kwargs: diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 2a73624f0535..a87add58417f 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -1,85 +1,5 @@ -""" -Helper module for the *bbox_inches* parameter in `.Figure.savefig`. -""" - -from matplotlib.transforms import Bbox, TransformedBbox, Affine2D - - -def adjust_bbox(fig, bbox_inches, fixed_dpi=None): - """ - Temporarily adjust the figure so that only the specified area - (bbox_inches) is saved. - - It modifies fig.bbox, fig.bbox_inches, - fig.transFigure._boxout, and fig.patch. While the figure size - changes, the scale of the original figure is conserved. A - function which restores the original values are returned. - """ - origBbox = fig.bbox - origBboxInches = fig.bbox_inches - orig_tight_layout = fig.get_tight_layout() - _boxout = fig.transFigure._boxout - - fig.set_tight_layout(False) - - old_aspect = [] - locator_list = [] - sentinel = object() - for ax in fig.axes: - locator_list.append(ax.get_axes_locator()) - current_pos = ax.get_position(original=False).frozen() - ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos) - # override the method that enforces the aspect ratio on the Axes - if 'apply_aspect' in ax.__dict__: - old_aspect.append(ax.apply_aspect) - else: - old_aspect.append(sentinel) - ax.apply_aspect = lambda pos=None: None - - def restore_bbox(): - for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): - ax.set_axes_locator(loc) - if aspect is sentinel: - # delete our no-op function which un-hides the original method - del ax.apply_aspect - else: - ax.apply_aspect = aspect - - fig.bbox = origBbox - fig.bbox_inches = origBboxInches - fig.set_tight_layout(orig_tight_layout) - fig.transFigure._boxout = _boxout - fig.transFigure.invalidate() - fig.patch.set_bounds(0, 0, 1, 1) - - if fixed_dpi is None: - fixed_dpi = fig.dpi - tr = Affine2D().scale(fixed_dpi) - dpi_scale = fixed_dpi / fig.dpi - - fig.bbox_inches = Bbox.from_bounds(0, 0, *bbox_inches.size) - x0, y0 = tr.transform(bbox_inches.p0) - w1, h1 = fig.bbox.size * dpi_scale - fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) - fig.transFigure.invalidate() - - fig.bbox = TransformedBbox(fig.bbox_inches, tr) - - fig.patch.set_bounds(x0 / w1, y0 / h1, - fig.bbox.width / w1, fig.bbox.height / h1) - - return restore_bbox - - -def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None): - """ - A function that needs to be called when figure dpi changes during the - drawing (e.g., rasterizing). It recovers the bbox and re-adjust it with - the new dpi. - """ - - bbox_inches, restore_bbox = bbox_inches_restore - restore_bbox() - r = adjust_bbox(fig, bbox_inches, fixed_dpi) - - return bbox_inches, r +from matplotlib._tight_bbox import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated( + "3.6", message="The module %(name)s is deprecated since %(since)s.", + name=f"{__name__}") diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 81465f9b5db6..3cc7d32e352a 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -1,351 +1,5 @@ -""" -Routines to adjust subplot params so that subplots are -nicely fit in the figure. In doing so, only axis labels, tick labels, axes -titles and offsetboxes that are anchored to axes are currently considered. - -Internally, this module assumes that the margins (left margin, etc.) which are -differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of -Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as -such cases as when left or right margin are affected by xlabel. -""" - -import numpy as np - -from matplotlib import _api, artist as martist, rcParams -from matplotlib.font_manager import FontProperties -from matplotlib.transforms import Bbox - - -def _auto_adjust_subplotpars( - fig, renderer, shape, span_pairs, subplot_list, - ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Return a dict of subplot parameters to adjust spacing between subplots - or ``None`` if resulting axes would have zero height or width. - - Note that this function ignores geometry information of subplot itself, but - uses what is given by the *shape* and *subplot_list* parameters. Also, the - results could be incorrect if some subplots have ``adjustable=datalim``. - - Parameters - ---------- - shape : tuple[int, int] - Number of rows and columns of the grid. - span_pairs : list[tuple[slice, slice]] - List of rowspans and colspans occupied by each subplot. - subplot_list : list of subplots - List of subplots that will be used to calculate optimal subplot_params. - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font size. - h_pad, w_pad : float - Padding (height/width) between edges of adjacent subplots, as a - fraction of the font size. Defaults to *pad*. - rect : tuple[float, float, float, float] - [left, bottom, right, top] in normalized (0, 1) figure coordinates. - """ - rows, cols = shape - - font_size_inch = ( - FontProperties(size=rcParams["font.size"]).get_size_in_points() / 72) - pad_inch = pad * font_size_inch - vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch - hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch - - if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0: - raise ValueError - - if rect is None: - margin_left = margin_bottom = margin_right = margin_top = None - else: - margin_left, margin_bottom, _right, _top = rect - margin_right = 1 - _right if _right else None - margin_top = 1 - _top if _top else None - - vspaces = np.zeros((rows + 1, cols)) - hspaces = np.zeros((rows, cols + 1)) - - if ax_bbox_list is None: - ax_bbox_list = [ - Bbox.union([ax.get_position(original=True) for ax in subplots]) - for subplots in subplot_list] - - for subplots, ax_bbox, (rowspan, colspan) in zip( - subplot_list, ax_bbox_list, span_pairs): - if all(not ax.get_visible() for ax in subplots): - continue - - bb = [] - for ax in subplots: - if ax.get_visible(): - bb += [martist._get_tightbbox_for_layout_only(ax, renderer)] - - tight_bbox_raw = Bbox.union(bb) - tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw) - - hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l - hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r - vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t - vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b - - fig_width_inch, fig_height_inch = fig.get_size_inches() - - # margins can be negative for axes with aspect applied, so use max(, 0) to - # make them nonnegative. - if not margin_left: - margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch - suplabel = fig._supylabel - if suplabel and suplabel.get_in_layout(): - rel_width = fig.transFigure.inverted().transform_bbox( - suplabel.get_window_extent(renderer)).width - margin_left += rel_width + pad_inch/fig_width_inch - if not margin_right: - margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch - if not margin_top: - margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch - if fig._suptitle and fig._suptitle.get_in_layout(): - rel_height = fig.transFigure.inverted().transform_bbox( - fig._suptitle.get_window_extent(renderer)).height - margin_top += rel_height + pad_inch/fig_height_inch - if not margin_bottom: - margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch - suplabel = fig._supxlabel - if suplabel and suplabel.get_in_layout(): - rel_height = fig.transFigure.inverted().transform_bbox( - suplabel.get_window_extent(renderer)).height - margin_bottom += rel_height + pad_inch/fig_height_inch - - if margin_left + margin_right >= 1: - _api.warn_external('Tight layout not applied. The left and right ' - 'margins cannot be made large enough to ' - 'accommodate all axes decorations.') - return None - if margin_bottom + margin_top >= 1: - _api.warn_external('Tight layout not applied. The bottom and top ' - 'margins cannot be made large enough to ' - 'accommodate all axes decorations.') - return None - - kwargs = dict(left=margin_left, - right=1 - margin_right, - bottom=margin_bottom, - top=1 - margin_top) - - if cols > 1: - hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch - # axes widths: - h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols - if h_axes < 0: - _api.warn_external('Tight layout not applied. tight_layout ' - 'cannot make axes width small enough to ' - 'accommodate all axes decorations') - return None - else: - kwargs["wspace"] = hspace / h_axes - if rows > 1: - vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch - v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows - if v_axes < 0: - _api.warn_external('Tight layout not applied. tight_layout ' - 'cannot make axes height small enough to ' - 'accommodate all axes decorations.') - return None - else: - kwargs["hspace"] = vspace / v_axes - - return kwargs - - -@_api.deprecated("3.5") -def auto_adjust_subplotpars( - fig, renderer, nrows_ncols, num1num2_list, subplot_list, - ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Return a dict of subplot parameters to adjust spacing between subplots - or ``None`` if resulting axes would have zero height or width. - - Note that this function ignores geometry information of subplot - itself, but uses what is given by the *nrows_ncols* and *num1num2_list* - parameters. Also, the results could be incorrect if some subplots have - ``adjustable=datalim``. - - Parameters - ---------- - nrows_ncols : tuple[int, int] - Number of rows and number of columns of the grid. - num1num2_list : list[tuple[int, int]] - List of numbers specifying the area occupied by the subplot - subplot_list : list of subplots - List of subplots that will be used to calculate optimal subplot_params. - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font size. - h_pad, w_pad : float - Padding (height/width) between edges of adjacent subplots, as a - fraction of the font size. Defaults to *pad*. - rect : tuple[float, float, float, float] - [left, bottom, right, top] in normalized (0, 1) figure coordinates. - """ - nrows, ncols = nrows_ncols - span_pairs = [] - for n1, n2 in num1num2_list: - if n2 is None: - n2 = n1 - span_pairs.append((slice(n1 // ncols, n2 // ncols + 1), - slice(n1 % ncols, n2 % ncols + 1))) - return _auto_adjust_subplotpars( - fig, renderer, nrows_ncols, num1num2_list, subplot_list, - ax_bbox_list, pad, h_pad, w_pad, rect) - - -def get_renderer(fig): - if fig._cachedRenderer: - return fig._cachedRenderer - else: - canvas = fig.canvas - if canvas and hasattr(canvas, "get_renderer"): - return canvas.get_renderer() - else: - from . import backend_bases - return backend_bases._get_renderer(fig) - - -def get_subplotspec_list(axes_list, grid_spec=None): - """ - Return a list of subplotspec from the given list of axes. - - For an instance of axes that does not support subplotspec, None is inserted - in the list. - - If grid_spec is given, None is inserted for those not from the given - grid_spec. - """ - subplotspec_list = [] - for ax in axes_list: - axes_or_locator = ax.get_axes_locator() - if axes_or_locator is None: - axes_or_locator = ax - - if hasattr(axes_or_locator, "get_subplotspec"): - subplotspec = axes_or_locator.get_subplotspec() - if subplotspec is not None: - subplotspec = subplotspec.get_topmost_subplotspec() - gs = subplotspec.get_gridspec() - if grid_spec is not None: - if gs != grid_spec: - subplotspec = None - elif gs.locally_modified_subplot_params(): - subplotspec = None - else: - subplotspec = None - - subplotspec_list.append(subplotspec) - - return subplotspec_list - - -def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, - pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Return subplot parameters for tight-layouted-figure with specified padding. - - Parameters - ---------- - fig : Figure - axes_list : list of Axes - subplotspec_list : list of `.SubplotSpec` - The subplotspecs of each axes. - renderer : renderer - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font size. - h_pad, w_pad : float - Padding (height/width) between edges of adjacent subplots. Defaults to - *pad*. - rect : tuple[float, float, float, float], optional - (left, bottom, right, top) rectangle in normalized figure coordinates - that the whole subplots area (including labels) will fit into. - Defaults to using the entire figure. - - Returns - ------- - subplotspec or None - subplotspec kwargs to be passed to `.Figure.subplots_adjust` or - None if tight_layout could not be accomplished. - """ - - # Multiple axes can share same subplotspec (e.g., if using axes_grid1); - # we need to group them together. - ss_to_subplots = {ss: [] for ss in subplotspec_list} - for ax, ss in zip(axes_list, subplotspec_list): - ss_to_subplots[ss].append(ax) - ss_to_subplots.pop(None, None) # Skip subplotspec == None. - if not ss_to_subplots: - return {} - subplot_list = list(ss_to_subplots.values()) - ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots] - - max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots) - max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots) - - span_pairs = [] - for ss in ss_to_subplots: - # The intent here is to support axes from different gridspecs where - # one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4), - # but this doesn't actually work because the computed wspace, in - # relative-axes-height, corresponds to different physical spacings for - # the 2-row grid and the 4-row grid. Still, this code is left, mostly - # for backcompat. - rows, cols = ss.get_gridspec().get_geometry() - div_row, mod_row = divmod(max_nrows, rows) - div_col, mod_col = divmod(max_ncols, cols) - if mod_row != 0: - _api.warn_external('tight_layout not applied: number of rows ' - 'in subplot specifications must be ' - 'multiples of one another.') - return {} - if mod_col != 0: - _api.warn_external('tight_layout not applied: number of ' - 'columns in subplot specifications must be ' - 'multiples of one another.') - return {} - span_pairs.append(( - slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row), - slice(ss.colspan.start * div_col, ss.colspan.stop * div_col))) - - kwargs = _auto_adjust_subplotpars(fig, renderer, - shape=(max_nrows, max_ncols), - span_pairs=span_pairs, - subplot_list=subplot_list, - ax_bbox_list=ax_bbox_list, - pad=pad, h_pad=h_pad, w_pad=w_pad) - - # kwargs can be none if tight_layout fails... - if rect is not None and kwargs is not None: - # if rect is given, the whole subplots area (including - # labels) will fit into the rect instead of the - # figure. Note that the rect argument of - # *auto_adjust_subplotpars* specify the area that will be - # covered by the total area of axes.bbox. Thus we call - # auto_adjust_subplotpars twice, where the second run - # with adjusted rect parameters. - - left, bottom, right, top = rect - if left is not None: - left += kwargs["left"] - if bottom is not None: - bottom += kwargs["bottom"] - if right is not None: - right -= (1 - kwargs["right"]) - if top is not None: - top -= (1 - kwargs["top"]) - - kwargs = _auto_adjust_subplotpars(fig, renderer, - shape=(max_nrows, max_ncols), - span_pairs=span_pairs, - subplot_list=subplot_list, - ax_bbox_list=ax_bbox_list, - pad=pad, h_pad=h_pad, w_pad=w_pad, - rect=(left, bottom, right, top)) - - return kwargs +from matplotlib._tight_layout import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated( + "3.6", message="The module %(name)s is deprecated since %(since)s.", + name=f"{__name__}")