123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- """
- This module provides 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, it assumes that the margins (left_margin, etc.) which are
- differences between ax.get_tightbbox and ax.bbox are independent of axes
- position. This may fail if Axes.adjustable is datalim. Also, This will fail
- for some cases (for example, left or right margin is affected by xlabel).
- """
- from matplotlib import cbook, rcParams
- from matplotlib.font_manager import FontProperties
- from matplotlib.transforms import TransformedBbox, Bbox
- def _get_left(tight_bbox, axes_bbox):
- return axes_bbox.xmin - tight_bbox.xmin
- def _get_right(tight_bbox, axes_bbox):
- return tight_bbox.xmax - axes_bbox.xmax
- def _get_bottom(tight_bbox, axes_bbox):
- return axes_bbox.ymin - tight_bbox.ymin
- def _get_top(tight_bbox, axes_bbox):
- return tight_bbox.ymax - axes_bbox.ymax
- 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[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.
- """
- rows, cols = nrows_ncols
- font_size_inches = (
- FontProperties(size=rcParams["font.size"]).get_size_in_points() / 72)
- pad_inches = pad * font_size_inches
- if h_pad is not None:
- vpad_inches = h_pad * font_size_inches
- else:
- vpad_inches = pad_inches
- if w_pad is not None:
- hpad_inches = w_pad * font_size_inches
- else:
- hpad_inches = pad_inches
- if len(num1num2_list) != 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
- if _right:
- margin_right = 1 - _right
- else:
- margin_right = None
- if _top:
- margin_top = 1 - _top
- else:
- margin_top = None
- vspaces = [[] for i in range((rows + 1) * cols)]
- hspaces = [[] for i in range(rows * (cols + 1))]
- union = Bbox.union
- if ax_bbox_list is None:
- ax_bbox_list = [
- union([ax.get_position(original=True) for ax in subplots])
- for subplots in subplot_list]
- for subplots, ax_bbox, (num1, num2) in zip(subplot_list,
- ax_bbox_list,
- num1num2_list):
- if all(not ax.get_visible() for ax in subplots):
- continue
- tight_bbox_raw = union([ax.get_tightbbox(renderer) for ax in subplots
- if ax.get_visible()])
- tight_bbox = TransformedBbox(tight_bbox_raw,
- fig.transFigure.inverted())
- row1, col1 = divmod(num1, cols)
- if num2 is None:
- # left
- hspaces[row1 * (cols + 1) + col1].append(
- _get_left(tight_bbox, ax_bbox))
- # right
- hspaces[row1 * (cols + 1) + (col1 + 1)].append(
- _get_right(tight_bbox, ax_bbox))
- # top
- vspaces[row1 * cols + col1].append(
- _get_top(tight_bbox, ax_bbox))
- # bottom
- vspaces[(row1 + 1) * cols + col1].append(
- _get_bottom(tight_bbox, ax_bbox))
- else:
- row2, col2 = divmod(num2, cols)
- for row_i in range(row1, row2 + 1):
- # left
- hspaces[row_i * (cols + 1) + col1].append(
- _get_left(tight_bbox, ax_bbox))
- # right
- hspaces[row_i * (cols + 1) + (col2 + 1)].append(
- _get_right(tight_bbox, ax_bbox))
- for col_i in range(col1, col2 + 1):
- # top
- vspaces[row1 * cols + col_i].append(
- _get_top(tight_bbox, ax_bbox))
- # bottom
- vspaces[(row2 + 1) * cols + col_i].append(
- _get_bottom(tight_bbox, ax_bbox))
- fig_width_inch, fig_height_inch = fig.get_size_inches()
- # margins can be negative for axes with aspect applied. And we
- # append + [0] to make minimum margins 0
- if not margin_left:
- margin_left = max([sum(s) for s in hspaces[::cols + 1]] + [0])
- margin_left += pad_inches / fig_width_inch
- if not margin_right:
- margin_right = max([sum(s) for s in hspaces[cols::cols + 1]] + [0])
- margin_right += pad_inches / fig_width_inch
- if not margin_top:
- margin_top = max([sum(s) for s in vspaces[:cols]] + [0])
- margin_top += pad_inches / fig_height_inch
- if not margin_bottom:
- margin_bottom = max([sum(s) for s in vspaces[-cols:]] + [0])
- margin_bottom += pad_inches / fig_height_inch
- if margin_left + margin_right >= 1:
- cbook._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:
- cbook._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 = (
- max(sum(s)
- for i in range(rows)
- for s in hspaces[i * (cols + 1) + 1:(i + 1) * (cols + 1) - 1])
- + hpad_inches / fig_width_inch)
- # axes widths:
- h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols
- if h_axes < 0:
- cbook._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 = (max(sum(s) for s in vspaces[cols:-cols])
- + vpad_inches / fig_height_inch)
- v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows
- if v_axes < 0:
- cbook._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
- def get_renderer(fig):
- if fig._cachedRenderer:
- renderer = fig._cachedRenderer
- else:
- canvas = fig.canvas
- if canvas and hasattr(canvas, "get_renderer"):
- renderer = canvas.get_renderer()
- else: # Some noninteractive backends have no renderer until draw time.
- cbook._warn_external("tight_layout: falling back to Agg renderer")
- from matplotlib.backends.backend_agg import FigureCanvasAgg
- canvas = FigureCanvasAgg(fig)
- renderer = canvas.get_renderer()
- return renderer
- 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()
- 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.
- """
- subplot_list = []
- nrows_list = []
- ncols_list = []
- ax_bbox_list = []
- # Multiple axes can share same subplot_interface (e.g., axes_grid1); thus
- # we need to join them together.
- subplot_dict = {}
- subplotspec_list2 = []
- for ax, subplotspec in zip(axes_list, subplotspec_list):
- if subplotspec is None:
- continue
- subplots = subplot_dict.setdefault(subplotspec, [])
- if not subplots:
- myrows, mycols, _, _ = subplotspec.get_geometry()
- nrows_list.append(myrows)
- ncols_list.append(mycols)
- subplotspec_list2.append(subplotspec)
- subplot_list.append(subplots)
- ax_bbox_list.append(subplotspec.get_position(fig))
- subplots.append(ax)
- if len(nrows_list) == 0 or len(ncols_list) == 0:
- return {}
- max_nrows = max(nrows_list)
- max_ncols = max(ncols_list)
- num1num2_list = []
- for subplotspec in subplotspec_list2:
- rows, cols, num1, num2 = subplotspec.get_geometry()
- div_row, mod_row = divmod(max_nrows, rows)
- div_col, mod_col = divmod(max_ncols, cols)
- if mod_row != 0:
- cbook._warn_external('tight_layout not applied: number of rows '
- 'in subplot specifications must be '
- 'multiples of one another.')
- return {}
- if mod_col != 0:
- cbook._warn_external('tight_layout not applied: number of '
- 'columns in subplot specifications must be '
- 'multiples of one another.')
- return {}
- rowNum1, colNum1 = divmod(num1, cols)
- if num2 is None:
- rowNum2, colNum2 = rowNum1, colNum1
- else:
- rowNum2, colNum2 = divmod(num2, cols)
- num1num2_list.append((rowNum1 * div_row * max_ncols +
- colNum1 * div_col,
- ((rowNum2 + 1) * div_row - 1) * max_ncols +
- (colNum2 + 1) * div_col - 1))
- kwargs = auto_adjust_subplotpars(fig, renderer,
- nrows_ncols=(max_nrows, max_ncols),
- num1num2_list=num1num2_list,
- 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,
- nrows_ncols=(max_nrows, max_ncols),
- num1num2_list=num1num2_list,
- 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
|